@yangfch3
2016-11-30T20:07:12.000000Z
字数 6480
阅读 5201
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
被赋值给了 $utils
value
和 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 里构建字符串的源代码:
// 比源代码多加了一句 console
var 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 | print
if (!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 模板引擎,阅读其源代码能让你对模板引擎的理解更进一步!