三步实现模板引擎

582 阅读1分钟

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.index
    if (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