在前端的开发中经常会使用到模板引擎,前端渲染中比如前端框架Knockout、Vue和Angular(React的JSX不属于模板,它是一个带语法糖的手写 AST),在解析指令语法时都会使用对应的模板引擎进行解析,而基于Node.js进行渲染的EJS,DoTjs这些,原理也都是类似。
现在我们一步步来,看看它们是怎么实现的。
首先,我想它的使用方式是这样的:
|
|
当使用templateEngine这个函数,并传入一些参数,我们期待它得到
|
|
第一步我们要得到动态模板,也就是<%name%>这些,然后再用真实的数据去替换它。对于字符串的处理,我们很容易就想到使用正则来处理,关于正则的使用基础,可以看看前面的文章。好了,来写我们的第一个正则表达式,
|
|
通过这个正则,我们可以获取到以<%开头,以%>结尾的片段。用exec执行下,可以得到一个包含匹配结果的数组:
|
|
可以看到返回的数组里只包括一个匹配结果,所以这是我们还需要使用一个while循环来获取全部的结果。
|
|
好了,现在我们能够获取到所有的模板片段,只需要把数据填充进去就可以了,这里使用replace方法来实现。来看看我们最粗略的模板函数:
|
|
Boom,第一个模板函数实现了,是不是很简单?
这样足够了吗?然而并没有,考虑下面的情况,我们把输入数据变成这样:
|
|
当使用profile.job去填充数据的时候,发现使用不了了,因为并没有找到profile.job这个属性。要如何解决这个问题呢?
要是profile.job是一个语句就好了,这样就可以通过.操作符去获取到job属性。通过这个思路,也容易想到,在JavaScript中要把字符串当作语句来执行,可用的方式有eval和new Function,在这里我们就用new Function来试试。来看一下它的基本使用方式:
|
|
上面的代码等价于:
|
|
现在,我们可以定义一个函数,它的参数和函数体都来自于字符串,很棒,这正是我们想要的。在创建这个函数之前,我们还需要想想怎么构造出它的函数体?
这个函数体应该返回一个处理好的模板字符串,像是这样:
|
|
但是这种方式并不完美,因为如果我们想在模板里嵌套循环的话,这没法做,例如:
|
|
转化为等价函数:
|
|
报语法错误了!
怎么办?
最终我们是要返回一个字符串的,但是又想在返回语句里把真实的语法和字符串分开,并且支持for循环,这里可以先把字符串放入数组里,再通过join方法转化为字符串:
|
|
下一步,我们来重写我们的模板函数,为了方便在浏览器中测试,我们使用旧的语法:
|
|
这里要注意的是处理双引号那部分,如果不进行转义,那么它将是无效的js语法。运行一下上面的例子,看看它打印出什么来。
|
|
出了点意外,this.name和this.profile.job不是我们想要的,它们不应该被双引号包裹着。来修改一下add方法:
|
|
最后生成了:
|
|
在templateEngine函数的最后,就可以执行生成的函数体了:
|
|
我们使用apply方法来调用这个函数,它生成一个函数作用域,这样里面this就自动指向了data。
到现在我们已经做得非常好了,能够支持基本的js语法,分离出模板,最后我们还想支持更多的js语法,比如说if/else、for循环。
|
|
对于上面的模板就会抛出一个Uncaught SyntaxError. Unexpected token for语法错误。我们调试一下,在控制台上就会看到错误所在:
|
|
for循环不应该放入数组里面,我们再添加一个正则表达式,把if, for, else, switch, case, break, { 或者 } 开头的单独的放在一行,
|
|
增加一个正则处理后的结果像这样:
|
|
模板渲染后就是这样啦:
|
|
当然我们还可以使用更多复杂的表达式了,比如:
|
|
现在再把代码稍微整理下,得到一个最终版本的模板处理函数:
|
|
参考资料:
1、https://github.com/krasimir/absurd/blob/master/lib/processors/html/helpers/TemplateEngine.js