artTemplate原理及源码解析

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

artTemplate原理及源码解析

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

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

5、渲染 —— render 函数接收 Date 运行

拿一个简单的例子来说说:我们需要将下面的 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 解析引擎的方法:

eval
Function
setTimeout
setInterval

我们不难想到,必然需要使用 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)负责使用 代码字符串 完成渲染方法的构建。

artTemplate 核心组件概念图

artTemplate 核心组件概念图

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

缓存

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

cache 机制
debug 机制

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 方法。

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

compiler

编译器,是由 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 字符串,我们只需要简单的拼接就可以了。对于逻辑代码字符串则需要比较复杂的处理,同时变量的识别与赋值也是一件很精细的事情。

主要的几个方法与片段:

getVariable(code) —— 按照规则识别并返回一段模板字符串中的变量
stringify(code)——字符串转义
html(code)——处理 HTML 字符串
logic(code)——处理逻辑字符串
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) {
    // 替换     &nbsp;&nbsp;(xxxx)
    var name = code.replace(/\s*\([^\)]+\)/, '');
    // 排除 utils.* | include | print
    if (!utils[name] && !/^(include|print)$/.test(name)) {
        code = "$escape(" + code + ")";
    }
// 不编码
} else {
    code = "$string(" + code + ")";
}

parser

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

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

artTemplate 里主要两个函数起到了 parser 的作用:
  1. compiler 里的 logic(code) 方法(主要用于预处理)
  2. parser()

helper 机制

artTemplate 支持自定义辅助函数。

helper 机制

定一个一个辅助函数:

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

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

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

artTemplate是一款十分优秀的 JavaScript模板引擎,源码分享就讲到这里,希望对大家有所帮助。artTemplate下载点击这里。
精品好课
HTML5基础入门视频教程易学必会
HTML5基础入门视频教程,教学思路清晰,简单易学必会。适合人群:创业者,只要会打字,对互联网编程感兴趣都可以学。课程概述:该课程主要讲解HTML(学习HTML5的必备基础语言)、CSS3、Javascript(学习...
HTML5视频播放器video开发教程
适用人群1、有html基础2、有css基础3、有javascript基础课程概述手把手教你如何开发属于自己的HTML5视频播放器,利用mp4转成m3u8格式的视频,并在移动端和PC端进行播放支持m3u8直播格式,兼容...
最新完整React视频教程从入门到精通纯干货纯实战
React是目前最火的前端框架,就业薪资很高,本课程教您如何快速学会React并应用到实战,教你如何解决内存泄漏,常用UI库的使用,自己封装组件,正式上线白屏问题,性能优化等。对正在工作当中或打算学习React高薪就...
React实战视频教程仿京东移动端电商
React是前端最火的框架之一,就业薪资很高,本课程教您如何快速学会React并应用到实战,对正在工作当中或打算学习React高薪就业的你来说,那么这门课程便是你手中的葵花宝典。
jQuery视频教程从入门到精通
jquery视频教程从入门到精通,课程主要包含:jquery选择器、jquery事件、jquery文档操作、动画、Ajax、jquery插件的制作、jquery下拉无限加载插件的制作等等......
最新完整React+VUE视频教程从入门到精,企业级实战项目
React和VUE是目前最火的前端框架,就业薪资很高,本课程教您如何快速学会React和VUE并应用到实战,教你如何解决内存泄漏,常用库的使用,自己封装组件,正式上线白屏问题,性能优化等。对正在工作当中或打算学习Re...
VUE2+VUE3视频教程从入门到精通(全网最全的Vue课程)
VUE是目前最火的前端框架之一,就业薪资很高,本课程教您如何快速学会VUE+ES6并应用到实战,教你如何解决内存泄漏,常用UI库的使用,自己封装组件,正式上线白屏问题,性能优化等。对正在工作当中或打算学习VUE高薪就...
Vue2+Vue3+ES6+TS+Uni-app开发微信小程序从入门到实战视频教程
2021年最新Vue2+Vue3+ES6+TypeScript和uni-app开发微信小程序从入门到实战视频教程,本课程教你如何快速学会VUE和uni-app并应用到实战,教你如何解决内存泄漏,常用UI库的使用,自己...
收藏
扫一扫关注我们