首先什么是模板引擎?
模板引擎 其实就是接收数据,把按照一定规则编写模板字符串,转换成 html 字符串。
const TemplateEngine = (tpl, data) => {
return xxx
}
var compiled = _.template('hello <%= user %>!');
compiled({ 'user': 'fred' });
// => 'hello fred!'
上面例子中 lodash 的 template 就实现了类似模板引擎的功能。
Vue.js 的 compiler 就是包含了模板引擎的功能。
// 来自 Vue.js 官网的例子
Vue.component('todo-item', {
// todo-item 组件现在接受一个
// "prop",类似于一个自定义 attribute。
// 这个 prop 名为 todo。
props: ['todo'],
template: '<li>{{ todo.text }}</li>'
})
上面的 <li>{{ todo.text }}</li> 就是模板字符串。
实现模板引擎
首先模板引擎的动态区域都有特定的规则,比如上面的 <% %>。
说明一下,下面的文字中的动态区域 都是特指 <% %>。
针对这个规则可以用正在匹配。
const tplStr = `hello <%user%>!`
const re = /<%([^%>]+)?%>/g;
re.exec(tplStr) 能够匹配出 user
这里说明一下正则式的 [^%>]。这是一个反向字符集,说明是不能匹配到中括号里面的 %>。这个正是我们上面写的模板字符串动态区域的。
(xx)? 是非贪婪匹配,这样就不会出现匹配到 <%foo%> barzzz <%bar%>
使用 re.exec 能够匹配动态区域了,但是实际情况是动态区域不止一个。因此要执行 re.exec 多次。
const re = /<%([^%>]+)?%>/g;
const tplStr = `<%foo%> barzzz <%bar%>`
let match
while(match = re.exec(tplStr)) {
console.log(match)
// <%foo%>
// <%bar%>
}
这样我们的模板引擎就可以按照最开始的写法
const TemplateEngine = (tpl, data) => {
const re = /<%([^%>]+)?%>/g;
let match;
while(match = re.exec(tpl)) {
tpl = tpl.replace(match[0], data[match[1]])
}
return tpl;
}
按照上面的方式,已经可以替换模板字符串成功了。
但是在我们实际开发过程中,比如 Vue.js 的模板支持 {{ todo.text }}
所以单纯替换是不够的。这时候需要语法运行。
接下来我们需要用到 new Function
var a = 1
var b = 2
var fn = new Function('return a+b')
fn() // 3
这就是 Vue.js 的模板 {{ a + b }} 支持 JavaScript 表达式的原因
支持 for...in 语法
假设现在我们需要处理的模板字符串是下面这样
var template =
'My skills:' +
'<%for(var index in this.skills) {%>' +
'<%this.skills[index]%>' +
'<%}%>';
替换掉 <% 和 %>并拼接起来,成为下面的形式。这样的字符串不是合法的语句,会报错。
return
'My skills:' +
for(var index in this.skills) { +
'' +
this.skills[index] +
'' +
}
我们需要将 for...in 的产出用其它的形式拼接。
定义一个数组来存储代码每一行。然后把存储代码信息的数组拼接起来。
var r = [];
r.push('My skills:');
for(var index in this.skills) {
r.push('');
r.push(this.skills[index]);
r.push('');
}
return r.join('');
new Function(body) body 就是上面代码字符串
省略干扰的代码就是 new Function("var r = []; return r;")
这里代码字符串就是引号里面的内容: var r= []; return r;
我把 r.push for in } 这些都省略了。
因此模板字符串转换成代码字符串的规则是:
普通字符: 'My skills:', 变成了代码 r.push('xxx')
普通的动态区域,即是没有 for...in, 变成了 r.push(this.skills[index])
特殊的动态区域,直接就是字符串 for (var index in this.skills) {
拼接代码字符串
将上面的规则写成代码就是
let code = 'var r=[];\\n';
const reExp = /(^( )?(for|if|{|}|;))(.*)?/g;
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;
}
add 函数的参数 js 就是一个标示,用来判断是不是动态区域。
reExp 判断是不是有关键字的动态区域
完整版 reExp
reExp = /(^( )?(var|if|for|else|switch|case|break|{|}|;))(.*)?/g
处理最初的模板字符串
为了生成代码字符串,我们需要定义一个索引 cursor。用来记录匹配和处理过的原始模板字符串的位置。
let match;
let cursor = 0;
while(match = re.exec(html)) {
add(html.slice(cursor, match.index))(match[1], true);
cursor = match.index + match[0].length;
}
// 处理剩余未被匹配的模板字符串
add(html.slice(cursor, html.length));
完整版简单的模板引擎
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.slice(cursor, html.length));
code = (code + 'return r.join("");').replace(/[\r\t\n]/g, ' ');
return new Function(code).apply(options)
}
var template =
'My skills:' +
'<%if(this.showSkills) {%>' +
'<%for(var index in this.skills) {%>' +
'<%this.skills[index]%>' +
'<%}%>' +
'<%} else {%>' +
'none' +
'<%}%>';
console.log(TemplateEngine(template, {
skills: ["js", "html", "css"],
showSkills: true
}));
注意:参考文章中的转义存在问题会导致 r.push 替换成了 .push
上面的版本是能够正常运行的
参考链接
lodash/lodash.js at 4.17.15 · lodash/lodash