刷朋友圈看到了一个不错的题目, 于是Google了一下, 找到一篇文章: JavaScript template engine in just 20 lines , 并不是逐字逐句翻译, 因此算是翻译+笔记吧.
1 2 3 4 5 6 7 8 var TemplateEngine = function (tpl, data ) { } var template = '<p>Hello, my name is <%name%>. I\'m <%age%> years old.</p>' ;console .log(TemplateEngine(template, { name: "Krasimir" , age: 29 }));
现在我们要实现TemplateEngine
函数, 由上可知, 该函数的两个参数为模板及数据. 执行上述代码后会出现以下结果:
1 <p > Hello, my name is Krasimir. I'm 29 years old.</p >
首先我们必须要获取模板中的动态变化部分, 之后将用二个参数中的真实数据替换动态变化部分的内容, 可以使用正则表达式实现.
1 var re = /<%([^%>]+)?%>/g ;
上面的表达式会提取所有以<%
为开头, %>
为结尾的部分内容, 末尾的g
(global)表示匹配所有项. 然后使用RegExp.prototype.exec() 方法, 将所有匹配的字符串存进一个数组中.
1 2 var re = /<%([^%>]+)?%>/g ;var match = re.exec(tpl);
输出match
得到这样的结果:
1 2 3 4 5 6 7 [ "<%name%>" , " name " , index: 21 , input: "<p>Hello, my name is <%name%>. I\'m <%age%> years old.</p>" ]
我们提取出了数据, 但是只得到一个数组元素, 我们需要处理的是所有匹配项, 因此使用while
循环实现:
1 2 3 4 var re = /<%([^%>]+)?%>/g , match;while (match = re.exec(tpl)) { console .log(match); }
执行上述代码之后会发现<%name%>
和<%age%>
都被提取出来了.
接下来要用真实的数据取代占位符. 最简单的方式是使用String.prototype.replace()
方法实现:
1 2 3 4 5 6 7 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"]
)就能够完成任务, 但是实际上会遇到更复杂的多层嵌套对象, 比如:
1 2 3 4 { name: "Krasimir Tsonev" , profile: { age : 29 } }
将函数的第二个参数改成上述形式之后, 使用以上的方法就没有办法解决问题了, 因为当我们输入<%profile.age%>
时, 得到的数据是[“profile.age”], 其值为undefined
. 此时replace()
方法不再适用. 如果对于在<%
和%>
之间的内容, 将其看成JavaScript代码, 可以直接执行并返回值, 那就比较好了, 比如:
1 var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>' ;
使用new Function()
语法, 构造函数:
1 2 var fn = new Function ("arg" , "console.log(arg + 1);" );fn(2 );
fn
函数接受一个参数, 其函数体为console.log(arg + 1)
, 上述的代码相当于:
1 2 3 4 var fn = function (arg ) { console .log(arg + 1 ); } fn(2 );
现在我们知道了可以通过上述方式由字符串构造出一个简单的函数. 不过在实现我们的需求时, 还需要花点时间思考如何构建我们所需的函数体. 该函数的功能是返回编译后的模板. 开始试试看如何实现:
1 2 3 4 5 6 return "<p>Hello, my name is " + this .name + ". I\'m " + this .profile.age + " years old.</p>" ;
将模板分离为由文本和JavaScript代码组成的部分. 利用简单的合并就可以获得预期的结果. 不过该方法还是无法100%符合我们的要求. 因为如果<%
和%>
之间的内容不是简单的变量, 而是其他更复杂的比如循环语句, 就无法获得预期结果, 例如:
1 2 3 4 5 var template = 'My skills:' + '<%for(var index in this.skills) {%>' + '<a href=""><%this.skills[index]%></a>' +'<%}%>' ;
如果使用简单的合并, 结果是这样的:
1 2 3 4 5 6 7 return 'My skills:' + for (var index in this .skills) { +'<a href="">' + this .skills[index] +'</a>' +}
这样的话会产生错误, for(var index in this.skills) {
无法正常执行, 因此采用另一种方式, 不要将所有内容添加到数组中, 而只将所需的内容添加, 最后合并数组:
1 2 3 4 5 6 7 8 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
)就能够实现我们想要得到的结果.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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("");' ; 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
变量中. 这之后我们会遇到一个棘手的问题, 需要利用转义解决双引号"
的问题:
1 2 3 4 5 6 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.profile.age
不应该被双引号引起. 可以这样改进add
函数来解决这个问题:
1 2 3 4 5 6 7 8 9 10 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 ); cursor = match.index + match[0 ].length; }
如果占位符的内容为JS代码, 则将其与布尔值true一同传入add
函数, 这样就可以得到我们预期的结果:
1 2 3 4 5 6 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
, 而是返回我们动态创建的函数:
1 return new Function (code.replace(/[\r\t\n]/g , '' )).apply(data);
不要在函数中直接传入参数, 利用apply
方法调用该函数并传入参数. 这样才会创建正确的作用域, this.name
才可正确执行, 此时this
指向data
对象.
最后我们还想在其中实现一些复杂的操作, 例如if/else
声明以及循环:
1 2 3 4 5 6 7 8 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
, 通过调试可以发现问题:
1 2 3 4 5 6 7 8 9 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
循环的那行代码不应该被添加到数组中, 于是我们这样进行改进:
1 2 3 4 5 6 7 8 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, { , }
这些内容为起始值, 则直接添加该行代码, 不添加到数组中. 那么最后的结果就是:
1 2 3 4 5 6 7 8 9 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("" );
这样的话, 所有的内容都被正确编译.
1 My skills:<a href ="#" > js</a > <a href ="#" > html</a > <a href ="#" > css</a >
最后的改进使函数功能更强大, 改进之后我们可以直接在模板里添加复杂逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 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 }));
添加了一些优化项的最终版本代码 就类似如下这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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); }
参考