实现一个简单的模版引擎只需简单的正则匹配即可。
var TemplateEngine = function(tpl, data) {
// magic here ...
}
var template = '<p>Hello, my name is <%name%>. I\'m <%age%> years old.</p>';
console.log(TemplateEngine(template, {
name: "Krasimir",
age: 29
}));
通过上面一个函数,我想把< %去掉,并把变量赋值上去。
<p>Hello, my name is Krasimir. I'm 29 years old.</p>
这是我们想到了强大的正则,exec方法来遍历和替换。
这句正则表达式会捕获所有以<%开头,以%>结尾的片段。
var re = /<%([^%>]+)?%>/g;
var match = re.exec(tpl);
[
"<%name%>",
" name ",
index: 21,
input:
"<p>Hello, my name is <%name%>. I'm <%age%> years old.</p>"
]
有了上面的思路,我们很快就可以开始了。
var TemplateEngine = function(tpl, data) {
var re = /<%([^%>]+)?%>/g;
while(match = re.exec(tpl)) {
tpl = tpl.replace(match[0], data[match[1]])
}
return tpl;
}
//output: <p>Hello, my name is Krasimir. I'm 29 years old.</p>
问题来了,如果我给出的data是这种形式怎么办
{
name: "Krasimir Tsonev",
profile: { age: 29 }
}
var template = '<p>Hello, my name is <%name%>. I'm <%profile.age%> years old.</p>';
显然这个时候如果还用上面的那个方法,就会有问题。我们先来看一下这个
var fn = new Function("arg", "console.log(arg + 1);");
fn(2); // outputs 3
相信有一定基础的同学见过这种写法。
按照之前的想法,这个模板引擎最终返回的应该是一个编译好的模板。还是用之前的模板字符串作为例子,那么返回的内容应该类似于:
return
"<p>Hello, my name is " +
this.name +
". I\'m " +
this.profile.age +
" years old.</p>";
下一步就是收集模板里面不同的代码行,用于生成函数。
var TemplateEngine = function(tpl, data) {
var re = /<%([^%>]+)?%>/g,
code = 'var r=[];\n', // 存放代码的变量
cursor = 0;
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; // cursor会增加
}
add(tpl.substr(cursor, tpl.length - cursor));
code += 'return r.join("");'; // <-- return the result
console.log(code);
return tpl;
}
所以我们放心大胆的去运行下面这段代码
var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>';
console.log(TemplateEngine(template, {
name: "Krasimir Tsonev",
profile: { age: 29 }
}));
var r=[];
r.push("<p>Hello, my name is ");
r.push("this.name");
r.push(". I'm ");
r.push("this.profile.age");
return r.join("");
问题来了,这个this.name 根本不需要引号,它本身是代码。
this.name和this.profile.age不应该有引号啊,再来改改。
var add = function(line, js) {
js? code += 'r.push(' + line + ');\n' :
code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
}
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 TemplateEngine = function(tpl, data) {
var re = /<%([^%>]+)?%>/g,
code = 'var r=[];\n', // 存放代码的变量
cursor = 0;
``` 省略
add(tpl.substr(cursor, tpl.length - cursor));
code += 'return r.join("");'; // <-- return the result
console.log(code);
return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);
模板引擎接近完成了,不过还有一点,我们需要支持更多复杂的语句,比如条件判断和循环。我们接着上面的例子继续写。
var template =
'My skills:' +
'<%for(var index in this.skills) {%>' +
'<a href="#"><%this.skills[index]%></a>' +
'<%}%>';
console.log(TemplateEngine(template, {
skills: ["js", "html", "css"]
}));
这里按照之前的写法,会有问题,对应的for循环不应被当作字符串加进数组里。
var r=[];
r.push("My skills:");
r.push(for(var index in this.skills) {); 这里不需要push进数组
r.push("<a href=\"\">");
r.push(this.skills[index]);
r.push("</a>");
r.push(});
r.push("");
return r.join("");
带有for循环的那一行不应该被直接放到数组里面,而是应该作为脚本的一部分直接运行。所以我们在把内容添加到code变量之前还要多做一个判断。
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';
}
这里我们新增加了一个正则表达式。它会判断代码中是否包含if、for、else等等关键字。如果有的话就直接添加到脚本代码中去,否则就添加到数组中去。运行结果如下:
var r=[];
r.push("My skills:");
for(var index in this.skills) {
r.push("<a href="#">");
r.push(this.skills[index]);
r.push("</a>");
}
r.push("");
return r.join("");
//My skills:<a href="#">js</a><a href="#">html</a><a href="#">css</a>
最后一个改进可以使我们的模板引擎更为强大。我们可以直接在模板中使用复杂逻辑,例如:
var TemplateEngine = function(html, options) {
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 += 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行代码就实现了一个简单的模版引擎,大家有兴趣可以自己去实现更为复杂的。