有哪些好用的前端模板引擎?
时间:2022-09-01 20:57:01 | 来源:网站运营
时间:2022-09-01 20:57:01 来源:网站运营
模板引擎有很多,比较知名的在各回答里也都提及了,而我要说的是一个更根本的问题,怎么样的模板引擎是适合前端的
首先说明一点,underscore.template这种东西不叫模板引擎,他还不够格来配上“引擎”这两个字,充其量就是一个模板函数,因此不会在这个回答的探讨范围里
由于这个推理的过程相当冗长,基本是用好几年的实际经验和踩过无数坑后堆出来的,别的不说光我们团队自己用的模板引擎都前后有3个版本,现在我正在思考第4个版本应该有的特性(某实现者已吐血),可见我们对此的思考从来没有停止。所以这边只说一些结论性的东西,一个前端的模板引擎需要有怎么样的特性
以下特性没有特定的排序,不分先后,纯粹想到一条写一条出来,也作为工作到现阶段在模板引擎这一话题上的总结吧
前端模板引擎需要有开发时的透明性
我认为前端任何框架和工具都要有对开发的透明性,模板引擎也不例外。所谓透明性即指我在搭建好开发环境后,随手写代码随手刷新浏览器就能看到最新的效果,而不需要额外地执行任何命令或有任何的等待过程
所以一切依赖编译过程的模板引擎并不适合前端使用,编译只能是模板引擎的一个特性,而不能是使用的前提
更严格地说,使用FileWatch等手段进行文件变更检测并自动编译也不在我的考虑范围之内,因为这会造成额外的等待,像我这种手速极快的人可能编译速度跟不上
由此可以推出,前端的模板引擎应该是具备
可在纯前端环境中解析使用的能力的
前端模板引擎要有良好的运行时调试能力
前端并不像后端,任何错误都可以有严格的日志记录和调用堆栈以供分析。由于用户行为的不确定性、执行环境的不确定性、各种第三方脚本的影响等,前端很难做到完全的错误处理和跟踪,这也导致前端必然存在需要直接在线上排查问题的情况
而当问题出现在模板引擎这一层时,就需要模板引擎提供良好的调试能力
一般来说,编译后生成的函数的调试能力是弱于原先手动编写的模板片断的,因为自动生成的函数基本不具备可读性和可断点跟踪性
因此在这一点上,一个供前端使用的模板引擎应该具备在特定情况下从“执行编译后函数获取HTML”换回“解析原模板再执行函数获取HTML”的模式,即应该支持在两种模式间切换
或者更好地,一个强大的前端模板引擎编译生成的函数,可以使用Source Map或其它自定义的手段直接映射回原模板片段,不过现在并没有什么模板引擎实现了这一功能
前端模板引擎要对文件合并友好
在HTTP/2普及之前,文件合并依旧是前端性能优化中的一个重要手段,模板作为文件的一部分,依旧是需要合并的
在提供编译功能的模板引擎中,我们可以使用编译的手段将模板变为JavaScript源码,再在JavaScript的基础上做文件合并
但是如果我们出于上文所说的调试能力等原因希望保留原模板片段,那就需要模板引擎本身支持模板片段合并为一个文件了
大部分仅支持将一段输入的字符串作为模板解析的引擎并不具备这一能力,他们天生并不能将一整个字符串切分为多个模板片段,因而无法支持模板片段层面上的文件合并
需要实现对文件合并的支持,最好的办法就是让模板的语法是基于“片段”的,如下面的模板
[[title]]<h2>{{title}}</h2>[[content]]<section class="article-content"> {{content}}</section>
就由title和content两个片段组成,多个片段可以出现在一个文件中,这就在语法层面上支持了我们的文件合并
前端模板引擎要担负XSS的防范
从安全性上来说,前端对XSS的控制是有严格要求的
我在
单页面(SPA)开发会不会比多页面有更多的安全问题? - 张立理的回答 中有提到过,前端对XSS的防范比较合适的方法是使用“默认转义”的白名单式策略
基于此,一个合理的模板引擎是
必须支持默认转义的,即所有数据的输出都默认经过escape的逻辑处理,将关键符号转为对应的HTML实体符号,以从根源上杜绝XSS的入侵路径
当然并不是所有的内容都必须经过转义的,在系统中免不了有对用户输入富文本的需求,因此需要支持特定的语法来产生无转义的输出,但时刻注意无转义输出才是特例,默认情况下必须是转义输出的
前端模板引擎要支持片段的复用
这并不是前端模板引擎的需求,事实上任何模板引擎都应该支持片段的复用,后端如Velocity、Smarty等无不拥有此功能
所谓片段复用,应该有以下几个层次的应用:
- 一个片段可以被引入到另一处,相当于一个变量到处用的效果
- 一个片段被引入时,可以向其传递不同的数据,相当于一个函数到处用的效果
- 一个片段可以被外部替换,但外部不提供此片段的话保持一个默认的内容,类似设计模式中的策略模式
满足第1和第2点的模板引擎并不少,而满足第3点的前端模板引擎却不多见,而后端的Razor、Smarty等都具备这一功能
话说我当时设计我们自己的模板引擎的第3个版本时,就想出了block这一个概念来实现第3点,在做完交付将近半年之后,有人告诉我说Smarty上就有这概念,顿时有种不知应该高兴还是悲伤的不知所措感。还好他并没有怀疑我直接抄了别人的功能,不然真是冤枉
前端模板引擎要支持数据输出时的处理
所谓数据输出时处理,指一个数据要在输出时做额外的转换,最常见的如字符串的trim操作,比较技术性的如markdown的转换等
诚然数据的转换完全可以在将数据交给模板引擎前就通过JavaScript的逻辑处理完,但这会导致不少有些丑陋又有些冗余的代码,对逻辑本身的复用性也会造成负面的影响
通常模板引擎对数据做额外处理会使用filter的形式实现,类似bash中的管道的逻辑。filter的实现和注册也会有不同的设计,如mustache其实注册的是fitler工厂,而另一些模板引擎则会直接注册filter本身,不同设计有不同的考量点,我们很难说谁好谁坏
但是,模板引擎支持数据的输出处理后,会另我们在编码过程中产生一个新的纠结,即哪些数据处理应该交由模板引擎的filter实现,哪些应该在交给模板引擎前由自己的逻辑逻辑实现。这个话题展开来又是一篇长长的论述,于当前的话题无关就略过吧
前端模板引擎要支持动态数据
在开发过程中,其实有不少数据并不是静态的,如EmberJS就提供了Computed Property这样的概念,Angular也有类似的东西,Backbone则可以通过重写Model的get方法来变相实现
虽然ES5在语言层面上直接提供了getter的支持,但我们在前端开发的大部分场景下依旧不会使用这一语言特性,而会选择将动态的数据封装为某种对象的get等方法
而模板引擎在将数据转为HTML片段的过程中,同样应该关注这一点,对这些动态计算的数据有良好的支持
说得更明白一些,模板引擎不应该仅仅接受纯对象(Plain Object)作为输入,而应该更开放地接受类似带有get方法的动态的数据
一个比较合理的逻辑是,如果一个对象有一个get方法(模板引擎决定这个接口),则数据通过该方法获取,其它情况下视输入的对象为纯对象(Plain Object),使用标准的属性获取逻辑
前端模板引擎要与异步流程严密结合
前端有一个很大的特点,就是到处充斥着异步的流程。由于JavaScript在浏览器提供的引擎中单线程执行的特性、大部分与IO相关的API都暴露为异步的事实,以及多数模块定义规范中模板的动态获取是异步的这一现象,注定我们无法将这个世界当作完全同步来看
一个很常见的例子是,我们有一个AMD模块存放了全局使用的常量,模板引擎需要使用这些常量。当然我们可以在使用模板引擎之前让JavaScript去异步获取这一模块,随后将常量作为数据传递给模板引擎,但这是一种业务与视图相对耦合的玩法,出于强迫症我并不觉得这是一个漂亮的设计,所以我们希望直接在模板中这么写:
<footer>{{$globals.ICP_SERIAL}}</footer>
这是我假想的一个语法,通过$globals可以使用AMD Loader获取globals这一模块,随后获取其中的ICP_SERIAL属性输出
模板引擎支持异步是一个比较具有挑战性的话题,我的计划是在我们自己的模板引擎的下一个版本中尝试实现。这其中涉及很多的技术点,比如:
- 模板的输出本身成了异步的方法,而不再像现在一样直接返回字符串
- 分析模板对异步操作的依赖,整个字符串的拼接逻辑被打断成多个异步
- 异步是需要等待的,且等待是未知的,从性能上考虑,是否需要考虑Stream式的输出,以便完成一段提供一段
- 是提供内置的固定几种异步逻辑,还是基于Promise支持任何自定义的异步逻辑,在复杂度和实用性上作出平衡
至今我还没有完全明确模板与异步结合的方式和接口,这个话题也没办法继续深入探讨了
前端模板引擎要支持不同的开发模式
前端发展至今,有很多不同的开发模式,比如:
- 最普通的HTML页面,使用DOMContentLoaded等事件添加逻辑,特定交互下局部刷新页面
- 采用传统的MVC模型进行单页式开发
- 使用MVVM方式以数据为核心,数据与视图方向绑定进行开发
- 基于Immutable Data进行数据比对Diff转DOM更新的开发(其中可能有Virtual DOM的引入)
一个模板引擎要能支持这么多种不同的的模式是一个非常大的挑战,特别是对双向绑定的支持尤为突出。至今为止几乎所有的支持双向绑定的开发框架都自带了专用的模板引擎,这是因为双向绑定对模板有两大要求:
- 能够从模板中提取“这一模板对哪些数据有依赖”的元信息
- 能够知道一个数据变化引擎的是模板的哪一块,而不至于整个刷新
而通用模板引擎很少提供这两个特性,所以没办法对不同的前端开发模式进行全面到位的支持
从模板引擎本身的实现上来说,一种方法是直接将模板解析后的类似AST的结构暴露出去,供其他框架合理地处理,同时提供对模板局部的刷新功能(也可与前面所说的模板片段一起考虑),但是大部分模板引擎为了性能等考虑,是不会解析出类似AST的语法结构来的
前端模板引擎要有实例间的隔离
在大型的前端项目,特别是单页式的项目中,会有完全未知个数的模板片段同时存在,如果这些片段是带有名称(出于复用的考虑)的,就很容易造成名称上的冲突
对于同一层级的逻辑(如大家都是业务层代码,或者大家都是控件层代码),名称冲突是可以通过一些开发时的约定来解决的。但不同层之间,由于封装性的要求,外部不应该知道一些仅内部使用的片段的名称,此时如果不幸有名称与其它层有冲突,会让情况变得比较麻烦,这类问题甚至都不容易跟踪,往往会导致大量的精力和时间的浪费
因此,一个好的模板引擎应该是多实例的,且不同实例间应该相互具备隔离性,不会出现这种不可预期的冲突
将这个话题再往深地研究,就会发现单纯的隔离是不够的,不同层间除了不冲突的需求,同样还有片段复用的需求,我们还会需要不同模板实例间可以开放一些固定的片段共享,因此模板引擎各个实例的关系是一种组合依赖但又具备基本的封装和隔离的状态
说了这么多,其实现在市面上大致是没有一个模板引擎能满足全部这些条件的,至少“异步流程集成”和“多开发模式支持”这两点就不是一个普通的模板引擎会去考虑的场景,只有我们这种大型的系统才会遇到并在这方向上产生一些思考
因此在选择或设计开发模板引擎时,这些准则可以给你作一个参考,却不需要完全去遵从
推荐自己做的东西就好像把初中二年级写的小说给别人看一样充满羞耻感,所以我就不推销我们自己的产品了……
为啥我不把这些写成博客去骗下季度的部门稿费- -