handlebars 是一款优秀的模板引擎,其基本的使用方法如下:

const str = `My name is {{name}}, I'm {{age}} years old`const data = {name: 'keliq', age: 10}console.log(require('handlebars').compile(str)(data))// 得到:My name is keliq, I'm 10 years old复制代码

内部究竟是如何实现的呢?其实只需要三步:

第一步:解析模板

解析模板的目的就是把下面的字符串:

My name is {{name}}, I'm {{age}} years old复制代码

变成下面的数组:

[ 'My name is ', '{{name}}', ", I'm ", '{{age}}', ' years old' ]复制代码

解析函数如下:

var parse = (tpl) => {  let result, firstPos  const arr = []  while (result = /{{(.*?)}}/g.exec(tpl)) {
    firstPos = result.indexif (firstPos !== 0) {
      arr.push(tpl.substring(0, firstPos))
      tpl = tpl.slice(firstPos)
    }
    arr.push(result[0])
    tpl = tpl.slice(result[0].length)
  }  if (tpl) arr.push(tpl)  return arr
}复制代码

第二步:构造表达式

构造表达式就是把第一步得到的解析结果:

[ 'My name is ', '{{name}}', ", I'm ", '{{age}}', ' years old' ]复制代码

转换成下面的 JS 表达式:

""+"My name is "+data.name+", I'm "+data.age+" years old"复制代码

这一步的实现相对比较简单,就是拼字符串,代码如下:

const compileToString = (tokens) => {  let fnStr = `""`
  tokens.map(t => {if (t.startsWith("{{") && t.endsWith("}}")) {
      fnStr += `+data.${t.split(/{{|}}/).filter(Boolean)[0].trim()}`} else {
      fnStr += `+"${t}"`}
  })  return fnStr
}复制代码

第三步:创建渲染函数

我们在第二步已经得到了 JS 表达式,但本质上还是一个字符串而已:

""+"My name is "+data.name+", I'm "+data.age+" years old"复制代码

那如何执行呢?通过 new Function 动态创建函数可以做到这一点:

const compile = (tpl) => {  return new Function("data", "return " + compileToString(parse(tpl)))
}复制代码

这就实现 handlebars 的 compile 函数了,不妨运行一下看看:

console.log(compile(str)(data))// My name is keliq, I'm 10 years old复制代码