目录
- 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. 问题的提出
本人在就职的公司维护一个比较朴素的系统,前端渲染有两种方式:
- 后台直接根据模板和数据直接把页面吐到前端。
- 后台只吐数据,前端用前端模板引擎渲染数据,动态塞到页面。
当数据状态变更的时候,前端用 jQuery 修改页面元素状态,或者把局部界面用模板引擎重新渲染一遍。当页面状态很多的时候,用 jQuery 代码中会就混杂着很多的 DOM 操作,编码复杂且不便于维护;而重新渲染虽然省事,但是会导致一些性能、焦点消失的问题(具体可以看这篇博客介绍)。
因为习惯了 MVVM 数据绑定的编码方式,对于用 jQuery 选择器修改 wordings 等细枝末节的劳力操作个人感觉不甚习惯。于是就构思能否在这种朴素的编码方式上做一些改进,解放双手,提升开发效率。其实只要加入数据状态 -> 视图的 one-way data-binding 开发效率就会有较大的提升。
而这种已经在运作多年的多人维护系统,引入新的 MVVM 框架并不是一个非常好的选择,在兼容性和风险规避上大家都有诸多的考虑。于是就构思了一个方案,在前端模板引擎上做手脚。可以在几乎零学习成本的情况下,做到 one-way data-binding,大量减少 jQuery DOM 操作,提升开发效率。
3. 模板引擎和 Virtual-DOM 结合 —— Virtual-Template
考虑以下模板语法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<div> <h1>{title}</h1> <ul> {each users as user i} <li class="user-item"> <img src="/avatars/{user.id}" /> <span>NO.{i + 1} - {user.name}</span> {if user.isAdmin} I am admin {elseif user.isAuthor} I am author {else} I am nobody {/if} </li> {/each} </ul> </div> |
这只一个普通的模板引擎语法(类似 artTemplate),支持循环语句(each)、条件语句(if elseif else ..)、和文本填充({…}), 应该比较容易看懂,这里就不解释。当用下面数据渲染该模板的时候:
1 2 3 4 5 6 7 8 |
var data = { title: 'Users List', users: [ {id: 'user0', name: 'Jerry', isAdmin: true}, {id: 'user1', name: 'Lucy', isAuthor: true}, {id: 'user2', name: 'Tomy'} ] } |
会得到下面的 HTML 字符串:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<div> <h1>Users List</h1> <ul> <li class="user-item"> <img src="/avatars/user0" /> <span>NO.1 - Jerry</span> I am admin </li> <li class="user-item"> <img src="/avatars/user1" /> <span>NO.2 - Lucy</span> I am author </li> <li class="user-item"> <img src="/avatars/user2" /> <span>NO.3 - Tomy</span> I am nobody </li> </ul> </div> |
把这个字符串塞入文档当中就可以生成 DOM 。但是问题是如果数据变更了,例如data.title
由Users List
修改成Users
,你只能用 jQuery 修改 DOM 或者直接重新渲染一个新的字符串塞入文档当中。
然而我们可以参考 ReactJS 的 JSX 的做法,不把模板编译成 HTML, 而是把模板编译成一个返回 Virtual-DOM 的 render 函数。render 函数会根据传入的 state 不同返回不一样的 Virtual-DOM ,然后就可以根据 Virtual-DOM 算法进行 diff 和 patch:
1 2 3 4 5 6 7 8 9 10 11 12 |
// setup codes // ... var render = template(tplString) // template 把模板编译成 render 函数而不是 HTML 字符串 var root1 = render(state1) // 根据初始状态返回的 virtual-dom var dom = root.render() // 根据 virtual-dom 构建一个真正的 dom 元素 document.body.appendChild(dom) var root2 = render(state2) // 状态变更,重新渲染另外一个 virtual-dom var patches = diff(root1, root2) // virtual-dom 的 diff 算法 patch(dom, patches) // 更新真正的 dom 元素 |
这样做好处就是:既保留了原来模板引擎的语法,又结合了 Virtual-DOM 特性:当状态改变的时候不再需要 jQuery 了,而是跑一遍 Virtual-DOM 算法把真正的 DOM 给patch了,达到了 one-way data-binding 的效果,总结流程就是:
- 先把模板编译成一个 render 函数,这个函数会根据数据状态返回 Virtual-DOM
- 用 render 函数构建 Virtual-DOM;并根据这个 Virtual-DOM 构建真正的 DOM 元素,塞入文档当中
- 当数据变更的时候,再用 render 函数渲染一个新的 Virtual-DOM
- 新旧的 Virtual-DOM 进行 diff,然后 patch 已经在文档中的 DOM 元素
(恩,其实就是一个类似于 JSX 的东西)
这里重点就是,如何能把模板语法编译成一个能够返回 Virtual-DOM 的 render 函数?例如上面的模板引擎,不再返回 HTML 字符串了,而是返回一个像下面那样的 render 函数: