来吧,造个模板引擎轮子——实现功能

上篇上上篇上上上篇,这一篇将介绍一些模板语言的feature怎么实现,不会都说,会介绍一些比较重要的,思路优先,代码不多。

函数

函数是现代编程语言当中最最最基本的一种代码组织和复用方式了,可以说模板语言作为一种“语言”,这个功能也是必不可少的。上篇文章留了个尾巴,这篇自然从函数开始。

函数定义

首先确定一下语法,这一篇文章中用我的轮子raze-tpl的语法:

1
2
3
4
5
6
@func userInfo(user) {
<li class="user">
<span class="name">@(user.name)</span>
<span class="email"><a href="mailto:@(email)">@(user.email)</a></span>
</li>
}

在前作当中已经能够识别这整个语法结构

1
2
3
函数定义 函数名(参数列表) {
函数体
}

对它生成代码其实就不难了,而且因为JS里函数套函数的特性,我们生成的代码天然就可以访问闭包变量,这样的特性和JS非常相似,上手难度位0.

1
2
3
4
5
function ___custom_func_函数名(参数列表) {
var __result_asdfghjkl = '';
函数体
return __result_asdfghjkl;
}

这里需要注意的是,函数体并不能直接像生成if/foreach那种方式,将结果拼接到整个render函数的结果上,而是需要自己独立维护一个新的结果,并将其返回。

值得注意的是,我们依然需要对函数体内的表达式进行变量名解析,例如上面的例子会解析出user, name, email三个变量名。其中,user是参数列表中已经包含,所以要将其“白名单”,不然再定义一次,参数很容易瞎。而如果这里出现对闭包变量的使用,也会被解析到变量名,如果在函数体内生成var变量声明语句,就违背了我们想要实现对闭包变量访问的初衷。所以一个比较无脑的办法就是,把函数内的变量名声明也提高到render函数的级别。例如

1
2
3
4
5
6
7
8
9
10
11
12
13
function ___render(data) {
var userList = data['userList']; // 这个是函数调用的参数表达式解析出来的变量名
var name = data['name']; // 这个是无用功
var email = data['email']; // 这个也是无用功
var closure_variable = data['closure_variable']; // 这个是将下文的闭包变量提前声明的结果

function ___custom_func_userInfo(user) {
// 这里面就可以正常访问 clousure_variable这个闭包变量了
// 同时,函数内访问“全局”变量(也就是data参数的字段
// 也可以被提前到 render 函数的级别,正常访问到
// 因为它会被当成闭包里的变量
}
}

函数调用

相比之下这个就比函数声明要简单多了,函数调用在raze-tpl里的语法是

1
2
3
4
5
<ul class="user-list">
@foreach(i:user in userList) {
@use userInfo(user)
}
</ul>

上面的@use代码被解析为

1
函数调用 函数名(参数列表)

生成的代码就是

1
__result += ___custom_func_userInfo(user);

你可以看到其实参数列表是不用动的,当然对它进行变量名分析的过程也还是需要。

block/override

本质上说,block其实也是一个函数定义,只不过它在定义的时候就已经确定了调用位置。例如

定义:

1
2
3
@block pageBody {
<span>这个block默认只有这么一个空的span</span>
}

覆盖

1
2
3
@override (pageBody) {
<strong>这里重写了pageBody这个block的定义,将其改为了一个strong</strong>
}

聪明的你一定早就找到了答案,首先我们可以使用正常的函数定义的方法来将@block定义为一个函数,并且原地生成一条函数调用语句。

1
2
3
4
function ___custom_block_pageBody() {
内容
}
__$result += ___custom_block_pageBody();

然后对于@override,我们照样生成一个函数定义。

1
2
3
function ___custom_block_pageBody() {
新的内容
}

由于这俩都是“函数声明”,利用JS的语言特性,“声明”会被提前(而函数表达式则不会),那么两个同名函数的声明都会被提前到它们被调用之前;又因为后定义压过先定义,结果后来通过@override重新声明的函数会成为最后的输出——这是一种比较讨巧的做法。

模板继承

Smarty 3里面加入了模板继承功能,好顶赞,例如首先定义一个layout.html,在里面把框架搭好,埋几个需要被覆写的地方为几个block,然后在xxx-page.html里面,extend layout.html,再按需覆写其中某些block

利用JS灵活的动态特性,实现这个是比较容易的。

首先当我们解析到@extend的时候,就要改变策略,当前模板的解析结果不要作为render函数返回了,而是将@extend的父模板作为render函数,但当前模板也依然需要进行解析,因为当中的@override是要作用到layout.html里所定义的那些@block身上的。

说起来似乎很麻烦,不过如果你真的在动手造轮子,聪明的你一定会有办法的。

filter

大多数模板引擎对于变量输出语法都支持filter,比如:

1
<span>@(phoneNumber | secureMobilePhoneDisplay)</span>

将会对phoneNumber这个变量调用secureMobilePhoneDisplay这个filter函数,可以将手机号变成158xxxx1024这样的格式。

这个功能非常实用,实现起来也不复杂,只需要对变量输出语法进行进一步解析就行了。对整个变量输出表达式按|进行split,然后把第一段当做输出源,后面都当做filter管道就行,filter可能带参数,比如@(text | replace(/\d+/g, '>>$0<<'),需要对它进行进一步解析,具体不表……

当然麻烦之处也在于此,因为变量输出语法当中本身可能有一些表达式已经包含了|这个符号。而我们能做的则很有限,首先需要处理一下||运算符,这个可以用split结果推导也可以逐字分析,正则高手也许直接split就行。

但在遇到单个|的时候,就瞎逼了,它可能出现在字符串里,比如我们需要用|来拼面包屑或者做分隔符的时候;甚至可能是按位或运算符……前者的情况可以在模板里用HTML转义字符,后者的情况就让它瞎去吧。当然其实可以使用代码块或者数据预处理解决问题,不过还是求你饶前端一条小命吧。

其他

注释

这个太好用了,也是必须要有的,不然半月不见代码必瞎无疑。对于使用定界符的模板,一般来说都是前后*这样,比如<%**%>之间就是注释。

对于raze-tpl,这个也完全可行,@* xxx *@中间就可以是注释,当然既然语法这么骚,也可以实现一个单行注释,比如@// xxxxx这样,会更方便一些。

块注释实现很简单,就是当发现了一个“块注释开始”的模板语句时,parser立即进入“注释”状态,中间忽略一切内容,直到匹配到一个“块注释结束”语句为止。

转义字符

对于使用定界符的模板,转义字符可有可无,毕竟<%/%>这类的东西正常代码里是比较难找的。但对于raze-tpl来说,因为它的leading char只有@这么简单,还是挺容易重的,比如email里就有,所以我安排了两个转义字符,一个是@@转义为@,一个是@}转义为}。尤其是后者,在括号匹配的时候要特殊处理一下,不要把它当模板语法结构里面的}给处理了。

literal block

以Smarty为例,很多人应该遇到过这样的问题,把定界符设置位了{{}},结果当模板里内联了一段JS或者CSS(而且它还是被压缩过的)的时候就很容易瞎。于是Smarty就弄了一个literal block,里面的所有东西都会被当纯文本,不会解析为模板。

实现literal block的方式和实现块注释几乎一样,不再赘述。

raze-tpl里实现了@# xxx #@作为literal block,聊胜于无。

小结 && 总结

到此为止,可以说已经可以造出一个全功能的模板引擎轮子,事实上所谓“纸上得来终觉浅,绝知此事要宫刑躬行”,光说不练还是不如真正造起轮子来过瘾。

可以发现模板引擎虽然是“前端”轮子,但是对狭义的前端技术——也就是HTML/JS/CSS这些——涉及甚少,基本上说除了用JavaScript以外,跟前端没啥关系其实。所以换种语言接着再造一个也可以啊(¯﹃¯)

不过为了对生产的render函数进行惨无人道的优化,还是需要用到一些JS的奇技淫巧的,比如对古老浏览器拼接字符串的优化;比如通过变量解析,提前变量声明来做到更快速的变量访问;或者做到etpl那样的“安全变量访问”;也可能是像上文中所说的在实现函数定义的时候充分借助JS自带闭包特性等等——这提示我们:选择一门语言来实现模板引擎的时候,模板语言本身的语言特性设定也是从这个实现语言出发的,这样实现的时候会事半功倍,在“学习模板语言”的时候面对的“概念冲击”也会更少。

然后吐槽一下:hexo什么尼玛代码染色,风骚的语法尽毁。

好了我要开始想我的下一个轮子了……(逃