@yangfch3
2016-11-30T12:07:12.000000Z
字数 6480
阅读 5866
FE
近日,阅读了 artTemplate 模板引擎的源码,引擎的整个结构十分优雅,代码组织得十分清晰。整个库加上注释才 700 余行,十分适合用于阅读与学习。
关于模板引擎的原理与实现思路网上有着许多系列文章,github 上也有着大量的实现可供参考。这篇文章的任务就是带你去阅读 artTemplate 这一款优秀的模板引擎,让你的阅读更加清晰。推荐先阅读一遍源码再来阅读此文。
每个引擎的实现都需要牵涉到以下五个点:
拿一个简单的例子来说说:我们需要将下面的 tpl 利用 model 数据正确地编译渲染输出为 下方的html
// tpl<h3><% if (typeof content === 'string') { %><=% content %><% } %><h3>// HTML// render({content: 'a h3 title'})'<h3>a h3 title</h3>'
首先上面的 tpl 已经为我们 制定好了模板的语法规则,逻辑语句 <% %>,赋值语句 <%= %>。
这其中我们必然需要通过 词法分析 与 语法解析,使得我们的 tpl 解析(拼接)为一段可行的 JavaScript 代码字符串。
$html += '<h3>';if (typeof content === 'string') {$html += content;}$html += '</h3>';
上面的 代码字符串 还没有实质性的作用,一段 代码字符串 需要真的执行需要借助能使用文本访问 JavaScript 解析引擎的方法:
我们不难想到,必然需要使用 Function 构造函数 将 代码字符串 做为函数体构建(编译)为真正可执行的代码(函数),为此我们借助 Function 构造函数 来构建(编译出)我们的 渲染方法,同时在渲染方法中需要完成变量赋值操作
var render = (function() {var cache ="var $html = '';\with($data) {\$html += '<h3>';\if (typeof content === 'string') {\$html += content;\}\$html += '</h3>';\}\return $html;\";return function (data) {var renderfn = new Function('$data', cache);return fn(data);}})();
最后,我们得到了 render 方法,直接调用吧:
render({content: 'a h3 title'});// <h3>a h3 title</h3>
上面介绍了模板引擎的基本作用过程,我们不难发现,模板引擎里最核心实现部分是:
语法解析器(parser)负责将模板语言转为 代码字符串,编译器(compilers)负责使用 代码字符串 完成渲染方法的构建。
图中我们能看到整个模板引擎的设计理念、调用栈和数据流动,可以看到对外暴露的接口(图中省略了 template.helper(name, fn) 辅助函数注册接口),可以看到我们这里面设计的最重要的两个引擎函数:compiler 和 parser。
同时查看源代码,我们能看到其最值得称道的两个特性:
cache 机制将生成的 render() 函数都缓存起来,大大提高了效率;debug 能将错误定位到模板的具体某行,方便了开发时的排错。
cache 机制的存储是通过一个对象实现的,cache 对象的键为模板字符串,值为编译后的可执行函数。
debug 机制则是通过在编译时视你的选择,决定是否在函数在加入行号变量以及错误捕获机制。
artTemplate 为我们提供了很好的模板编译思路,期望的目标规则:将 tpl 转为下面那样的 render() 函数。
// tpl{{ if isAdmin }}<h1>{{title}}</h1><ul>{{each list as value i}}<li>索引 {{i + 1}} :{{value}}</li>{{/each}}</ul>{{/if}}// Render 方法:一般模式下var render = function($data, $filename) {'use strict';var $utils = this,$helpers = $utils.$helpers,$escape = $utils.$escape,$each = $utils.$each,isAdmin = $data.isAdmin,title = $data.title,list = $data.list,value = $data.value,i = $data.i,$out = '';if (isAdmin) {$out += '\n\n <h1>';$out += $escape(title);$out += '</h1>\n <ul>\n ';$each(list, function(value, i) {$out += '\n <li>索引 ';$out += $escape(i + 1);$out += ' :';$out += $escape(value);$out += '</li>\n ';});$out += '\n </ul>\n\n ';}return new String($out);}// Render() 方法:debug 模式下var render = function ($data, $filename) {try {'use strict';var $utils = this,$helpers = $utils.$helpers,$line = 0,isAdmin = $data.isAdmin,$escape = $utils.$escape,title = $data.title,$each = $utils.$each,list = $data.list,value = $data.value,i = $data.i,$out = '';$out += '\n ';$line = 2;if (isAdmin) {$out += '\n\n <h1>';$line = 4;$out += $escape(title);$out += '</h1>\n <ul>\n ';$line = 6;$each(list, function(value, i) {$out += '\n <li>索引 ';$line = 7;$out += $escape(i + 1);$out += ' :';$line = 7;$out += $escape(value);$out += '</li>\n ';$line = 8;});$out += '\n </ul>\n\n ';$line = 11;}$out += '\n ';return new String($out);} catch (e) {throw { filename: $filename, name: 'Render Error', message: e.message, line: $line, source: '\n {{if isAdmin}}\n\n <h1>{{title}}</h1>\n <ul>\n {{each list as value i}}\n <li>索引 {{i + 1}} :{{value}}</li>\n {{/each}}\n </ul>\n\n {{/if}}\n '.split(/\n/)[$line - 1].replace(/^\s+/, '') }; }}
其中有两个疑问的地方:
this 被赋值给了 $utilsvalue 和 i首先第 2 点。$data 数据对象里是不一定有 value 和 i 属性的,并且最终构建的 Render 函数也并没有用到 value 和 i(each 回调函数的 value 和 i 与上一级的 value 和 i 是不一样的)。
经过检验发现这是由于 artTemplate 将循环里用到的变量(value, i)也在上一级进行了声明,这减少了 logic(code) 判断的工作量,但是带来了不必要的代码。
然后是第 1点:渲染方法里的 this 被赋值给了 $utils。请看下面的说明:
renderFile(filename, data)里的fn(data)指向的是compile(source, options)返回的render(data)函数,render(data)执行时运行的是new Render(data, filename),new Render()执行过程中的this指向Render的prototype(template.utils,在代码内清楚地指定了的)。
你在纸上画出 render, Redner, new Render() 实例 的原型链就能知道为什么可以直接将这里的 this 赋值给 $utils 了。
为了验证,我们稍微修改一下 template.js 里构建字符串的源代码:
// 比源代码多加了一句 consolevar headerCode = "'use strict';"+ "console.log(this['__proto__'] === template.utils);" // true+ "var $utils=this,$helpers=$utils.$helpers,"+ (debug ? "$line=0," : "");
现在我们的目标已经明确:由 tpl 生成上面所示的 Render 方法。
下面需要着手实现模板解释器与编译器了。
编译器,是由 tpl => Render() 的转换场所,用于拼接字符串,生成函数体代码字符串,然后调用 new Function() 构建起 Render() 函数。
编译器牵涉到了大量字符的处理,同时 compiler 也调用了 parser() 用于处理逻辑型的代码字符串。
{{if isAdmin}}<h1>{{title}}</h1><ul>{{each list as value i}}<li>索引 {{i + 1}} :{{value}}</li>{{/each}}</ul>{{/if}}
我们很容易发现,{{ 到下一个 }} 中间的是逻辑代码字符串,}} 到下一个 {{ 中间的是普通的 HTML 字符串。对于普通的 HTML 字符串,我们只需要简单的拼接就可以了。对于逻辑代码字符串则需要比较复杂的处理,同时变量的识别与赋值也是一件很精细的事情。
主要的几个方法与片段:
forEach 遍历
forEach(source.split(openTag), function (code) {code = code.split(closeTag);var $0 = code[0];var $1 = code[1];// code: [html]if (code.length === 1) {mainCode += html($0);// code: [logic, html]} else {mainCode += logic($0);if ($1) {mainCode += html($1);}}})
在几个主要的遍历与函数处打上断点,查看其一步步的执行过程,便能看到其一步步的执行过程了。
在编译的过程中用到了大量的正则匹配,几个比较大的正则如下:
var REMOVE_RE = /\/\*[\w\W]*?\*\/|\/\/[^\n]*\n|\/\/[^\n]*$|"(?:[^"\\]|\\[\w\W])*"|'(?:[^'\\]|\\[\w\W])*'|\s*\.\s*[$\w\.]+/g;// 匹配 /*xxx*/注释,//xxx 注释,字符串 "xxxx" 'xxx'," . $data."var SPLIT_RE = /[^\w$]+/g;// 匹配 空格+运算符+符号(除_)var KEYWORDS_RE = /\bbreak\b|\bcase\b|\bcatch\b|\bcontinue\b|\bdebugger\b|\bdefault\b|\bdelete\b|\bdo\b|\belse\b|\bfalse\b|\bfinally\b|\bfor\b|\bfunction\b|\bif\b|\bin\b|\binstanceof\b|\bnew\b|\bnull\b|\breturn\b|\bswitch\b|\bthis\b|\bthrow\b|\btrue\b|\btry\b|\btypeof\b|\bvar\b|\bvoid\b|\bwhile\b|\bwith\b|\babstract\b|\bboolean\b|\bbyte\b|\bchar\b|\bclass\b|\bconst\b|\bdouble\b|\benum\b|\bexport\b|\bextends\b|\bfinal\b|\bfloat\b|\bgoto\b|\bimplements\b|\bimport\b|\bint\b|\binterface\b|\blong\b|\bnative\b|\bpackage\b|\bprivate\b|\bprotected\b|\bpublic\b|\bshort\b|\bstatic\b|\bsuper\b|\bsynchronized\b|\bthrows\b|\btransient\b|\bvolatile\b|\barguments\b|\blet\b|\byield\b|\bundefined\b/g;// 匹配 JS 关键字var NUMBER_RE = /^\d[^,]*|,\d[^,]*/g;// 匹配数字var BOUNDARY_RE = /^,+|,+$/g;// 边界匹配var SPLIT2_RE = /^$|,+/;
以及处理是否编码的问题
var escapeSyntax = escape && !/^=[=#]/.test(code);code = code.replace(/^=[=#]?|[\s;]*$/g, '');// 对内容编码if (escapeSyntax) {// 替换 (xxxx)var name = code.replace(/\s*\([^\)]+\)/, '');// 排除 utils.* | include | printif (!utils[name] && !/^(include|print)$/.test(name)) {code = "$escape(" + code + ")";}// 不编码} else {code = "$string(" + code + ")";}
解释器的本质是:将模板里的逻辑型字符串转为正确的 JS 代码字符串。
"if isAdmin" => "if(isAdmin){""title" => "$out+=$escape(title);""each list as value i" => "$each(list,function(value,i){"
artTemplate 里主要两个函数起到了 parser 的作用:
artTemplate 支持自定义辅助函数。
定一个一个辅助函数:
template.helper('dateFormat', function (date, format) {// ..return value;});
然后我们便能在模板里使用辅助函数了:
{{time | dateFormat:'yyyy-MM-dd hh:mm:ss'}}
artTemplate 是一款十分优秀的 JavaScript 模板引擎,阅读其源代码能让你对模板引擎的理解更进一步!