Fork me on GitHub

Any application that can be written in JavaScript, will eventually be written in JavaScript.

Dust 模版引擎

Dust 模版引擎是 LinkedIn 使用的一套模版引擎,但是因为 Dust.js 缺少中文文档,导致在国内的普及率比较低。但是现在这家公司是一家外企,美国那边使用的就是Dust.js,于是我决定在这里对Dust的语法进行一些必要的介绍。

1.模版引擎介绍

在静态页面中,包括已经从后台生成的HTML中,一般很少需要应用到模板,但是随着AJAX技术的发展,不刷新页面而动态更新内容的需求越来越高。为了降低通讯成本,这种通讯技术传输的一般是一个JSON对象,而不是一整串HTML字符串,所以在前端接受JSON数据之后,还要经过处理才能按要求显示在浏览器上。若只是用纯javascript进行拼接处理,将是一个比较繁琐的过程,而且写出来的代码不直观,可读性比较低。比如如果一个JSON对象people是下面这样的:

1
2
3
4
{
"title": "Famous People",
"names": [{ "name": "Larry" }, { "name": "Curly" }, { "name": "Moe" }]
}

我们要把他渲染成一个HTML列表如下:

1
2
3
4
5
6
Famous People
<ul>
<li>Larry</li>
<li>Curly</li>
<li>Moe</li>
</ul>

使用纯粹的javascript将是这样的:

1
2
3
4
5
6
var result = people.title + '\n';
result += '<ul>' + '\n';
for (var i = 0; i < people.names.length; i++) {
result += '<li>' + people.names[i].name + '</li>' + '\n';
}
result += '</ul>'

当然了这只是一个比较简单的例子,而且代码具有专用性且不是最简形式,但是为了兼顾可读性和简介性这样写是比较好的。而使用dust模板将只需要一个模板:

1
2
3
4
5
6
{title}
<ul>
{#names}
<li>{name}</li>{~n}
{/names}
</ul>

然后将people传给编译好的模板则可生成所需要的结果,非常直观。

2.什么是Dust.js

Dustjs、dust.js或者直接叫Dust,是一种模板,一开始是由Aleksander 编写并于2010发布第一个版本于Github。因为后台编译使用 Node.js 所以延续了在插件名后加.js的传统。Aleksander 很喜欢胡子模板 Mustache 的语法。但是Mustache缺少了Aleksander想要的特性,比如模板块和高性能。
从dust.js的导引页面看来,这个模板制作者还是很有诚意的。可惜这个项目已于两年前停止更新,版本停留在0.3.0。好消息是大型职业社交网LinkedIn也了解到了这个模板的优点和潜力,并接手了Dust.js的后续开发,最终出来的就是现在的Dust.js(LinkedIn),为了简便起见后面继续称之为Dust。经过不断地更新,Dust目前已经到了2.0版本了。

3.Dust在线测试器

首先要介绍一下Dust项目中的一个在线测试器,在了解Dust语法的同时,在这个测试器上尝试应用学到的语法,既可以验证语法是否正确,也可以加强对语法的记忆。进入测试器后可以见到四个框,从左上、左下、右上、右下分别编号为1、2、3、4。测试时在1号框中填入一个Dust模板,然后2号框将显示该模板编译后的结果,再在3号框填入一个JSON对象,4号框中将显示最终的渲染结果。

4.标签(Tag)

Dust模板以一种嵌入到HTML中的标签的形式存在。Dust标签使用一对花括号包裹,类似于HTML标签使用一对尖括号包裹:

1
{name}

5.注释

以下标签将不会产生任何内容,即可用作注释(感叹后之间):

1
{! Comment syntax !}

6.键(Key)

一般Dust标签的表示只有两种形式,一种是键,另一种是区段。键是一个最简单的Dust标签,其中包含的花括号中的值称之为键,对应于JSON对象的属性名,对应的属性值一般为简单类型,比如字符串,渲染后将直接以属性值代替整个标签。如果搜索不到任何匹配值,则不会返回任何数据。

1
{name}

在键名后面可以跟随过滤器,使用竖线分隔,一般用于选择处理“<”,“>”等特殊符号的转义:

  • {name|s} 禁用自动转码
  • {name|h} 强制使用HTML转码
  • {name|j} 强制使用Javascript转码
  • {name|u} 使用encodeURI编码
  • {name|uc} 使用encodeURIComponent编码
  • {name|js} 将JSON对象转换为字符串
  • {name|jp} 将JSON 字符串转换为JSON对象

过滤器也可以进行组合:

1
{name|jp|h}

一些特殊字符也可以键的形式直接取值输出:

  • {~n} 换行
  • {~r} CR换行
  • {~lb} 左花括号
  • {~rb} 右花括号
  • {~s} 空格

7.区段(Section)

以下两个标签及其包裹的部分称之为区段,用于循环显示数据。其中“#”为开始标签,“/”为结束标签,其后的键值同样对应于JSON对象的属性名,对应的属性值一般为数组或单个对象,单个对象将被当做一个只有一个元素的数组来对待。模板会按下标对数组中的每个元素调用一次区段包裹着的模板。上一篇中的例子就是利用了区段来循环输出列表元素。区段有两种形式写法第一种如下:

1
{#names}....{/names}

第二种形式(自封闭式)如下:

1
{#names/}

在区段中可以使用两个特殊的键:

  • {$idx} 表示当前迭代的序号(从0开始)
  • {$len} 表示数组长度

8.上下文(Context)

Dust对键或区段值的查询与javascript中对作用域链中变量值的查询类似,换而言之使用区段时会临时改变当前的上下文。 例如一个嵌套的JSON对象:

1
2
3
4
5
6
7
8
9
10
11
{
"name": "root",
"anotherName": "root2",
"A":{
"name":"Albert",
"B":{
"name":"Bob"
}
}
}
}

使用区段索值:

1
{#A}{name}{/A}

则会得到这个对象的A.name的值:

1
Albert

因为使用区段时将上下文转移到A属性对应的对象中。而使用以下区段索值:

1
{#A}{anotherName}{/A}

因为在对象A的属性中不存在“anotherName”属性,于是Dust会向上查询A所处的上下文,发现存在“anotherName”属性,于是得到:

1
root2

若往上查找到JSON对象根部间的所有的上下文均无对应属性时将返回空白,索值不会向下查找。

9.路径(Path)

若使用不带路径的区段索值,那么相当于从JSON对象的根部开始定位区段上下文。而使用路径可以指定开始搜索的位置。路径使用标志“.”来标记标签,跟javascript语法类似。依然是这个JSON对象:

1
2
3
4
5
6
7
8
9
10
{
"name": "root",
"anotherName": "root2",
"A":{
"name":"Albert",
"B":{
"name":"Bob"
}
}
}

若我们需要取A属性下的B属性的name则可以表达成这样:

1
{A.B.name}

或者使用路径标记区块:

1
{#A.B}{name}{/A.B}

或者使用单个“.”表示当前上下文对象(当前为字符串):

1
{#A.B.name}{.}{/A.B.name}

规定路径后,首先在指定的上下文进行查找name的值,找不到时不会向上追溯,而是从根部开始查找。

1
{#A.B}{A.name}{/A.B}

上面这个模板将会在A.B中搜索A,因为B并无A属性,所以从JSON对象根部开始找到A属性,从而找到A.name,返回“Albert”,若从根部也无法找到,则返回空白。

10.修改上下文

我们也可以在一定程度上修改上下文的关系。通过使用冒号“:”可以用冒号后面的键值代替前面的键值的父级上下文:

1
{#A:A2} ... {/A}

以上这个区段会屏蔽掉A的父级上下文,临时将A2作为A的父级上下文,即在A中找不到目标时不会往上回溯,而去搜索A2下的属性。

11.区段参数

在区段中可以设置参数:

1
2
3
{#A.B foo="Hi" bar=" Good to see you"}
{foo} {name} {bar}
{/A.B}

模板会将参数值替代键值标签,结果为:

1
Hi Bob Good to see you

参数也可以是键名,但是赋值时的上下文在区段之外:

1
2
3
{#A.B foo=A.name bar=anotherName}
{foo} {name} {bar}
{/A.B}

12.逻辑区段

?标签

用?来代替区段标签中的#时,仅当name的值为真时,才执行区段主体部分。

1
{?name} body {/name}

^标签

用^来代替#时,仅当name的值为假时,才执行区段主体部分。

1
{^name} body {/name}

{:else}标签

当一个区段标签(包括#、?、^、以及逻辑标签等)的值为假时,若区段主体中包含{:else}标签,则执行{:else}标签以及区段结束标签之间的内容,否则忽略这些内容。

1
2
3
4
5
6
7
<ul>
{#friends}
<li>{name}, {age}{~n}</li>
{:else}
<p>You have no friends!<p>
{/friends}
</ul>

若friend为空,则仅仅输出:

1
2
3
<ul>
<p>You have no friends!</p>
</ul>

值的真假

在区段中判断标签的真假的方法与Javascript本身稍有不同,Dust将以下值判断为假:

  • 空字符串’’、””
  • 布尔false
  • null
  • undefined
  • 空列表[]

其余值均为真值,包括数字“0”,空对象{}。

拆分(Partials)

拆分是一种将重复使用的模板抽取出来,并在使用到这段模板的模板中直接导入该模板,避免重复劳动的方法。在服务端,一个名为“xxx”的Dust模板通常通常保存在一个名为xxx.dust的模板文件中。我们可以利用模板名来在模板中插入一段来自其他模版文件的模板:

1
{>name /}

以上是一个自封闭的区段标签,代表将name.dust中的模版插入到当前位置。若文件包含路径,则用双引号包裹:

1
{>”dust/name” /}

标签中也可以填写参数:

1
{>”dust/name” foo=”Hello” bar=” World”/}

甚至可以使用动态路径:

1
{>”dust/{pathName}” /}

区块(Blocks)

通过拆分可以重用一个模版,但是用这种方法来派生模版有一个缺点,就是你需要记得需要在什么位置插入哪个模版,并且对每一个派生出来的模版都要重新布局一次。区块可以解决这个问题,在父模板中使用区块可以方便地在子模板中替换区块中的内容。区块也是一种特殊的区段,定义方法如下:

1
{+name /}

或者在区段中填写默认内容,当区块没有被替换时,将显示默认内容:

1
{+name}default Content{/name}

使用区块替换需要在子模板中使用拆分区段(>)导入父模板,并使用替换区段(<)进行替换:

1
2
{>father/}
{<name}Content{/name}

比如一个父模板可以写成这样:

1
2
3
4
5
6
7
8
9
<div class="page">
<h1>{+pageHeader}PayPal{/pageHeader}</h>
<div class="bodyContent">
{+bodyContent/}
</div>
<div class="footer">
{+pageFooter}Contact Us {/pageFooter}
</div>
</div>

然后保存为shared/base_template.dust文件,然后定义子模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{! 首先导入父模板 !}
{>"shared/base_template"/}
{! 然后定义对应的部分 !}
{<bodyContent}
<p>These are your current settings:</p>
<ul>
<li>xxxx</li>
<li>yyy</li>
</ul>
{/bodyContent}
{<pageFooter}
<hr>
<a href="/contactUs">About Us</a> |
<a href="/contactUs">Contact Us</a>
{/pageFooter}

13.编译

之前也介绍过,Dust是编译型模板,意思则是若需应用模板,首先要将模板可执行化,即将模板变成可执行的代码。如果你使用过Dust测试器,那么你会发现在你输入模板后,会在2号框中显示一个函数定义,那就是编译生成的代码。使用编译型模板有一个好处,就是当模板编译好之后,若需要重复使用模板,不需要每次都对模板重新进行分析,加快模板解析的速度,而且,模板可以预先编译好保存在服务器,甚至让前端连第一次编译的时间都节省了。
因此Dust库有两种发行版本:

  • dust-core-2.0.2.js
  • dust-full-2.0.2.js
    前者为核心(Core)版本,其只包含模板解析的相关代码,大小只有十几k,而完全版(Full)则包含Dust的所有代码,包括编译器,大小有一百多k。对于不需要在前端进行编译的项目,仅仅需要使用核心版本即可,这也是速度比较快的做法。但是对于需要在前端动态编译的项目,则只能使用包含编译器的完全版。
    编译模板的方法很简单,使用完全版的dust.compile()方法:
1
var compiled = dust.compile("Hello {name}!", "intro");

其中第一个参数为模板字符串,第二个参数为模板名,函数将返回包含编译好的可执行代码的字符串。这个操作不会注册这个模板,仅进行编译,此时仍不可通过模板名来调用这段代码。

14.注册

如果直接执行一遍compiled中的代码,则模板会按之前指定的名字注册到dust,从而可以通过模板名来调用该模板。但若compiled代码未被执行过,则需要在渲染前手动将其注册到dust中,注册的方法很简单:

1
dust.loadSource(compiled);

15.渲染

通过编译注册可以让多套模板处于就绪状态,对于这些模板,我们可以直接用它将JSON对象渲染成HTML文本,通过调用dust.render()方法。

1
2
3
dust.render("intro", {name: "Fred"}, function(err, out) {
console.log(out);
});

这个方法接受3个参数,第一个为模板名,第二个为JSON对象,第三个是一个接受两个参数的回调函数。执行这个方法后Dust会使用注册好的对应模板对JSON对象进行处理,得出一个渲染结果字符串,然后调用回调函数,其中第一个参数包含了在处理过程中出现的错误信息,第二个参数就是渲染结果字符串。一般会在回调函数中将渲染结果插入到当前的DOM结构中,以便在浏览器中显示渲染结果。

16.区块和拆分

一般使用文件来保存模板并且使用区块和拆分是让Dust作为服务端模板时应用的技术,因为在客户端Javascript中无法很方便地对分布式文件进行操作。但是我们可以通过在本地部署模板数据,编译成可执行代码并用一个js文件来保存的方式来使用区块和拆分。
若在Linux平台则直接在终端安装npm和dust并使用dustc命令编译成代码,得到js文件。

1
2
$ npm install dustjs-linkedin
$ dustc input.dust output.js

或者在js引擎中使用dust.compile(),将模板复制到第一个参数,指定第二参数为其不带后缀的文件名,并将结果输出到js文件。

1
2
3
var output1 = dust.compile(partialStr, "partial");
var output2 = dust.compile(baseStr, "base");
var output3 = dust.compile(childStr, "child");

最后在HTML中导入所有生成的js文件即可使用。

1
2
3
<script type="text/javascript" src="partial.js"></script>
<script type="text/javascript" src="base.js"></script>
<script type="text/javascript" src="child.js"></script>

注意此时不再需要使用dust.loadSource()来注册,因为script标签将js文件执行了一次,已经将模板注册好了。此时已可使用dust.render()进行渲染。

ps:这些都是因为工作开发中用到了dust,在国内很少开发人员用到这种引擎,而公司是一家外企,有幸可以用上这个,这只是些基础,会这些也大概会百分之七八十了,还有不能的,可以google下dust,查看官方文档,或者关注我的GitHub,里面有dust的教学