blog
blog copied to clipboard
Underscore 模板引擎 API 更新
Underscore 模板引擎 API 更新
语法
Underscore 的模板引擎 _.template() 脱胎于 jQuery 作者的作品 Micro-Templating。但从 Underscore 1.3.3 开始,这个方法做了较大的调整,在保留旧语法的基础上,还新增支持了一个 {variable: 'foo'} 对象作为第三个参数。
一旦传入了这个参数,则模板(第一个参数)中的变量将不再指向待渲染数据(第二个参数)的属性,模板中的 foo 变量将直接指向待渲染数据(第二个参数)自身。
比如,原来的调用方法:
_.template('I love <%= person %>.', {person: 'you'});
在新语法下可以写为:
_.template('I love <%= foo.person %>.', {person: 'you'}, {variable: 'foo'});
比较
看起来似乎变复杂了,但这样做有两个好处:
内部实现
旧语法在实现上,需要使用 with 声明来实现对数据对象属性的查找。with 有哪些问题,这里就不多说了。而新语法由于已经在模板中指定了数据对象自身,则不需要用 with 来搜索其属性。据官方文档称,“模板渲染性能得到极大提升”。
外部接口
新语法带来了一个隐性的改良——接口灵活性更高,即待渲染数据(第二个参数)可以不仅是对象,也可以是数组等其它数据类型。仍然以上面的代码为例,用新语法还可以写成这样:
_.template('I love <%= foo %>.', 'you', {variable: 'foo'});
这样传入的数据更自由,数组等数据不需要被包装为对象再传入了。
使用
在实际应用中,往往存在某个常用的模板需要被多次渲染的情况。此时,为优化性能,我们通常会采用“两步渲染法”——先把模板编译成模板函数备用;按需执行已经编译好的模板函数,把不同的数据渲染为不同的结果——以避免同一模板的重复编译。如下所示:
var fnRender = _.template('I love <%= person %>.');
fnRender({person: 'you'}); //'I love you.'
fnRender({person: 'her'}); //'I love her.'
在这种情况下,我们无法使用 {variable: 'foo'} 参数。那怎么办呢?
幸好有 _.templateSettings 可以进行 _.template() 的全局设置:
_.extend(_.templateSettings, {variable: 'foo'});
在此之后编译的所有模板函数即工作在新语法之下。需要注意的是,这个设置是全局的,也就是说,当前页面的所有模板和相关功能都需要以新语法来写,并将 foo 统一命名。
更新
从 Underscore 1.7 开始,这个 API 的行为又发生了一些变化,我们有必要再来看一看。
从这个版本开始,_.template() 方法将不再接受模板数据了,它的返回值就总是编译生成的模板函数了。也就是说原先一步渲染模板的用法需要修改成两步走:
// before
_.template('I love <%= person %>.', {person: 'you'});
// after
_.template('I love <%= person %>.')({person: 'you'});
看起来很蛋疼?无关痛痒?其实 Underscore 下决心引入这个 “破坏性变更” 还是很有深意的。我认为这个改动的好处在于:
- 消灭了返回值的不确定性,令这个 API 的行为更易于理解。
- 去掉一步到位的用法,虽然牺牲了眼前的便利,但同时也强制使用者了解模板引擎的基本原理,一定程度上会推动使用者考虑模板缓存,进而提升应用的整体性能。
汗,1.4 都用了好久了,还真没注意到有这个更新,这么个设置还是挺有用的 主要是项目里全换成 handlebars 了,underscore 的模板还是有点弱啊
@edokeh 我的都是小项目,本来就依赖 Underscore,模板引擎也直接用它的了。简单够用,更复杂的估计我也不会用,呵呵。 不过等等,Underscore 的模板支持完整的 JS 逻辑,开放、灵活,可以说它简单,但并不“弱”吧?
嘿嘿,我们也是小项目啊,只是碰到些奇怪的需求嘛 主要遇到的问题是,underscore 缺少一个 helper 层,导致某些功能实现起来很别扭 我们的前端模板里面某些文字要做 i18n ,用 underscore 的话就只能污染数据对象来实现了
比如模板是这样
<div>
<h1><%= i18n('title') %>:<%= name %></h1>
</div>
那么就得这么弄
var data = {name:'edokeh'};
_.extend(data, {
i18n: function(key) {
return I18N_DATA[key]
}
})
_.template('...', data);
这时候 data 对象就被污染了,可能会有些潜在问题,而且从逻辑上来说也很不好啊
@edokeh Helper 层完全可以自己写,我感觉反而更灵活(借口,实际上模板规则太多了我记不住啊,哈哈)。Underscore 模板内部可以调用任何全局作用域可以调用的函数和变量,因为模板内的所有逻辑代码就是原生的 JavaScript 代码。
比如你的例子,其实不需要扩展数据源,直接这样写模板就可以:
<div>
<h1><%= I18N_DATA['title'] %>:<%= name %></h1>
</div>
如果一定解除模板对全局变量的依赖,并且不想改写数据源,也可以为模板单独准备数据:
var data = {name: 'edokeh'};
var dataToRender = _.extend({}, data, {
title: I18N_DATA['title']
});
然后使用这个模板:
<div>
<h1><%= title %>:<%= name %></h1>
</div>
当然这样稍显琐碎,还是前一种方法更直观。我感觉模板里存在适度逻辑(或对全局变量的适度调用)还是 OK 的。
我们用 SeaJS 封装的模块,没有全局变量。。。 那个扩展的方式其实我们之前采用过的,可是写起来真的好麻烦。。。索性整体换个模板系统好了 其实 handlebars 用起来也不是很爽,里面完全不能放逻辑,不过幸好我们的系统没那么复杂,基本都能应对 赞同你说的适度逻辑的观点,handlebars 的世界观有些过于激进了
@edokeh 是这样啊,明白了。 p.s. 不过 Sea.js 模块应该也可以手工暴露全局变量的,嘿嘿。
是否有哪个线上应用用到了这个!
underscore is a greate template engine for html render.