• 作者:老汪软件技巧
  • 发表时间:2024-11-16 07:02
  • 浏览量:

var TemplateEngine = function(tpl, data) {
    // magic here ...
}
var template = 'Hello, my name is <%name%>. I\'m <%age%> years old.';
console.log(TemplateEngine(template, {
    name: "Krasimir",
    age: 29
}));

一个简单的函数,它接受我们的模板和一个数据对象。你可能猜到了,我们最后想要达到的结果是:

Hello, my name is Krasimir. I'm 29 years old.

我们要做的第一件事就是把动态块放入模板中。稍后我们将用传递给引擎的真实数据替换它们。我决定使用正则表达式来实现这一点。

var re = /<%([^%>]+)?%>/g;

我们将捕获所有以结束的片段。标志g(全局)意味着我们将得到不是一个匹配,而是所有匹配。

var re = /<%([^%>]+)?%>/g;
var match = re.exec(tpl);

如果我们console.log匹配变量,我们将得到:

[
    "<%name%>",
    " name ", 
    index: 21,
    input: 
    "Hello, my name is <%name%>. I\'m <%age%> years old."
]

因此,我们得到了数据,但正如您所看到的,返回的数组只有一个元素。我们需要处理所有的匹配。要做到这一点,我们应该将逻辑包装到while循环中。

var re = /<%([^%>]+)?%>/g, match;
while(match = re.exec(tpl)) {
    console.log(match);
}

如果运行上面的代码,您将看到同时显示和。

现在它变得有趣了。我们用真实数据替换占位符。我们可以这样写:

var TemplateEngine = function(tpl, data) {
    var re = /<%([^%>]+)?%>/g, match;
    while(match = re.exec(tpl)) {
        tpl = tpl.replace(match[0], data[match[1]])
    }
    return tpl;
}

好的,这是可行的,但当然这还不够。我们有非常简单的对象,并且很容易使用data["property"]。但在实践中,我们可能有复杂的嵌套对象。例如,我们将数据更改为:

{
    name: "Krasimir Tsonev",
    profile: { age: 29 }
}

这不起作用,因为当我们键入我们将得到data["profile.age"]这实际上是没有定义的。replace方法在我们的例子中不起作用。所以,我们需要别的东西。最好的做法是将真正的JavaScript代码放在之间。如果它是根据传递的数据计算的,那就太好了。例如:

var template = 'Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.';

这怎么可能呢?即从字符串创建一个函数。让我们看一个简单的例子。

var fn = new Function("arg", "console.log(arg + 1);");
fn(2); // outputs 3

fn是一个实函数,只有一个参数。它的主体是console.log(arg+1);换句话说,上面的代码等于:

var fn = function(arg) {
    console.log(arg + 1);
}
fn(2); // outputs 3

我们可以用简单的字符串定义一个函数。这正是我们所需要的。但是在创建这样一个函数之前,我们需要构造它的函数体。该方法应该返回最终编译的模板。让我们得到到目前为止使用的字符串,并试着想象它的样子。

return "Hello, my name is " + this.name + ". I\'m " + this.profile.age + "years old.";

当然,我们将把模板分成文本和有意义的JavaScript。正如你在上面看到的,我们可以使用一个简单的连接并产生想要的结果。然而,这种方法并不完全符合我们的需求。

var template = 
'My skills:' + 
'<%for(var index in this.skills) {%>' + 
'<%this.skills[index]%>' +
'<%}%>';

如果我们使用方法连接,结果将是:

return
'My skills:' + 
for(var index in this.skills) { +
'' + 
this.skills[index] +
'' +
}

把所有的字符串放在一个数组中,并在数组的末尾连接它的元素。

var r = [];
r.push('My skills:'); 
for(var index in this.skills) {
    r.push('');
    r.push(this.skills[index]);
    r.push('');
}
return r.join('');

我们已经从模板中提取了一些信息。我们知道占位符的内容及其位置。因此,通过使用辅助变量(游标),我们能够产生期望的结果。

var TemplateEngine = function(tpl, data) {
    var re = /<%([^%>]+)?%>/g,
        code = 'var r=[];\n',
        cursor = 0, match;
    var add = function(line) {
        code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
    }
    while(match = re.exec(tpl)) {
        add(tpl.slice(cursor, match.index));
        add(match[1]);
        cursor = match.index + match[0].length;
    }
    add(tpl.substr(cursor, tpl.length - cursor));
    code += 'return r.join("");'; // <-- return the result
    console.log(code);
    return tpl;
}
var template = 'Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.';
console.log(TemplateEngine(template, {
    name: "Krasimir Tsonev",

    profile: { age: 29 }
}));

code变量保存函数体。正如我所说,光标显示我们在模板中的位置。我们需要这样一个变量来遍历整个字符串并跳过数据块。创建了一个附加的add函数。它的工作是向代码变量追加行。

var r=[];
r.push(

Hello, my name is "); r.push("this.name"); r.push(". I'm "); r.push("this.profile.age"); return r.join("");

嗯……这不是我们想要的。This.name和this.profile.age不应该被引用。对add方法稍加改进就解决了这个问题。

var add = function(line, js) {
    js? code += 'r.push(' + line + ');\n' :
        code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
}
var match;
while(match = re.exec(tpl)) {
    add(tpl.slice(cursor, match.index));
    add(match[1], true); // <-- say that this is actually valid js
    cursor = match.index + match[0].length;
}

占位符的内容与一个布尔变量一起传递。这就生成了正确的主体。

var r=[];
r.push("

Hello, my name is "); r.push(this.name); r.push(". I'm "); r.push(this.profile.age); return r.join("");

我们所需要做的就是创建函数并执行它。

return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);

我们甚至不需要向函数发送任何参数。我们使用apply方法来调用它。它会自动设置作用域。

我们快做完了。最后一件事。我们需要支持更复杂的操作,比如if/else语句和循环。

var template = 
'My skills:' + 
'<%for(var index in this.skills) {%>' + 
'<%this.skills[index]%>' +
'<%}%>';
console.log(TemplateEngine(template, {
    skills: ["js", "html", "css"]
}));

结果是一个错误Uncaught SyntaxError: Unexpected token for。如果我们调试一点并打印出代码变量,我们就会看到问题所在。

var r=[];
r.push("My skills:");
r.push(for(var index in this.skills) {);
r.push("");
r.push(this.skills[index]);
r.push("");
r.push(});
r.push("");
return r.join("");

包含for循环的行不应该被压入数组。它应该只是放置在脚本中。为了实现这一点,我们必须在附加代码之前再进行一次检查。

var re = /<%([^%>]+)?%>/g,
    reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g,
    code = 'var r=[];\n',
    cursor = 0;
var add = function(line, js) {
    js? code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n' :
        code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
}

添加了一个新的正则表达式。它告诉我们javascript代码是否以if, for, else, switch, case, break,{or}开头。如果是,那么它只是添加一行。否则,它将它包装在一个push语句中。结果是:

var r=[];
r.push("My skills:");
for(var index in this.skills) {
    r.push("");
    r.push(this.skills[index]);
    r.push("");
}
r.push("");
return r.join("");

当然,一切都是正确编译的。

My skills:jshtmlcss

我们可以将复杂的逻辑直接应用到模板中。例如:

var template = 
'My skills:' + 
'<%if(this.showSkills) {%>' +
    '<%for(var index in this.skills) {%>' + 
    '<%this.skills[index]%>' +
    '<%}%>' +
'<%} else {%>' +
    '

none

'
+ '<%}%>'; console.log(TemplateEngine(template, { skills: ["js", "html", "css"], showSkills: true }));

最终版本看起来是这样的:

var TemplateEngine = function(html, options) {
    var re = /<%([^%>]+)?%>/g, reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g, code = 'var r=[];\n', cursor = 0, match;
    var add = function(line, js) {
        js? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n') :
            (code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '');
        return add;
    }
    while(match = re.exec(html)) {
        add(html.slice(cursor, match.index))(match[1], true);
        cursor = match.index + match[0].length;
    }
    add(html.substr(cursor, html.length - cursor));
    code += 'return r.join("");';
    return new Function(code.replace(/[\r\t\n]/g, '')).apply(options);
}

更少,15行。

原文:


上一条查看详情 +JDK 17 源码阅读 - 数据类型 - 02 - Short
下一条 查看详情 +没有了