模板引擎的原理:实现一个简单的模板引擎

2,221 阅读4分钟

首先什么是模板引擎?

模板引擎 其实就是接收数据,把按照一定规则编写模板字符串,转换成 html 字符串。

const TemplateEngine = (tpl, data) => {
    return xxx
}

var compiled = _.template('hello <%= user %>!');
compiled({ 'user': 'fred' });
// => 'hello fred!'

上面例子中 lodashtemplate 就实现了类似模板引擎的功能。

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%>

special-negated-character-set - JavaScript | MDN

使用 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

Function - JavaScript | MDN

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

absurd/TemplateEngine.js at master · krasimir/absurd

JavaScript template engine in just 20 lines