首先什么是模板引擎?css
模板引擎
其实就是接收数据,把按照必定规则编写模板字符串,转换成 html 字符串。html
const TemplateEngine = (tpl, data) => { return xxx } var compiled = _.template('hello <%= user %>!'); compiled({ 'user': 'fred' }); // => 'hello fred!' 复制代码
上面例子中 lodash
的 template
就实现了相似模板引擎的功能。git
Vue.js 的 compiler 就是包含了模板引擎的功能。github
// 来自 Vue.js 官网的例子 Vue.component('todo-item', { // todo-item 组件如今接受一个 // "prop",相似于一个自定义 attribute。 // 这个 prop 名为 todo。 props: ['todo'], template: '<li>{{ todo.text }}</li>' }) 复制代码
上面的 <li>{{ todo.text }}</li>
就是模板字符串。数组
首先模板引擎的动态区域都有特定的规则,好比上面的 <% %>
。bash
说明一下,下面的文字中的动态区域
都是特指 <% %>
。markdown
针对这个规则能够用正在匹配。app
const tplStr = `hello <%user%>!` const re = /<%([^%>]+)?%>/g; 复制代码
re.exec(tplStr)
可以匹配出 user
ide
这里说明一下正则式的 [^%>]
。这是一个反向字符集,说明是不能匹配到中括号里面的 %>
。这个正是咱们上面写的模板字符串动态区域的。函数
(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 表达式的缘由
假设如今咱们须要处理的模板字符串是下面这样
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