本文详细解析JavaScript模板引擎的工作原理与实现方法。通过正则表达式提取模板变量,将模板字符串转换为可执行的JavaScript代码,并利用Function构造函数动态生成渲染函数。文章涵盖基础变量替换、逻辑控制处理及数据注入机制,并提供了实际应用场景与优化建议,帮助开发者理解前端模板引擎的核心逻辑。
本文详细介绍JavaScript模板引擎的工作原理与实际应用方法,供读者参考学习。
一、前言
模板引擎的核心概念在于处理包含待定变量的字符串。例如:
var tpl = 'Hei, my name is <%name%>, and I\'m <%age%> years old.';
利用模板引擎函数将数据填充至模板中,
var data = {
"name": "Barret Lee",
"age": "20"
};
var result = tplEngine(tpl, data);
//Hei, my name is Barret Lee, and I'm 20 years old.
模板引擎的主要作用在于作为预处理器。对于熟悉PHP开发的开发者而言,Smarty模板引擎并不陌生。Smarty通过匹配tpl文件中的待处理字符并输出对应的HTML代码,结合高效的缓存技术,实现了优异的性能与易用性。JavaScript模板引擎遵循相同逻辑:数据库中存储大量数据时,每条数据通过统一模板生成输出。例如,无需在数据库中存储数千条完整的"Hei, my name..."字符串,只需保存对应的name和age字段,再通过模板引擎生成最终结果。
JavaScript模板引擎需要实现哪些功能?参考以下代码示例:
var tpl = '<% for(var i = 0; i < this.posts.length; i++) {' +
'var post = posts[i]; %>' +
'<% if(!post.expert){ %>' +
'<span>post is null</span>' +
'<% } else { %>' +
'<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" ><% post.expert %> at <% post.time %></a>' +
'<% } %>' +
'<% } %>';
一个基础的模板引擎需确保上述代码能正常解析。若输入数据为:
var data = {
"posts": [{
"expert": "content 1",
"time": "yesterday"
},{
"expert": "content 2",
"time": "today"
},{
"expert": "content 3",
"time": "tomorrow"
},{
"expert": "",
"time": "eee"
}]
};
则应输出:
<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >content 1 at yesterday</a>
<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >content 2 at today</a>
<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >content 3 at tomorrow</a>
<span>post is null</span>
接下来具体探讨该模板引擎的实现原理。
二、JS模板引擎的实现原理
1.正则抠出要匹配的内容
针对上述代码,通过正则表达式提取匹配内容:
var tpl = 'Hei, my name is <%name%>, and I\'m <%age%> years old.';
var data = {
"name": "Barret Lee",
"age": "20"
};
最简单的方式是利用replace函数:
var result = tpl.replace(/<%([^%>]+)?%>/g, function(s0, s1){
return data[s1];
});
通过正则替换,我们可以轻松获取result,读者可自行验证,这正是期望的结果。然而,若修改data和tpl,
var tpl = 'Hei, my name is <%name%>, and I\'m <%info.age%> years old.';
var data = {
"name": "Barret Lee",
"info": { age": "20"}
};
再次使用上述方式获取结果时,会发现失效。原因在于data["info.age"]本身为undefined,因此需要换一种处理方式,即将其转换为真正的JavaScript代码。例如:
return 'Hei, my name is ' + data.name + ', and I\'m ' + data.info.age' + ' years old.'
但随之出现新问题,当代码中包含for循环和if语句时,上述转换方式显然无效。例如:
var tpl = 'Posts: ' +
'<% for(var i = 0; i < post.length; i++) {'+
'<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" ><% post[i].expert %></a>' +
'<% } %>'
若继续采用上述方式,得到的结果为:
return 'Posts: ' +
for(var i = 0; i < post.length; i++) { +
'<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >' + post[i].exper + '</a>' +
}
这并非期望结果。稍加观察,若能返回如下结构则更为理想:
'Posts: '
for(var i = 0; i < post.length; i++) {
'<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >' + post[i].exper + '</a>'
}
但我们需要的是字符串而非零散片段,因此可将这些内容存入数组中。
2.装入数组
var r = [];
r.push('Posts: ' );
r.push(for(var i = 0; i < post.length; i++) {);
r.push('<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >');
r.push(post[i].exper);
r.push('</a>');
r.push(});
有人可能指出上述代码中第三行与最后一行逻辑存在错误,如何解决?处理很简单,即不将这些部分放入数组中:
var r = [];
r.push('Posts: ' );
for(var i = 0; i < post.length; i++) {
r.push('<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >');
r.push(post[i].exper);
r.push('</a>');
}
如此逻辑便较为完善,漏洞较少。但转化过程如何实现?仍需编写模板解析函数。
3.分辨js逻辑部分
var r = [];
tpl.replace(/<%([^%>]+)?%>/g, function(s0, s1){
//完蛋了,这里貌似又要回到上面那可笑的逻辑有错误的一步啦... 该怎么处理比较好?
});
此处似乎又回到之前逻辑错误的困境。如何处理更为妥当?我们知道,JavaScript提供了构造函数的“类”:
var fn = new Function("data",
"var r = []; for(var i in data){ r.push(data[i]); } return r.join(' ')");
fn({"name": "barretlee", "age": "20"}); // barretlee 20
明确这一点后,可将逻辑部分与非逻辑部分的代码链接为字符串,再利用类似fn的函数直接编译代码。而/<%([^%>]+)?%>/g这一正则仅能匹配逻辑部分,要组合所有代码,还需匹配非逻辑部分。replace函数虽强大,但实现逻辑晦涩,因此采用其他方式处理。
先看一个简单示例:
var reg = /<%([^%>]+)?%>/g;
var tpl = 'Hei, my name is <%name%>, and I\'m <%age%> years old.';
var match = reg.exec(tpl);
console.log(match);
输出结果为:
[
0: "<%name%>",
1: name,
index: 16,
input: "Hei, my name is <%name%>, and I'm <%age%> years old."
length: 2
]
我们期望获取所有匹配项,但上述方式仅获取name而忽略age。熟悉正则表达式的读者应知处理方式:
var reg = /<%([^%>]+)?%>/g;
while(match = reg.exec(tpl)) {
console.log(match);
}
关于正则表达式的内容不再赘述,有兴趣者可深入了解match、exec、search等相关函数。此处主要依靠match的index属性定位遍历位置,并利用while循环获取所有内容。
4.引擎函数
因此,引擎函数的雏形大致如下:
var tplEngine = function(tpl, data){
var reg = /<%([^%>]+)?%>/g,
code = 'var r=[];\n',
cursor = 0; //主要的作用是定位代码最后一截
var add = function(line) {
code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
};
while(match = reg.exec(tpl)) {
add(tpl.slice(cursor, match.index)); //添加非逻辑部分
add(match[1]); //添加逻辑部分 match[0] = "<%" + match[1] + "%>";
cursor = match.index + match[0].length;
}
add(tpl.substr(cursor, tpl.length - cursor)); //代码的最后一截 如:" years old."
code += 'return r.join("");'; // 返回结果,在这里我们就拿到了装入数组后的代码
console.log(code);
return tpl;
};
测试一个小demo:
var tpl = '<% for(var i = 0; i < this.posts.length; i++) {' +
'var post = posts[i]; %>' +
'<% if(!post.expert){ %>' +
'<span>post is null</span>' +
'<% } else { %>' +
'<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" ><% post.expert %> at <% post.time %></a>' +
'<% } %>' +
'<% } %>';
tplEngine(tpl, data);
返回结果令人满意:
var r=[];
r.push("");
r.push(" for(var i = 0; i < this.posts.length; i++) {var post = posts[i]; ");
r.push("");
r.push(" if(!post.expert){ ");
r.push("<span>post is null</span>");
r.push(" } else { ");
r.push("<a href=\"#\">");
r.push(" post.expert ");
r.push(" at ");
r.push(" post.time ");
r.push("</a>");
r.push(" } ");
r.push("");
r.push(" } ");
r.push("");
return r.join("");
但我们不需要将for、if、switch等逻辑代码push到r数组中,因此需改进上述代码。若line中包含JavaScript逻辑代码,则不应将其加入:
regOut = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g;
var add = function(line, js) {
js? code += line.match(regOut) ? line + '\n' : 'r.push(' + line + ');\n' :
code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
};
最后一步工作是将data注入。
5.把data扔进去
完成此部分并不复杂,通过前文对Function函数的讲解,读者应已掌握方法。
return new Function(code).apply(data);
使用apply的作用是将code中的变量作用域绑定到data上,避免作用域指向global导致数据索引出错。当然可进一步优化:
return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);
匹配并移除回车换行及tab键,使代码更简洁。最终代码如下:
var tplEngine = function(tpl, data) {
var reg = /<%([^%>]+)?%>/g,
regOut = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g,
code = 'var r=[];\n',
cursor = 0;
var add = function(line, js) {
js? (code += line.match(regOut) ? line + '\n' : 'r.push(' + line + ');\n') :
(code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '');
return add;
}
while(match = reg.exec(tpl)) {
add(tpl.slice(cursor, match.index))(match[1], true);
cursor = match.index + match[0].length;
}
add(tpl.substr(cursor, tpl.length - cursor));
code += 'return r.join("");';
return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);
};
三、应用场景
作为前端代码,模板引擎旨在服务前端。通常处理HTML模板,模板代码多置于script标签或textarea中。首先需获取这些内容,再进行解析。
var barretTpl = function(str, data) {
//获取元素
var element = document.getElementById(str);
if (element) {
//textarea或input则取value,其它情况取innerHTML
var html = /^(textarea|input)$/i.test(element.nodeName) ? element.value : element.innerHTML;
return tplEngine(html, data);
} else {
//是模板字符串,则生成一个函数
//如果直接传入字符串作为模板,则可能变化过多,因此不考虑缓存
return tplEngine(str, data);
}
var tplEngine = function(tpl, data) {
// content above
};
};
使用方式更为简便:barretTpl(str, data),其中str可以是模板代码,也可以是DOM元素的id。
四、优化以及功能拓展
代码仅三四十行,完成的是简洁版实现,但对于简单页面已足够。若需优化,可考虑以下方面:
- 优化获取的模板代码,例如去除行尾空格等。
- 符号转义。若需输出
<span>hehe</span>等类似源代码,在push前必须进行转义处理。 - 代码缓存。若模板频繁使用,可将其存入barretTpl闭包内的数组中缓存。
- 允许用户自定义分隔符。
更多关于JavaScript相关内容可查看本站专题:《javascript面向对象入门教程》、《JavaScript切换特效与技巧总结》、《JavaScript查找算法技巧总结》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》
希望本文所述对大家JavaScript程序设计有所帮助。