前端模板技术面面观(2)

猪小花1号2018-12-04 11:34

此文已由作者郑海波授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验


Living Template Engine

String-based 和 Dom-based的模板技术都或多或少的依赖与innerHTML, 它们的区别是一个是主要是为了Rendering 一个是为了 Parsing 提取信息

所以为什么不结合它们两者来完全移除对innerHTML的依赖呢?

事实上,值得庆幸的是,已经有几个现实例子在这么做了。

例子

  1. htmlbar: 运行在handlebar 之后的二次编译
  2. ractivejs: 独立
  3. Regularjs 独立, 本文作者结精之一

基本原理

就如图中所示,parse和compile的过程分别类似于String-based 模板技术 和 Dom-based模板技术。

下面来完整讲述下这两个过程

1 . Parsing

首先我们使用一个内建DSL来解析模板字符串并输出AST。

例如,在regularjs中,下面这段简单的模板字符串

<button {{#if !isLogin}} on-click={{this.login()}} {{/if}}>
  {{isLogin? 'Login': 'Wellcome'}}
</button>'

会被解析为以下这段数据结构

[
  {
    "type": "element",
    "tag": "button",
    "attrs": [
      {
        "type": "if",
        "test": {
          "type": "expression",
          "body": "(!_d_['isLogin'])",
          "constant": false,
          "setbody": false
        },
        "consequent": [
          [
            {
              "type": "attribute",
              "name": "on-click",
              "value": {
                "type": "expression",
                "body": "_c_['login']()",
                "constant": false,
                "setbody": false
              }
            }
          ]
        ],
        "alternate": []
      }
    ],
    "children": [
      {
        "type": "expression",
        "body": "_d_['isLogin']?'Login':'Wellcome'",
        "constant": false,
        "setbody": false
      }
    ]
  }
]

这个过程有以下特点

  1. 灵活强大的语法,因为它与基于字符串的模板一般,DSL是自治的,完全不依赖与html,你可以想像下dom-based的模板的那些语法相关的指令,事实上它们甚至无法表达上述那段简单的模板的逻辑。
  2. Living模板技术需要同时处理dsl元素 与 xml元素来实现最终视图层的活动性,即它们是dom-aware的,而在字符串模板中其实xml元素完全可以无需关心,它们被统一视为文本元素

2 Compiler

结合特定的数据模型(在regularjs中,是一个裸数据), 模板引擎层级游历AST并递归生成Dom节点(不会涉及到innerHTML). 与此同时,指令、事件和插值等binder也同时完成了绑定,使得最终产生的Dom是与Model相维系的,即是活动的.

事实上,Living template的compile过程相对与Dom-based的模板技术更加纯粹, 因为它完全的依照AST生成,而不是在原Dom上的改写。

以上面的模板代码的一个插值为例:{{isLogin? 'Login': 'Wellcome'}}。一旦regularjs的引擎遇到这段模板与代表的语法元素节点,会进入如下函数处理

// some sourcecode from regularjs
walkers.expression = function(ast){
  var node = document.createTextNode("");
  this.$watch(ast, function(newval){
    dom.text(node, "" + (newval == null? "": String(newval)));
  })
  return node;
}

正如我们所见, 归功于$watch函数,一旦表达式发生改变,文本节点也会随之改变,这一切其实与angularjs并无两样(事实上regularjs同样也是基于脏检查)

与Dom-based 模板技术利用Dom节点承载信息所不同的是,它的中间产物AST 承载了所有Compile过程中需要的信息(语句, 指令, 属性...等等). 这带来几个好处

  1. 轻量级, 在Dom中进行读写操作是低效的.
  2. 可重用的.
  3. 可序列化 , 你可以在本地或服务器端预处理这个过程。
  4. 安全, 因为安全不需要innerHTML帮我们生成初始Dom

如果你查看Living Template的输出,你会发现是这样的

只有需要的内容被输出了

总结Living templating

我们可以发现Living templating几乎同时拥有String-based和Dom-based模板技术的优点

利用一个如字符串模板的自定义DSL来描述结构来达到了语法上的灵活性,并在Parse后承载信息(AST)。而在Compile阶段,利用AST和Dom API来完成View的组装,在组装过程中,我们同样可以引入Dom-based模板技术的诸如Directive等优良的种子。

living template's 近亲 —— React

React当然也可以称之为一种模板解决方案,它同样也巧妙规避了innerHTML,不过却使用的是截然不同的策略:react使用一种virtual dom 的技术,它也同样基于脏检查,不过与众不同的是,它的脏检查发生在view层面,即发生在virtual dom上,从而可以以较小的开销来实现局部更新。

Example

var MyComponent = React.createClass({
 render: function() {
   if (this.props.first) {
     return <div className="first"><span>A Span</span></div>;
   } else {
     return <div className="second"><p>A Paragraph</p></div>;
   }
 }
});

同样的逻辑使用regularjs描述

{{#if first}}
  <div className="first"><span>A Span</span></div>
{{#else}}
  <div className="second"><p>A Paragraph</p></div>;
{{/if}}

仁者见仁智者见智, 反正我倾向于使用模板来描述结构,而不是杂糅Virtual dom和js语句。你呢?

值得一提的是,由于React的特性,它两次render之间,内部节点的替换是无法预计的(参考这里),所以无法有效的保留信息,所以它也有大量的关于id的placeholder存在。你可以同样查看react-todomvc生成的节点

一个全面的对照表

Contrast /Solutions String-based templating Dom-based templating Living templating
例子 Mustache,Dustjs Angularjs, Vuejs Regularjs 、Ractivejs、htmlbars
语法 ♦♦♦ ♦♦♦ ♦♦♦
活动性 X ♦♦♦ ♦♦♦
性能 初始: ♦♦♦
更新: ♦
初始: ♦ 
更新: ♦♦♦
初始: ♦ 
更新: ♦♦♦
安全性 ♦♦ ♦♦♦♦♦
Dom 无关 ♦♦♦♦♦ X ♦♦
SVG support(*1) X ♦♦ ♦♦♦
  1. 任何一类无法被另一类全面替代
  2. 它们并不是无法同时存在的,比如你可以使用字符串模板来生成Dom-based的模板需要的模板字符串。

参考资料

  1. Template Engines by @Sendhil
  2. string-templating-considered-harmful


免费领取验证码、内容安全、短信发送、直播点播体验包及云服务器等套餐

更多网易技术、产品、运营经验分享请点击