构建一个使用 Virtual-DOM 的前端模版引擎

331 查看

目录

  • 1. 前言
  • 2. 问题的提出
  • 3. 模板引擎和 Virtual-DOM 结合 —— Virtual-Template
  • 4. Virtual-Template 的实现
    • 4.1 编译原理相关
    • 4.2 模版引擎的EBNF
    • 4.3 词法分析
    • 4.4 语法分析与抽象语法树
    • 4.5 代码生成
  • 5. 完整的 Virtual-Template
  • 6. 结语

1. 前言

本文尝试构建一个 Web 前端模板引擎,并且把这个引擎和 Virtual-DOM 进行结合。把传统模板引擎编译成 HTML 字符串的方式改进为编译成 Virtual-DOM 的 render 函数,可以有效地结合模板引擎的便利性和 Virtual-DOM 的性能。类似 ReactJS 中的 JSX。

阅读本文需要一些关于 ReactJS 实现原理或者 Virtual-DOM 的相关知识,可以先阅读这篇博客:深度剖析:如何实现一个 Virtual DOM 算法 , 进行相关知识的了解。

同时还需要对编译原理相关知识有基本的了解,包括 EBNF,LL(1),递归下降的方法等。

2. 问题的提出

本人在就职的公司维护一个比较朴素的系统,前端渲染有两种方式:

  1. 后台直接根据模板和数据直接把页面吐到前端。
  2. 后台只吐数据,前端用前端模板引擎渲染数据,动态塞到页面。

当数据状态变更的时候,前端用 jQuery 修改页面元素状态,或者把局部界面用模板引擎重新渲染一遍。当页面状态很多的时候,用 jQuery 代码中会就混杂着很多的 DOM 操作,编码复杂且不便于维护;而重新渲染虽然省事,但是会导致一些性能、焦点消失的问题(具体可以看这篇博客介绍)。

因为习惯了 MVVM 数据绑定的编码方式,对于用 jQuery 选择器修改 wordings 等细枝末节的劳力操作个人感觉不甚习惯。于是就构思能否在这种朴素的编码方式上做一些改进,解放双手,提升开发效率。其实只要加入数据状态 -> 视图的 one-way data-binding 开发效率就会有较大的提升。

而这种已经在运作多年的多人维护系统,引入新的 MVVM 框架并不是一个非常好的选择,在兼容性和风险规避上大家都有诸多的考虑。于是就构思了一个方案,在前端模板引擎上做手脚。可以在几乎零学习成本的情况下,做到 one-way data-binding,大量减少 jQuery DOM 操作,提升开发效率。

3. 模板引擎和 Virtual-DOM 结合 —— Virtual-Template

考虑以下模板语法:

这只一个普通的模板引擎语法(类似 artTemplate),支持循环语句(each)、条件语句(if elseif else ..)、和文本填充({…}), 应该比较容易看懂,这里就不解释。当用下面数据渲染该模板的时候:

会得到下面的 HTML 字符串:

把这个字符串塞入文档当中就可以生成 DOM 。但是问题是如果数据变更了,例如data.titleUsers List修改成Users,你只能用 jQuery 修改 DOM 或者直接重新渲染一个新的字符串塞入文档当中。

然而我们可以参考 ReactJS 的 JSX 的做法,不把模板编译成 HTML, 而是把模板编译成一个返回 Virtual-DOM 的 render 函数。render 函数会根据传入的 state 不同返回不一样的 Virtual-DOM ,然后就可以根据 Virtual-DOM 算法进行 diff 和 patch:

这样做好处就是:既保留了原来模板引擎的语法,又结合了 Virtual-DOM 特性:当状态改变的时候不再需要 jQuery 了,而是跑一遍 Virtual-DOM 算法把真正的 DOM 给patch了,达到了 one-way data-binding 的效果,总结流程就是:

  1. 先把模板编译成一个 render 函数,这个函数会根据数据状态返回 Virtual-DOM
  2. 用 render 函数构建 Virtual-DOM;并根据这个 Virtual-DOM 构建真正的 DOM 元素,塞入文档当中
  3. 当数据变更的时候,再用 render 函数渲染一个新的 Virtual-DOM
  4. 新旧的 Virtual-DOM 进行 diff,然后 patch 已经在文档中的 DOM 元素

(恩,其实就是一个类似于 JSX 的东西)

这里重点就是,如何能把模板语法编译成一个能够返回 Virtual-DOM 的 render 函数?例如上面的模板引擎,不再返回 HTML 字符串了,而是返回一个像下面那样的 render 函数: