20行代码实现JavaScript模板引擎

本文首发于我的博客: icyfish.mejavascript

正文

刷朋友圈看到了一个不错的题目, 因而Google了一下, 找到一篇文章: JavaScript template engine in just 20 lines, 并非逐字逐句翻译, 所以算是翻译+笔记吧.css

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
}));复制代码

如今咱们要实现TemplateEngine函数, 由上可知, 该函数的两个参数为模板及数据. 执行上述代码后会出现如下结果: html

<p>Hello, my name is Krasimir. I'm 29 years old.</p>复制代码

首先咱们必需要获取模板中的动态变化部分, 以后将用二个参数中的真实数据替换动态变化部分的内容, 可使用正则表达式实现.java

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

上面的表达式会提取全部以<%为开头, %>为结尾的部份内容, 末尾的g(global)表示匹配全部项. 而后使用RegExp.prototype.exec()方法, 将全部匹配的字符串存进一个数组中.git

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

输出match获得这样的结果:github

[
    "<%name%>",
    " name ", 
    index: 21,
    input: 
    "<p>Hello, my name is <%name%>. I\'m <%age%> years old.</p>"
]复制代码

咱们提取出了数据, 可是只获得一个数组元素, 咱们须要处理的是全部匹配项, 所以使用while循环实现:正则表达式

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

执行上述代码以后会发现<%name%><%age%>都被提取出来了.数组

接下来要用真实的数据取代占位符. 最简单的方式是使用String.prototype.replace()方法实现:app

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 }
}复制代码

将函数的第二个参数改为上述形式以后, 使用以上的方法就没有办法解决问题了, 由于当咱们输入<%profile.age%>时, 获得的数据是["profile.age"], 其值为undefined. 此时replace()方法再也不适用. 若是对于在<%%>之间的内容, 将其当作JavaScript代码, 能够直接执行并返回值, 那就比较好了, 好比:

var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>';复制代码

使用new Function()语法, 构造函数:

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 
"<p>Hello, my name is " + 
this.name + 
". I\'m " + 
this.profile.age + 
" years old.</p>";复制代码

将模板分离为由文本和JavaScript代码组成的部分. 利用简单的合并就能够得到预期的结果. 不过该方法仍是没法100%符合咱们的要求. 由于若是<%%>之间的内容不是简单的变量, 而是其余更复杂的好比循环语句, 就没法得到预期结果, 例如:

var template = 
'My skills:' + 
'<%for(var index in this.skills) {%>' + 
'<a href=""><%this.skills[index]%></a>' +
'<%}%>';复制代码

若是使用简单的合并, 结果是这样的:

return
'My skills:' + 
for(var index in this.skills) { +
'<a href="">' + 
this.skills[index] +
'</a>' +
}复制代码

这样的话会产生错误, for(var index in this.skills) {没法正常执行, 所以采用另外一种方式, 不要将全部内容添加到数组中, 而只将所需的内容添加, 最后合并数组:

var r = [];
r.push('My skills:'); 
for(var index in this.skills) {
r.push('<a href="">');
r.push(this.skills[index]);
r.push('</a>');
}
return r.join('');复制代码

所以接下来的步骤是在构造的函数体中根据状况添加各行代码, 以前咱们已从模板中提取出一些相关的信息: 占位符的内容以及它们所处的位置. 那么, 再定义一个辅助的变量(cursor)就可以实现咱们想要获得的结果.

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 = '<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 }
}));复制代码

code变量的值为咱们本身构造的函数的函数体, 函数体中首先定义了一个空数组. 能够经过cursor变量存储<%this.name%>这种形式的内容以后的文字处于模板中的位置索引值. 而后咱们又建立了add函数, 利用这个函数能够添加各行代码到code变量中. 这以后咱们会遇到一个棘手的问题, 须要利用转义解决双引号"的问题:

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.namethis.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;
}复制代码

若是占位符的内容为JS代码, 则将其与布尔值true一同传入add函数, 这样就能够获得咱们预期的结果:

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("");复制代码

而后咱们须要作的就是建立这个函数并执行. 在TemplateEngine函数中不返回tpl, 而是返回咱们动态建立的函数:

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

不要在函数中直接传入参数, 利用apply方法调用该函数并传入参数. 这样才会建立正确的做用域, this.name才可正确执行, 此时this指向data对象.

最后咱们还想在其中实现一些复杂的操做, 例如if/else声明以及循环:

var template = 
'My skills:' + 
'<%for(var index in this.skills) {%>' + 
'<a href="#"><%this.skills[index]%></a>' +
'<%}%>';
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("<a href=\"\">");
r.push(this.skills[index]);
r.push("</a>");
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';
}复制代码

上述代码添加了一个新的正则表达式, 若是JS代码以if, for, else, switch, case, break, { , }这些内容为起始值, 则直接添加该行代码, 不添加到数组中. 那么最后的结果就是:

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 template = 
'My skills:' + 
'<%if(this.showSkills) {%>' +
    '<%for(var index in this.skills) {%>' + 
    '<a href="#"><%this.skills[index]%></a>' +
    '<%}%>' +
'<%} else {%>' +
    '<p>none</p>' +
'<%}%>';
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);
}复制代码

参考

相关文章
相关标签/搜索