[关闭]
@yangfch3 2016-11-30T12:07:12.000000Z 字数 6480 阅读 5016

artTemplate 模板引擎源码解析

FE


近日,阅读了 artTemplate 模板引擎的源码,引擎的整个结构十分优雅,代码组织得十分清晰。整个库加上注释才 700 余行,十分适合用于阅读与学习。

关于模板引擎的原理与实现思路网上有着许多系列文章,github 上也有着大量的实现可供参考。这篇文章的任务就是带你去阅读 artTemplate 这一款优秀的模板引擎,让你的阅读更加清晰。推荐先阅读一遍源码再来阅读此文。

预备知识:关于模板引擎

每个引擎的实现都需要牵涉到以下五个点:

  1. 语法
    • 弱逻辑语法
    • 强逻辑语法
  2. 解析(用到大量正则、转义)
    • 词法分析
    • 语法分析
  3. 编译 —— 编译模板为可执行的函数
    • 语法解析器(parser)
    • 编译器(compiler)
  4. 缓存
  5. 渲染 —— render 函数接收 Date 运行

拿一个简单的例子来说说:我们需要将下面的 tpl 利用 model 数据正确地编译渲染输出为 下方的html

  1. // tpl
  2. <h3>
  3. <% if (typeof content === 'string') { %>
  4. <=% content %>
  5. <% } %>
  6. <h3>
  7. // HTML
  8. // render({content: 'a h3 title'})
  9. '<h3>a h3 title</h3>'

首先上面的 tpl 已经为我们 制定好了模板的语法规则,逻辑语句 <% %>,赋值语句 <%= %>

这其中我们必然需要通过 词法分析语法解析,使得我们的 tpl 解析拼接)为一段可行的 JavaScript 代码字符串。

  1. $html += '<h3>';
  2. if (typeof content === 'string') {
  3. $html += content;
  4. }
  5. $html += '</h3>';

上面的 代码字符串 还没有实质性的作用,一段 代码字符串 需要真的执行需要借助能使用文本访问 JavaScript 解析引擎的方法:

  1. eval
  2. Function
  3. setTimeout
  4. setInterval

我们不难想到,必然需要使用 Function 构造函数代码字符串 做为函数体构建(编译)为真正可执行的代码(函数),为此我们借助 Function 构造函数 来构建(编译出)我们的 渲染方法,同时在渲染方法中需要完成变量赋值操作

  1. var render = (function() {
  2. var cache =
  3. "var $html = '';\
  4. with($data) {\
  5. $html += '<h3>';\
  6. if (typeof content === 'string') {\
  7. $html += content;\
  8. }\
  9. $html += '</h3>';\
  10. }\
  11. return $html;\
  12. ";
  13. return function (data) {
  14. var renderfn = new Function('$data', cache);
  15. return fn(data);
  16. }
  17. })();

最后,我们得到了 render 方法,直接调用吧:

  1. render({content: 'a h3 title'});
  2. // <h3>a h3 title</h3>

上面介绍了模板引擎的基本作用过程,我们不难发现,模板引擎里最核心实现部分是:

  1. 语法解析器
  2. 编译器

语法解析器(parser)负责将模板语言转为 代码字符串,编译器(compilers)负责使用 代码字符串 完成渲染方法的构建。

artTemplate 核心组件概念图

IMG_1031.JPG-1731kB

图中我们能看到整个模板引擎的设计理念、调用栈和数据流动,可以看到对外暴露的接口(图中省略了 template.helper(name, fn) 辅助函数注册接口),可以看到我们这里面设计的最重要的两个引擎函数:compilerparser

同时查看源代码,我们能看到其最值得称道的两个特性:

  1. cache 机制
  2. debug 机制

cache 机制将生成的 render() 函数都缓存起来,大大提高了效率;debug 能将错误定位到模板的具体某行,方便了开发时的排错。

cache 机制的存储是通过一个对象实现的,cache 对象的键为模板字符串,值为编译后的可执行函数。

debug 机制则是通过在编译时视你的选择,决定是否在函数在加入行号变量以及错误捕获机制。

模板的编译思路

artTemplate 为我们提供了很好的模板编译思路,期望的目标规则:将 tpl 转为下面那样的 render() 函数。

  1. // tpl
  2. {{ if isAdmin }}
  3. <h1>{{title}}</h1>
  4. <ul>
  5. {{each list as value i}}
  6. <li>索引 {{i + 1}} :{{value}}</li>
  7. {{/each}}
  8. </ul>
  9. {{/if}}
  10. // Render 方法:一般模式下
  11. var render = function($data, $filename) {
  12. 'use strict';
  13. var $utils = this,
  14. $helpers = $utils.$helpers,
  15. $escape = $utils.$escape,
  16. $each = $utils.$each,
  17. isAdmin = $data.isAdmin,
  18. title = $data.title,
  19. list = $data.list,
  20. value = $data.value,
  21. i = $data.i,
  22. $out = '';
  23. if (isAdmin) {
  24. $out += '\n\n <h1>';
  25. $out += $escape(title);
  26. $out += '</h1>\n <ul>\n ';
  27. $each(list, function(value, i) {
  28. $out += '\n <li>索引 ';
  29. $out += $escape(i + 1);
  30. $out += ' :';
  31. $out += $escape(value);
  32. $out += '</li>\n ';
  33. });
  34. $out += '\n </ul>\n\n ';
  35. }
  36. return new String($out);
  37. }
  38. // Render() 方法:debug 模式下
  39. var render = function ($data, $filename) {
  40. try {
  41. 'use strict';
  42. var $utils = this,
  43. $helpers = $utils.$helpers,
  44. $line = 0,
  45. isAdmin = $data.isAdmin,
  46. $escape = $utils.$escape,
  47. title = $data.title,
  48. $each = $utils.$each,
  49. list = $data.list,
  50. value = $data.value,
  51. i = $data.i,
  52. $out = '';
  53. $out += '\n ';
  54. $line = 2;
  55. if (isAdmin) {
  56. $out += '\n\n <h1>';
  57. $line = 4;
  58. $out += $escape(title);
  59. $out += '</h1>\n <ul>\n ';
  60. $line = 6;
  61. $each(list, function(value, i) {
  62. $out += '\n <li>索引 ';
  63. $line = 7;
  64. $out += $escape(i + 1);
  65. $out += ' :';
  66. $line = 7;
  67. $out += $escape(value);
  68. $out += '</li>\n ';
  69. $line = 8;
  70. });
  71. $out += '\n </ul>\n\n ';
  72. $line = 11;
  73. }
  74. $out += '\n ';
  75. return new String($out);
  76. } catch (e) {
  77. 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+/, '') }; }
  78. }

其中有两个疑问的地方:

  1. 渲染方法里的 this 被赋值给了 $utils
  2. 方法头部得到的 valuei

首先第 2 点。$data 数据对象里是不一定有 valuei 属性的,并且最终构建的 Render 函数也并没有用到 valueieach 回调函数的 valuei 与上一级的 valuei 是不一样的)。

经过检验发现这是由于 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 指向 Renderprototype(template.utils,在代码内清楚地指定了的)。

你在纸上画出 render, Redner, new Render() 实例 的原型链就能知道为什么可以直接将这里的 this 赋值给 $utils 了。

为了验证,我们稍微修改一下 template.js 里构建字符串的源代码:

  1. // 比源代码多加了一句 console
  2. var headerCode = "'use strict';"
  3. + "console.log(this['__proto__'] === template.utils);" // true
  4. + "var $utils=this,$helpers=$utils.$helpers,"
  5. + (debug ? "$line=0," : "");

现在我们的目标已经明确:由 tpl 生成上面所示的 Render 方法。

下面需要着手实现模板解释器与编译器了。

compiler

编译器,是由 tpl => Render() 的转换场所,用于拼接字符串,生成函数体代码字符串,然后调用 new Function() 构建起 Render() 函数。

编译器牵涉到了大量字符的处理,同时 compiler 也调用了 parser() 用于处理逻辑型的代码字符串。

  1. {{if isAdmin}}
  2. <h1>{{title}}</h1>
  3. <ul>
  4. {{each list as value i}}
  5. <li>索引 {{i + 1}} :{{value}}</li>
  6. {{/each}}
  7. </ul>
  8. {{/if}}

我们很容易发现,{{ 到下一个 }} 中间的是逻辑代码字符串,}} 到下一个 {{ 中间的是普通的 HTML 字符串。对于普通的 HTML 字符串,我们只需要简单的拼接就可以了。对于逻辑代码字符串则需要比较复杂的处理,同时变量的识别与赋值也是一件很精细的事情。

主要的几个方法与片段:

  1. getVariable(code) —— 按照规则识别并返回一段模板字符串中的变量
  2. stringify(code)——字符串转义
  3. html(code)——处理 HTML 字符串
  4. logic(code)——处理逻辑字符串
  5. forEach 遍历

    1. forEach(source.split(openTag), function (code) {
    2. code = code.split(closeTag);
    3. var $0 = code[0];
    4. var $1 = code[1];
    5. // code: [html]
    6. if (code.length === 1) {
    7. mainCode += html($0);
    8. // code: [logic, html]
    9. } else {
    10. mainCode += logic($0);
    11. if ($1) {
    12. mainCode += html($1);
    13. }
    14. }
    15. })

在几个主要的遍历与函数处打上断点,查看其一步步的执行过程,便能看到其一步步的执行过程了。

在编译的过程中用到了大量的正则匹配,几个比较大的正则如下:

  1. var REMOVE_RE = /\/\*[\w\W]*?\*\/|\/\/[^\n]*\n|\/\/[^\n]*$|"(?:[^"\\]|\\[\w\W])*"|'(?:[^'\\]|\\[\w\W])*'|\s*\.\s*[$\w\.]+/g;
  2. // 匹配 /*xxx*/注释,//xxx 注释,字符串 "xxxx" 'xxx'," . $data."
  3. var SPLIT_RE = /[^\w$]+/g;
  4. // 匹配 空格+运算符+符号(除_)
  5. 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;
  6. // 匹配 JS 关键字
  7. var NUMBER_RE = /^\d[^,]*|,\d[^,]*/g;
  8. // 匹配数字
  9. var BOUNDARY_RE = /^,+|,+$/g;
  10. // 边界匹配
  11. var SPLIT2_RE = /^$|,+/;

以及处理是否编码的问题

  1. var escapeSyntax = escape && !/^=[=#]/.test(code);
  2. code = code.replace(/^=[=#]?|[\s;]*$/g, '');
  3. // 对内容编码
  4. if (escapeSyntax) {
  5. // 替换 &nbsp;&nbsp;(xxxx)
  6. var name = code.replace(/\s*\([^\)]+\)/, '');
  7. // 排除 utils.* | include | print
  8. if (!utils[name] && !/^(include|print)$/.test(name)) {
  9. code = "$escape(" + code + ")";
  10. }
  11. // 不编码
  12. } else {
  13. code = "$string(" + code + ")";
  14. }

parser

解释器的本质是:将模板里的逻辑型字符串转为正确的 JS 代码字符串

  1. "if isAdmin" => "if(isAdmin){"
  2. "title" => "$out+=$escape(title);"
  3. "each list as value i" => "$each(list,function(value,i){"

artTemplate 里主要两个函数起到了 parser 的作用:

  1. compiler 里的 logic(code) 方法(主要用于预处理)
  2. parser()

helper 机制

artTemplate 支持自定义辅助函数。

IMG_1032.JPG-1564.5kB

定一个一个辅助函数:

  1. template.helper('dateFormat', function (date, format) {
  2. // ..
  3. return value;
  4. });

然后我们便能在模板里使用辅助函数了:

  1. {{time | dateFormat:'yyyy-MM-dd hh:mm:ss'}}

artTemplate 是一款十分优秀的 JavaScript 模板引擎,阅读其源代码能让你对模板引擎的理解更进一步!

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注