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

首先什么是模板引擎?css

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

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

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

上面例子中 lodashtemplate 就实现了相似模板引擎的功能。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) 可以匹配出 useride

这里说明一下正则式的 [^%>]。这是一个反向字符集,说明是不能匹配到中括号里面的 %>。这个正是咱们上面写的模板字符串动态区域的。函数

(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

相关文章
相关标签/搜索