转自:http://msdn.microsoft.com/zh-cn/magazine/hh975379.aspx
尽管模板很强大,但有时模板引擎提供的现成标准功能无法满足您的需求。 您可能要转换数据、定义自定义帮助程序函数或创建您自己的标记。 值得高兴的是,您可以使用 JsRender 的核心功能执行所有这些操作以及更多操作。
我的四月专栏 (msdn.microsoft.com/magazine/hh882454) 介绍了 JsRender 模板库的基本功能。 本专栏将在更多方案中继续探讨 JsRender,例如呈现外部模板、使用 {{for}} 标记更改上下文以及使用复杂表达式。 此外,我还将演示如何使用某些更强大的 JsRender 功能,包括创建自定义标记、转换器和上下文帮助程序以及允许自定义代码。 可以从 archive.msdn.microsoft.com/mag201205ClientInsight 下载所有代码示例,并可以从 bit.ly/ywSoNu 下载 JsRender。
{{for}} 变体
您可以通过多种方法使 {{for}} 标记成为理想的解决方案。 在上月的专栏中,我演示了 {{for}} 标记如何使用块帮助循环访问数组,以及如何一次性循环访问多个对象:
- <!-- looping {{for}} -->
- {{for students}}
- {{/for}}
- <!-- combo iterators {{for}} -->
- {{for teachers students staff}}
- {{/for}}
通过将块内容替换为外部模板(可通过声明为 tmpl 属性的方式指向此模板),可以将 {{for}}(或任何块标记)从块标记(包含内容)转换为自结束标记。 此标记随后将呈现外部模板而非内联内容。
这样,您便可以轻松对模板采用模块化方法,即,可以在不同位置重用模板标记,并组织和编写模板:
- <!-- self closing {{for}} -->
- {{for lineItems tmpl="#lineItemsDetailTmpl" /}}
由于数据很少是平面的,因此钻入和钻出对象层次结构就成为模板的一个重要功能。 在上月的专栏中,我演示了使用点标记和方括号钻入对象层次结构的核心技术,但您也可以使用 {{for}} 标记帮助减少代码。 如果您有一个对象结构,您要钻入对象层次结构中并需要呈现子对象中的一组属性,此优点将变得更加明显。 例如,在呈现人员对象的 address 时,您可能通过以下方式编写模板(在该方式中,路径中的“address”一词多次重复):
- <div>{{:address.street1}}</div>
- <div>{{:address.street2}}</div>
- <div>{{:address.city}}, {{:address.state}} {{:address.postalCode}}</div>
由于无需重复 address 对象,因此 {{for}} 可以显著简化用于呈现地址的代码,如下所示:
- <!-- "with" {{for}} -->
- {{for address}}
- <div>{{:street1}}</div>
- <div>{{:street2}}</div>
- <div>{{:city}}, {{:state}} {{:postalCode}}</div>
- {{/for}}
{{for}} 作用于 address 属性,后者是包含属性的单个对象而非对象数组。 如果 address 为 true(包含某个非 false 值),则将呈现 {{for}} 块的内容。 {{for}} 还会将当前的数据上下文从 person 对象变为 address 对象;就此看来,它的用法类似于许多库和语言所拥有的“with”命令。 因此,在上面的示例中,{{for}} 标记可将数据上下文更改为地址,然后呈现模板内容一次(因为只有一个地址)。 如果此人没有地址(address 属性为空或未定义),则不会呈现任何内容。 这使得 {{for}} 块非常适合于包含只应在特定环境中显示的模板。 以下示例(源自随附的代码下载中的文件 08-for-variations.html)演示了此代码示例如何使用 {{for}} 显示存在的价格信息:
- {{for pricing}}
- <div class="text">${{:salePrice}}</div>
- {{if fullPrice !== salePrice}}
- <div class="text highlightText">PRICED TO SELL!</div>
- {{/if}}
- {{/for}}
外部模板
代码重用是使用模板的一大优势。 如果模板在其应用于的同一页中的 <script> 标记内部定义,则模板将不再像原本那样可以重用。 应可以从多个页面访问的模板可在其自身的文件中创建,并可以根据需要进行检索。 JavaScript 和 jQuery 用于简化从外部文件中检索模板,而 JsRender 用于简化模板的呈现。
我喜欢对外部模板使用的一个约定是在文件名前面加上一个下划线前缀,这是部分视图的通用命名约定。 我还喜欢为所有模板文件加上 .tmpl.html 后缀。 .tmpl 表示它是一个模板,而 .html 扩展名仅仅是为了便于 Visual Studio 等开发工具确定此模板包含 HTML。 图 1 显示了外部模板的呈现代码。
图 1 外部模板的呈现代码
- my.utils = (function () {
- var
- formatTemplatePath = function (name) {
- return "/templates/_" + name + ".tmpl.html";
- },
- renderTemplate = function (tmplName, targetSelector, data) {
- var file = formatTemplatePath(tmplName);
- $.get(file, null, function (template) {
- var tmpl = $.templates(template);
- var htmlString = tmpl.render(data);
- if (targetSelector) {
- $(targetSelector).html(htmlString);
- }
- return htmlString;
- });
- };
- return {
- formatTemplatePath: formatTemplatePath,
- renderExternalTemplate: renderTemplate
- };
- })()
从外部文件中检索模板的方法之一是编写一个可供 Web 应用程序中的 JavaScript 调用的实用工具函数。 在图 1 中请注意,my.utils object 中的 renderExternalTemplate 函数首先使用 $.get 函数检索模板。 调用完成时,将使用 $.templates 函数通过响应内容创建 JsRender 模板。 最后,将使用模板的 render 函数呈现模板,并在目标中显示最终的 HTML。 此代码可通过以下代码调用,其中的模板名称、DOM 目标和数据上下网将传递给自定义的 renderExternalTemplates 函数:
- my.utils.renderExternalTemplate("medMovie", "#movieContainer", my.vm);
此示例的外部模板位于 _medMovie.tmpl.html 示例文件中,并只包含 HTML 和 JsRender 标记。 该模板未使用 <script> 标记封装。 我之所以喜欢对外部模板使用此技术是因为开发环境将能够确定内容为 HTML,这将使代码编写不易出错,因为 IntelliSense 是现成可用的。 然而,此文件可能包含多个模板,每个模板均封装在 <script> 标记中并被指定一个 id 作为其唯一标识符。 这只是处理外部模板的另一种方法。 最后的结果如图 2 所示。
图 2 外部模板的呈现结果
视图路径
JsRender 提供了多个特殊的视图路径来简化对当前视图对象的访问。 #view 用于访问当前视图,#data 用于访问视图的当前数据上下文,#parent 用于向上遍历对象层次结构,而 #index 用于返回索引属性:
- <div>{{:#data.section}}</div>
- <div>{{:#parent.parent.data.number}}</div>
- <div>{{:#parent.parent.parent.parent.data.name}}</div>
- <div>{{:#view.data.section}}</div>
使用视图路径(除 #view 以外)时,这些路径已作用于当前视图。 换言之,以下路径是等效的:
- #data
- #view.data
在导航对象层次结构 - 例如,客户以及订单和订单明细或存储位置中的数据仓库内的电影(如代码下载示例文件 11-view-paths.html 所示)时,视图路径将很有用。
表达式
通用表达式是逻辑的重要部分,并在决定如何呈现模板时很有用。 JsRender 支持通用表达式,其中包括(但不限于)图 3 中显示的表达式。
图 3 JsRender 中的通用表达式
表达式 | 示例 | 注释 |
+ | {{ :a + b }} | 加法 |
- | {{ :a - b }} | 减法 |
* | {{ :a * b }} | 乘法 |
/ | {{ :a / b }} | 除法 |
|| | {{ :a || b }} | 逻辑或 |
&& | {{ :a && b }} | 逻辑与 |
! | {{ :!a }} | 求反 |
? : | {{ :a === 1 ? b * 2: c * 2 }} | 三元表达式 |
( ) | {{ :(a||-1) + (b||-1) }} | 使用圆括号的阶运算 |
% | {{ :a % b }} | 取模运算 |
<= 和 >= 以及 < 和 > | {{ :a <= b }} | 比较运算 |
=== 和 !== | {{ :a === b }} | 等式和不等式 |
JsRender 支持表达式计算,但不支持表达式赋值和随机代码的运行。 这可以防止表达式执行变量赋值或其他操作,例如打开警报窗口。 表达式的目的是计算表达式,然后呈现结果、根据结果采取操作或在其他运算中使用此结果。
例如,使用 JsRender 执行 {{:a++}} 将产生错误,因为该表达式尝试将变量值递增。 此外,执行 {{:alert(‘hello’)}} 也会产生错误,因为该表达式尝试调用不存在的函数 #view.data.alert。
注册自定义标记
JsRender 提供了多个强大的扩展点,例如自定义标记、转换器、帮助程序函数以及模板参数。 用于调用其中的每个扩展点的语法如下所示:
- {{myConverter:name}}
- {{myTag name}}
- {{:~myHelper(name)}}
- {{:~myParameter}}
这些语法分别用于不同的用途;但根据相应的情况,它们可能略有重叠。 在介绍如何在这些语法之间进行选择之前,务必了解一下每个语法的功能及其定义方法。
如果需要呈现的内容拥有“控件风格”的功能并可以独立时,则使用自定义标签比较适合。 例如,可以使用数据将星级评定仅呈现为数字,如下所示:
- {{:rating}}
但使用 JavaScript 逻辑通过 CSS 和一系列空白及已填充的星级图像来呈现星级评定可能会更好:
- {{createStars averageRating max=5/}}
用于创建星级的逻辑部分可以(并且应该)与表示部分分开。 JsRender 提供了一种方法来创建封装此功能的自定义标记。 图 4 中的代码定义了一个名为 createStars 的自定义标记,并在 JsRender 中进行了注册,以便该标记能够在加载此脚本的任何页面中使用。 使用此自定义标记需要在页面中包含其 JavaScript 文件,即示例代码中的 jsrender.tag.js。
图 4 创建自定义标记
- $.views.tags({
- createStars: function (rating) {
- var ratingArray = [], defaultMax = 5;
- var max = this.props.max || defaultMax;
- for (var i = 1; i <= max; i++) {
- ratingArray.push(i <= rating ?
- "rating fullStar" : "rating emptyStar");
- }
- var htmlString = "";
- if (this.tmpl) {
- // Use the content or the template passed in with the template property.
- htmlString = this. renderContent(ratingArray);
- } else {
- // Use the compiled named template.
- htmlString = $.render.compiledRatingTmpl(ratingArray);
- }
- return htmlString;
- }
自定义标记可能有声明属性,例如前面所示的 {{createStars}} 的 max=5 属性。 可通过 this.props 在代码中访问这些属性。 例如,以下代码注册了一个名为 sort 并接受数组(如果名为 reverse 的属性设置为 true,即 {{sort array reverse=true/}},该数组将以相反顺序返回)的自定义标记:
- $.views.tags({
- sort: function(array){
- var ret = "";
- if (this.props.reverse) {
- for (var i = array.length; i; i--) {
- ret += this.tmpl.render(array[i - 1]);
- }
- } else {
- ret += this.tmpl.render(array);
- }
- return ret;
- }}
一个有效的经验法则是,当您需要呈现略微复杂一些的内容(例如 createStars 或 sort 标记)并且此内容可以重用时,请使用自定义标记。 自定义标记不太适合一次性方案。
转换器
自定义标记适合于创建内容,而转换器更适合于将源值转换为其他值这一简单任务。 转换器可以将源值(例如布尔值 true 或 false)更改为完全不同的值(例如分别更改为绿色或红色)。 例如,以下代码将使用 priceAlert 转换器返回一个字符串,该字符串包含一个基于 salePrice 值的价格提示:
- <div class="text highlightText">{{priceAlert:salePrice}}</div>
转换器也非常适合于更改 URL,如下所示:
- <img src="{{ensureUrl:boxArt.smallUrl}}" class="rightAlign"/>
在以下示例中,ensureUrl 转换器应将 boxArt.smallUrl 值转换为限定的 URL(文件 12-converters.html 中同时使用了这两个转换器,并使用 JsRender $.views.converters 函数在 jsrender.helpers.js 中注册了两者):
- $.views.converters({
- ensureUrl: function (value) {
- return (value ? value : "/images/icon-nocover.png");
- },
- priceAlert: function (value) {0
- return (value < 10 ? "1 Day Special!" : "Sale Price");
- }
- });
转换器适用于以非参数化方式将数据转换为呈现值。 如果方案调用参数,则帮助程序函数或自定义标记要比转换器更适合。 正如我们在前面所看到的,自定义标记允许命名参数,因此 createStars 标记可以使用参数来定义星级的大小、颜色、对星级应用的 CSS 类等。 此处要强调的重点是,转换器适用于简单转换,而自定义标记适用于更复杂的完整呈现。
帮助程序函数和模板参数
您可以通过两种方法传入帮助程序函数或参数,以便在模板呈现过程中使用。 一种方法是使用 $.views.helpers 注册它们,该方法类似于注册标记或转换器:
- $.views.helpers({
- todaysPrices: { unitPrice: 23.40 },
- extPrice:function(unitPrice, qty){
- return unitPrice * qty;
- }
- });
这段代码将使它们可用于应用程序中的所有模板。 另一种方法是将它们作为呈现调用中的选项传入:
- $.render.myTemplate( data, {
- todaysPrices: { unitPrice: 23.40 },
- extPrice:function(unitPrice, qty){
- return unitPrice * qty;
- }
- });
这段代码使它们只能在该特定模板呈现调用的上下文中可用。 无论采用哪种方法,都可以通过为参数或函数名(或路径)加上“~”前缀在模板中访问帮助程序:
- {{: ~extPrice(~todaysPrices.unitPrice, qty) }}
帮助程序函数几乎无所不能,包括转换数据、执行计算、运行应用程序逻辑、返回数组或对象,甚至返回模板。
例如,可以创建一个名为 getGuitars 的帮助程序函数以搜索产品数组并查找吉他产品。 该函数还接受表示吉他类型的参数。 随后,可以使用结果呈现单个值或循环访问生成的数组(因为帮助程序函数可以返回任何内容)。 以下代码可获取由所有木吉他产品组成的数组,并使用 {{for}} 块循环访问这些产品:
- {{for ~getGuitars('acoustic')}} ... {{/for}}
帮助程序函数还可以调用其他帮助程序函数,例如使用订单的明细项目数组并应用折扣率和税率来计算总价:
- {{:~totalPrice(~extendedPrice(lineItems, discount), taxRate}}
要定义可供多个模板访问的帮助程序函数,可以将包含帮助程序函数的对象文字传递给 JsRender $.views.helpers 函数。 以下示例定义了 concat 函数以连接多个参数:
- $.views.helpers({
- concat:function concat() {
- return "".concat.apply( "", arguments );
- }
- })
可以使用 {{:~concat(first, age, last)}} 调用 concat 帮助程序函数。 假设可以访问第一个参数、中间参数和最后一个参数的值,并且分别为 John、25 和 Doe,则将呈现值 John25Doe。
适用于独特方案的帮助程序函数
您可能遇到这样的情况:您希望将某个帮助程序函数用于特定模板,但不希望在其他模板中重用此函数。 例如,购物车模板可能需要一个特定于此模板的计算。 帮助程序函数可以执行此计算,但无需使其可以由所有模板访问。 JsRender 利用前面提到的第二种方法支持此方案 — 使用呈现调用中的选项传入此函数:
- $.render.shoppingCartTemplate( data, {
- todaysPrices: { unitPrice: 23.40 },
- extPrice:function(unitPrice, qty){
- return unitPrice * qty;
- }
- });
本示例中将呈现购物车模板,此模板计算所需的帮助程序函数和模板参数将直接通过呈现调用提供。 此处的要点是,此示例中的帮助程序函数仅在呈现该特定模板期间存在。
使用哪个功能?
JsRender 提供了多种可选方法,使您可以通过转换器、自定义标记和帮助程序函数创建强大的模板,但您必须了解每个功能的适用场合。 一个有效的经验法则是使用图 5 中显示的决策树,该决策树概括了如何确定要使用这些功能中的哪一个。
图 5 用于选择正确帮助程序的决策树
- if (youPlanToReuse) {
- if (simpleConversion && !parameters){
- // Register a converter.
- }
- else if (itFeelsLikeAControl && canBeSelfContained){
- // Register a custom tag.
- }
- else{
- // Register a helper function.
- }
- }
- else {
- // Pass in a helper function with options for a template.
- }
如果函数只使用一次,则无需使其在整个应用程序中均可访问,从而避免额外的开销。 这种情况下比较适合使用在需要时传入的“一次性”帮助程序函数。
允许嵌入代码
某些情况下,在模板内部编写自定义代码可能更简单。 JsRender 允许嵌入代码,但建议您仅当其他方法均告失败时才使用此方法,这是因为此类代码混合了表示和行为,因此很难维护。
通过使用带有星号 {{* }} 前缀的块封装代码并将 allowCode 设置为 true,可以将代码嵌入到模板内部。 例如,名为 myTmpl 的模板(如图 6 所示)嵌入了代码,以便计算在一系列语言中呈现命令或单词“and”的正确位置。 整个示例可以在文件 13-allowcode.html 中找到。 尽管逻辑并不是很复杂,但可能很难在模板中读取代码。
除非 allowCode 属性设置为 true(默认值为 false),否则 JsRender 将不允许执行代码。 以下代码定义了名为 movieTmpl 的已编译模板,在脚本标记中为此模板分配了标记(如图 6 所示),并指示它应允许在模板中嵌入代码:
- $.templates("movieTmpl", {
- markup: "#myTmpl",
- allowCode: true
- });
- $("#movieRows").html(
- $.render.movieTmpl(my.vm.movies)
- );
创建模板后,便会呈现此模板。 allowCode 功能可能导致代码难以读取,在某些情况下,帮助程序函数可以解决此问题。 例如,图 6 中的示例使用 JsRender 的 allowCode 功能在需要的位置添加逗号和单词“and”。 然而,此项工作也可以通过创建帮助程序函数完成:
- $.views.helpers({
- languagesSeparator: function () {
- var view = this;
- var text = "";
- if (view.index === view.parent.data.length - 2) {
- text = " and";
- } else if (view.index < view.parent.data.length - 2) {
- text = ",";
- }
- return text;
- }
- })
图 6 允许在模板中嵌入代码
- <script id="myTmpl" type="text/x-jsrender">
- <tr>
- <td>{{:name}}</td>
- <td>
- {{for languages}}
- {{:#data}}{{*
- if ( view.index === view.parent.data.length - 2 ) {
- }} and {{*
- } else if ( view.index < view.parent.data.length - 2 ) {
- }}, {{* } }}
- {{/for}}
- </td>
- </tr>
- </script>
通过为此 languagesSeparator 帮助程序函数的名称加上“~”前缀,可以调用此函数。这使得调用帮助程序的模板代码更易于读取,如下所示:
- {{for languages}}
- {{:#data}}{{:~languagesSeparator()}}
- {{/for}}
通过将逻辑移动至帮助程序函数,删除了模板中的行为并将此行为移动至遵循良好分离模式的 JavaScript 中。
性能和灵活性
除了呈现属性值以外,JsRender 还提供了各种其他功能,包括支持复杂表达式、使用 {{for}} 标记循环访问和更改上下文,以及用于导航上下文的视图路径。 JsRender 还提供了多种方法,用于根据需要添加自定义标记、转换器和帮助程序以扩展其功能。 这些功能以及基于纯字符串的模板编写方法可帮助 JsRender 获益于优良的性能,并使其非常灵活。
John Papa 曾任 Microsoft Silverlight 和 Windows 8 团队推广专家,他主持的“Silverlight 电视秀”节目深受观众欢迎。 他在全球参与了 BUILD、MIX、PDC、TechEd、Visual Studio Live! 和 DevConnections 活动的主题演讲和研讨会。 Papa 同时也是 Microsoft 区域总监、Visual Studio 杂志的专栏作家 (Papa's Perspective) 以及 Pluralsight 培训视频作者。 有关他的情况,请访问 Twitter 上的 twitter.com/john_papa。
衷心感谢以下技术专家对本文的审阅: Boris Moore