- 作者:老汪软件技巧
- 发表时间: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行。
原文: