概念
模板引擎(这里指的时用于Web
开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML
文档
模板引擎不属于特定技术领域,它是跨领域平台的概念。在Asp
下有模板引擎,在PHP
也有模板引擎,在#C
下也有,甚至JavaScript
、WinForm
开发会用到的模板引擎技术。
举例:在一个ul
里插入li
,li
包含name
和age
,最后生成结构如下
- XXX - 20
实现方式:
1.拼接HTML
字符串
var html = '' + name + ' - ' + age + ' ';
var ulEl = document.getElementsByTagName('ul')[0];
ulEl.innerHTML = html;
2.构造DOM
var ulEl = document.getElementsByTagName('ul')[0];
var liEl = document.createElement('li');
liEl.textContent= name + ' - ' + age;
ulEl.appendChild(liEl);
但这种方式写起来比较麻烦,下面就在方式1的基础上进行优化
字符串格式化
如果想要字符串不包含变量,跟变量区分开,而是一个完整的字符串,不含加号,方便书写。这时就需要用特定的格式代替变量,格式化前后变化如下:
// before
var li = '' + obj[i].name + '-' + obj[i].age + ' ';
// after
var li = stringFormat('{0}-{1} ', obj[i].name, obj[i].age);
将字符串格式化需封装stringFormat
,变量用{}
括起来表示,传入参数的按顺序放入{0}
、{1}
上,由函数返回字符串
声明函数时,如果需传入多个参数就需要写多个形参,然而一个函数最多可以有255
个参数,参数太多书写时影响美观,直接传入需要处理的字符串即可
function stringFormat(str){
// arguments 是传入参数的类数组,不可直接使用数组的方法,不能直接写 arguments.slice(1)
var params = [].slice.call(arguments, 1);
// 用正则表达式将 {} 替换成对应变量的值
var reg = /\{(\d+)\}/g;
str= str.replace(reg, function(){
console.log(arguments);
var index = arguments[1];
return params[index];
});
return str;
}
console.log(stringFormat('{0} - {1}', 'xxx', 20));
运行结果如下
字符串格式化之后效率提升不少,但是还有个问题,如果传入的参数没按顺序,完全打乱,那出来的结果也不是预期中的,还需要进一步改进,接下来就要说到本文的重点了——模板引擎
模板引擎
模板引擎其实就是字符串格式化的升级版
在上述的例子基础上进行改进,可以将数据存到一个对象里,可将{0}
、{1}
分别替换成{name}
、{age}
,也就是对象里的属性。代码如下:
function stringFormat(str, data){
var params = [].slice.call(arguments, 1);
var reg = /\{(\S+)\}/g;
str= str.replace(reg, function(){
console.log(arguments);
var index = arguments[1];
return data[index];
});
return str;
}
var data = {
name: "Krasimir",
age: 29
};
console.log(stringFormat('{name} - {age}', data));
运行结果如下:
还有另一种方法,为跟上一个例子做点区分,将模板换成<%property%>
,于是可将{name}
、{age}
分别替换成<%name%>
、<%age%>
,用到了reg.exec
遍历查找传入字符串,匹配模板,然后用str.replace
替换成对应的属性值
先了解exec
的作用,代码和运行结果如下:
从结果可看出:
- 若
reg
不是全局匹配,则每次运行reg.exec(str)
得到的结果都是字符串中第一个匹配的数组 - 若
reg
是全局匹配,则每次运行reg.exec(str)
得到的结果都按顺序会往下匹配,当全部匹配完之后,再执行一次reg.exec(str)
则返回null
利用reg.exec
匹配之后再替换,代码如下:
var TemplateEngine = function(tpl, data){
var reg = /<%([^%>]+)?%>/g;
var match;
while(match = reg.exec(tpl)){
console.log(match);
tpl = tpl.replace(match[0], data[match[1]]);
}
return tpl;
};
var template = 'Hello, my name is <%name%>. I\'m <%age%> years old.
';
var data = {
name: "Krasimir",
age: 29
};
var string = TemplateEngine(template, data);
console.log(string);
运行结果如下:
一个简易版的模板引擎就实现了,但如果数据是这样的,如下所示:
var data = {
name: "Krasimir",
info: {
age: 29
}
};
要取出 Hello, my name is <%name%>. I\'m <%info.age%> years old.age
的值,传入的模板字符串改成var template = '
,TemplateEngine
里匹配到并返回的就是data["info.age"]
,这样得不到正确的age
属性值
再者,如果想插入多行字符串,上面这方法就得执行多次;如果想在字符串中插入JavaScript
代码,循环插入多行字符串,实现如下代码:
var template =
'My skills:' +
'<%if(this.showSkills) {%>' +
'<%for(var index in this.skills) {%>' +
'<%this.skills[index]%>' +
'<%}%>' +
'<%} else {%>' +
'none
' +
'<%}%>';
var string = TemplateEngine(template, {
skills: ["js", "html", "css"],
showSkills: true
})
document.body.innerHTML = string
也就是说TemplateEngine
函数输出的结果得是:
var line = "";
line += "My skills:";
if(this.showSkills) {
for(var index in this.skills) {
line += "";
line += this.skills[index];
line += "";
}
} else {
line += "none
";
}
return line;
返回的结果得当作JavaScript
代码来执行
怎么把字符串当作代码来执行呢?Function 构造函数了解一下
new Functon (arg1, arg2, ... argN, functionBody)
每个arg
都是一个参数,最后一个参数是函数主体(要执行的代码)。这些参数必须是字符串
举例:
function sayHi(sName, sMessage) {
alert("Hello " + sName + sMessage);
}
还可以定义成:
var sayHi = new Function("sName", "sMessage", "alert(\"Hello \" + sName + sMessage);");
函数主体字符串若涉及到双引号则需进行转义(\"
)
有了可以将字符串当成函数主体执行的函数构造器,于是TemplateEngine
的封装如下:
var TemplateEngine = function(tpl, data){
var reg = /<%([^%>]+)?%>/g;
var regJS = /^( )?(^if|else|switch|case|default|break|for|{|})(.*)/g;
var cursor = 0;
var code = 'var line = "";\n';
var match;
var isExp = function(str){
if(str.match(regJS)){
return str;
}else {
return 'line += ' + str + ';';
}
};
while(match = reg.exec(tpl)){
if(cursor !== match.index){
code += 'line += "' + tpl.slice(cursor, match.index).replace(/"/g, '\\"') + '";\n';
}
code += isExp(match[1])+'\n';
cursor = match.index + match[0].length;
}
code += tpl.substr(cursor, tpl.length - 1);
code += 'return line;';
console.log(code);
var str = new Function(code.replace(/[\r\t\n]/g, '')).apply(data);
return str;
};
var template =
'My skills:' +
'<%if(this.showSkills) {%>' +
'<%for(var index in this.skills) {%>' +
'<%this.skills[index]%>' +
'<%}%>' +
'<%} else {%>' +
'none
' +
'<%}%>';
var string = TemplateEngine(template, {
skills: ["js", "html", "css"],
showSkills: true
});
document.body.innerHTML = string;
控制台打印如下:
-
showSkills: true
文档中插入html
效果:
再打开调式看下生成的html
,如下所示:
-
showSkills: false
为了验证准确性,再将showSkills
改为false
文档中插入html
效果:
再打开调式看下生成的html
,如下所示:
更具体的思路可参考只有20行Javascript代码!手把手教你写一个页面模板引擎
原理
介绍了以上几种实现模板引擎的方法之后,可知其实现原理如下:
将模板和数据输入到模板引擎,执行该函数,输出结果即
HTML
代码段,再将该片段插入到文档中
模板引擎可用于任意一端,前后端即插即用,不局限于生成内容的语法,只要生成内容为字符串文本即可。然而,此模板引擎依赖于
innerHTML
,存在脚本注入的风险;且对于数据的更改,需要重新渲染模板,所以在初次渲染和之后的模板更新需要耗费同样的资源
参考
- 实现一个简单的模板引擎
- Juicer – 一个 JavaScript 模板引擎的实现和优化
- https://blog.csdn.net/kjfcpua/article/details/7295451
- ECMAScript Function 对象(类)