本章将演示一些最广泛使用的库,它们可以更快地创建复杂的 HTML 模板,同时与传统的字符串连接技术相比,使我们的实现更易于阅读和理解。我们将更详细地学习如何使用Underscore.js
和Handlebars.js
模板库,了解它们的约定,评估它们的特性,并找到最适合我们口味的。
在本章结束时,我们将能够通过使用可读模板和利用每个模板库的独特特性,在浏览器中高效地生成复杂的 HTML 结构。
在本章中,我们将:
- 讨论使用专门模板库的好处
- 介绍客户端模板的当前趋势,特别是使用
<% %>
和{{ }}
作为占位符的家族的顶级代表 - 以
Underscore.js
为例介绍使用<% %>
占位符的模板引擎系列 - 以
Handlebars.js
为例介绍使用花括号{{ }}
占位符的模板引擎系列
Underscore.js
是一个 JavaScript 库,它提供了一系列实用方法,帮助 web 开发人员更高效地工作,并专注于应用程序的实际实现,而不是重复的算法问题。默认情况下,Underscore.js
可以通过全局名称空间的“_
”标识符访问,而这正是其名称的来源。
与 jQuery 中的$
标识符一样,下划线“_
”标识符也可以用作 JavaScript 中的变量名。
它提供的一个实用功能是_.template()
方法,它为我们提供了一种方便的方法,将特定值插入遵循特定格式的现有模板字符串中。_.template()
方法识别模板内的三个特殊占位符符号,用于添加动态特性:
<%= %>
符号是用于在模板中插入变量值或表达式的最简单方法。<%- %>
符号对变量或表达式执行 HTML 转义,然后将其插入模板中。- 作为模板生成的一部分,
<% %>
符号用于执行任何有效的 JavaScript 语句。
_.template()
方法接受一个遵循这些特征的模板字符串,并返回一个普通 JavaScript 函数,通常称为模板函数,可以使用包含将在模板中插值的值的对象调用该函数。调用模板函数的结果是一个字符串值,它是在模板内插入提供的值的结果:
var templateFn = _.template('<h1><%= title %></h1>');
var resultHtml = templateFn({
title: 'Underscore.js example'
});
例如,上面的代码返回<h1>Underscore.js example</h1>
,相当于下面的速记调用:
var resultHtml = _.template('<h1><%= title %></h1>')({
title: 'Underscore.js example'
});
有关_.template
方法的更多信息,您可以阅读以下文档:http://underscorejs.org/#template 。
使Underscore.js
模板非常灵活的是<% %>
符号,它允许我们执行任何方法调用,例如,它被用作在模板中创建循环的推荐方法。另一方面,过度使用此功能可能会给您的模板添加太多的逻辑,这是在许多其他框架中发现的已知反模式,违反了关注点分离的原则。
作为使用Underscore.js
进行模板化的一个示例,我们现在将使用它重构在仪表板示例的一些模块中生成的 HTML 代码,如前几章所示。对现有实现所需的修改仅限于categories
和informationBox
模块,它们通过添加新元素来操作页面的 DOM 树。
这种重构可以应用的第一个地方是categories
模块的init()
方法。我们可以修改创建<select>
类别的可用<option>
的代码,如下所示:
var optionTemplate = _.template('<option value="<%= value %>"><%- title %></option>');
var optionsHtmlArray = [];
for (var i = 0; i < dashboard.categories.data.length; i++) {
var categoryInfo = dashboard.categories.data[i];
optionsHtmlArray.push(optionTemplate({
value: i,
title: categoryInfo.title
}));
}
$categoriesSelector.append(optionsHtmlArray.join(''));
如您所见,我们迭代了仪表板的类别,以便创建适当的<option>
元素并将其附加到<select>
类别元素。在我们的模板中,我们对<option>
的value
属性使用<%= %>
符号,因为我们知道它将保存一个不需要转义的整数值。另一方面,我们对每个<option>
的内容部分使用<%- %>
符号,以便在其值不是 HTML 安全字符串的情况下转义每个类别的标题。
我们在for
循环之外使用_.template()
方法,以创建一个编译过的模板函数,该函数将在for
循环的每次迭代中重用。这样,浏览器不仅只执行一次_.template()
方法,而且优化生成的模板函数,使其在for
循环内的后续每次执行中运行得更快。最后,我们使用join('')
方法通过一次操作将optionsHtmlArray
变量和append()
结果的所有 HTML 字符串组合到 DOM 中。
实现相同结果的另一种可能更简单的方法是结合<% %>
符号和Underscore.js
提供的_.each()
方法,使我们能够在模板本身内部实现一个循环。通过这种方式,模板将负责对所提供的类别数组进行迭代,将模块实现的复杂性转移到模板中。
var templateSource = ''.concat(
'<% _.each(categoryInfos, function(categoryInfo, i) { %>',
'<option value="<%= i %>"><%- categoryInfo.title %></option>',
'<% }); %>');
var optionsHtml = _.template(templateSource)({
categoryInfos: dashboard.categories.data
});
$categoriesSelector.append(optionsHtml);
正如您在上面的代码中所看到的,我们的 JavaScript 实现不再包含for
循环,从而降低了其复杂性和所需的嵌套。只有一个对_.template()
方法的调用,它很好地将实现抽象为一个生成 HTML 并呈现所有类别的<option>
元素的操作。您还可以看到这项技术与 jQuery 本身遵循的复合逻辑非常吻合,在这种逻辑中,方法被设计为对元素集合而不是单个项进行操作。
即使在介绍了所有上述改进之后,很快就会发现在应用程序逻辑之间编写模板可能不是最好的方法。一旦您的应用程序变得足够复杂,或者当您需要使用超过几行的模板时,应用程序的逻辑和 HTML 模板的混合会让实现开始感到支离破碎。
解决这个问题的一种更干净的方法是将模板与页面的其余 HTML 代码一起存储。这是朝着更好的关注点分离迈出的一大步,因为它正确地将表示与应用程序逻辑隔离开来。
为了将 HTML 模板作为网页的一部分包含在非活动表单中,我们需要使用一个主机标记来防止它们被呈现,但也允许我们在需要时以编程方式检索其内容。为此,我们可以在页面的<head>
或<body>
中使用<script>
标记,并指定除通常用于 JavaScript 代码的常用text/javascript
之外的任何type
。这背后的操作原理是浏览器不会试图解析、执行或呈现<script>
标记的内容,以防它们的type
属性无法识别。经过一些实验,Underscore.js
用户社区基本上采用了这种做法,并同意将text/template
指定为这些<script>
标签的首选类型,以使这些实现在开发人员中更加统一。
尽管Underscore.js
既不固执己见,也不包含任何特定于模板可用方式的实现,但使用text/template``<script>
标记和/或 AJAX 请求一直是被广泛使用的有价值的技术,被视为最佳实践。
**作为有利于移动到<script>
标记的复杂模板的示例,我们将重构到informationBox
模块的openNew()
方法。正如您在下面的代码中所看到的,生成的<script>
标记格式清晰,我们不再需要使用字符串连接来定义多行模板:
<script id="box-template" type="text/template">
<div class="boxsizer">
<article class="box">
<header class="boxHeader">
<button class="boxCloseButton">✖</button>
<%- itemName %>
</header>
<div class="boxContent">Loading...</div>
</article>
</div>
</script>
将 HTML 模板移出代码时的一个良好实践是编写一个抽象机制来负责检索它们并提供编译后的模板函数。这种方法不仅将实现的其余部分与模板检索机制分离,而且减少了重复性,并创建了一种集中式方法,旨在为应用程序的其余部分提供模板。此外,正如我们在下面看到的,这种方法还允许我们优化模板的检索方式,将好处传播到所有使用它们的地方。
var templateCache = {};
function getEmbeddedTemplate(templateName) {
var compiledTemplate = templateCache[templateName];
if (!compiledTemplate) {
var template = $('#' + templateName).html();
compiledTemplate = _.template(template);
templateCache[templateName] = compiledTemplate;
}
return compiledTemplate;
}
dashboard.informationBox.openNew = function(itemName) {
var boxCompiledTemplate = getEmbeddedTemplate('box-template');
var boxHtml = boxCompiledTemplate({
itemName: itemName
});
var $box = $(boxHtml).appendTo($boxContainer);
/* ... */
};
如上面的实现所示,informationBox
模块的openNew()
方法通过传递与请求模板关联的唯一标识符来调用getEmbeddedTemplate()
函数,并使用返回的模板函数生成新框的 HTML,最后将其附加到页面。实现中最有趣的部分是getEmbeddedTemplate()
方法,它使用templateCache
变量作为字典来保存所有以前编译的模板函数。
第一步始终是检查我们的模板缓存中是否存在请求的模板标识符。如果没有,则在页面的 DOM 树中搜索具有相关 ID 的<script>
标记,并使用其 HTML 内容创建模板函数,然后将其存储在缓存中并返回给调用方。
请记住,最好为 HTML 模板的所有标识符使用特定的前缀或后缀,以避免与其他页面元素的 ID 冲突。为此,在上面的示例中,我们使用-template
作为框模板标识符的后缀。
理想情况下,模板提供程序方法的实现应该在一个单独的模块中,该模块将由应用程序的所有部分使用,但是,由于在我们的仪表板中,这只在一个地方使用,因此我们只需使用一个函数即可满足演示的需要。
handlebar.js或简称 handlebar,是一个专门的客户端模板库,使 web 开发人员能够有效地创建语义模板。使用手柄进行模板化可以创建无逻辑模板,从而确保视图和代码是隔离的,有助于保持关注点分离原则。它在很大程度上与 mustachetemplates 兼容,mustachetemplates 是一种模板语言规范,随着时间的推移已经证明了它们的有效性,并且对所有主要编程语言都有许多实现。此外,handlebar 在 Mustache 模板规范的基础上提供了一组扩展,例如 helper 方法和 partials,作为扩展模板引擎和创建更有效模板的手段。
您可以在处查看所有关于把手的文档 http://handlebarsjs.com/ 。您可以在 JavaScript 中获得关于胡子的更多信息,网址为:https://github.com/janl/mustache.js/ 。
Handlebar 提供的主要模板符号是双花括号语法{{ }}
。由于 Handlebar 从一开始就设计用于 HTML 模板,因此此符号在默认情况下也应用 HTML 转义,降低了非转义值到达模板的可能性,从而导致潜在的安全问题。如果模板的特定部分需要非转义插值,我们可以使用三个大括号符号{{{ }}}
。
此外,由于 handlebar 阻止我们直接从模板中调用方法,因此它使我们能够定义和使用 helper 方法和块表达式,以覆盖更复杂的用例,同时帮助保持模板尽可能干净和可读。内置帮助程序集包括{{#if }}
和{{#each }}
帮助程序,它们允许我们在数组上执行迭代,并根据条件非常轻松地更改模板的结果。
Handlebar 库的中心方法是Handlebars.compile()
方法,该方法接受模板字符串作为参数,并返回一个函数,该函数可用于生成符合所提供模板形式的字符串值。然后可以使用对象作为参数调用此函数(如在Underscore.js
中),其属性将用作评估原始模板中定义的所有把手表达式(花括号符号)的上下文:
var templateFn = Handlebars.compile('<h1>!!!{{ title }}!!!</h1>');
var resultHtml = templateFn({
title: '> Handlebars example <'
});
例如,上面的代码返回"<h1>!!!> Handlebars example <!!!</h1>"
,将插入的标题转换为安全的 HTML 字符串,但如果附加到页面的 DOM 树,则该字符串将正确呈现。当然,如果我们不需要保留对已编译模板函数的引用以备将来使用,那么通过以下速记调用也可以实现相同的结果:
var resultHtml = Handlebars.compile('<h1>!!!{{ title }}!!!</h1>')({
title: '> Handlebars example <'
});
作为使用Handlebars.js
进行模板化的示例,为了展示其与Underscore.js
模板的区别,我们现在将使用它重构仪表板示例,就像我们在上一节中所做的那样。与之前一样,重构仅限于categories
和模块informationBox
模块,它们通过添加新元素来操作页面的 DOM 树。
categories
模块的init()
方法的重构实现应该如下所示:
var optionTemplate = Handlebars.compile('<option value= "{{ value }}">{{ title }}</option>');
var optionsHtmlArray = [];
for (var i = 0; i < dashboard.categories.data.length; i++) {
var categoryInfo = dashboard.categories.data[i];
optionsHtmlArray .push(optionTemplate({
value: i,
title: categoryInfo.title
}));
}
$categoriesSelector.append(optionsHtmlArray.join(''));
首先,我们使用了Handlebars.compile()
方法,该方法根据提供的模板字符串生成并返回模板函数。我们在上一节中看到的Underscore.js
实现的主要区别在于,我们现在使用双大括号符号{{ }}
在模板中插值。除了不同的外观,Handlebars.js
还默认进行 HTML 字符串转义,试图通过将转义作为其主要用例的一部分来消除 HTML 注入安全漏洞。
正如我们在本章前面所做的,我们将在for
循环之外创建模板函数,并使用它为每个<option>
元素生成 HTML。所有生成的 HTML 字符串都收集在一个数组中,最后使用$.append()
方法通过一个操作组合并附加到 DOM 树。
降低实现复杂性的下一个增量步骤是使用模板引擎本身的循环功能将迭代从 JavaScript 代码中抽象出来:
var templateSource = ''.concat(
'{{#each categoryInfos}}',
'<option value="{{@index}}">{{ title }}</option>',
'{{/each}}');
var optionsHtml = Handlebars.compile(templateSource)({
categoryInfos: dashboard.categories.data
});
$categoriesSelector.append(optionsHtml);
Handlebars.js
库允许我们通过使用特殊的{{#each }}
符号来实现这一点。在{{#each }}
和{{/each}},
之间,模板的上下文被改变,以匹配迭代的每个单独对象,允许直接访问和插值categoryInfos
数组中每个对象的{{ title }}
。此外,为了访问循环计数器,Handlebar 为我们提供特殊@index
变量作为循环上下文的一部分。
有关车把提供的所有特殊符号的完整列表,您可以阅读以下文档:http://handlebarsjs.com/reference.html
与大多数模板引擎一样,Handlebar 还引导我们将模板从应用程序的 JavaScript 实现中分离出来,并通过将模板包含在<script>
标记中、页面的 HTML 中,将其交付给浏览器。此外,Handlebars 固执己见,更喜欢使用特殊的text/x-handlebars-template
作为包含 Handlebars 模板的所有<script>
标记的类型属性。例如,以下是应如何根据库建议定义仪表板框的模板:
<script id="box-template" type="text/x-handlebars-template">
<div class="boxsizer">
<article class="box">
<header class="boxHeader">
<button class="boxCloseButton">✖</button>
{{ itemName }}
</header>
<div class="boxContent">Loading...</div>
</article>
</div>
</script>
即使在为<script>
标记指定了不同的type
时,我们的实现仍然可以工作,但遵循库的指导原则显然可以使开发人员之间的实现更加统一。
正如我们在本章前面所做的,我们将遵循最佳实践,创建一个单独的函数,负责在应用程序中需要的任何地方提供模板:
var templateCache = {};
function getEmbeddedTemplate(templateName) {
var compiledTemplate = templateCache[templateName];
if (!compiledTemplate) {
var template = $('#' + templateName).html();
compiledTemplate = Handlebars.compile(template);
templateCache[templateName] = compiledTemplate;
}
return compiledTemplate;
}
dashboard.informationBox.openNew = function(itemName) {
var boxCompiledTemplate = getEmbeddedTemplate('box-template');
var boxHtml = boxCompiledTemplate({
itemName: itemName
});
var $box = $(boxHtml).appendTo($boxContainer);
/* ... */
};
正如您所看到的,实现与我们在本章前面看到的Undescore.js
示例基本相同。唯一的区别是,我们现在使用Handlebars.compile()
方法从检索到的模板生成已编译的模板函数。
Handlebar 库的一个额外功能是支持模板预编译。这允许我们用一个简单的终端命令预生成所有模板函数,然后让服务器将它们交付给浏览器,而不是实际的模板。通过这种方式,浏览器将能够直接使用预编译的模板,无需编译每个单独的模板,并使库和应用程序的执行速度更快。
为了预编译模板,我们首先需要将它们放在单独的文件中。Handlebar 文档建议对我们的文件使用.handlebars
扩展名,但如果愿意,我们仍然可以使用.html
扩展名。在我们的开发机器上安装编译工具后(使用npm install handlebars -g
,我们可以在我们的终端中发出以下命令来编译模板:
handlebars box-template.handlebars -f box-template.js
这将生成box-template.js
文件,该文件实际上是一个将模板添加到Handlebars.templates
的小模块定义。生成的文件可以像普通 JavaScript 文件一样进行组合和缩小,当浏览器加载时,模板功能将通过Handlebars.templates['box-template']
属性可用。
请记住,如果模板使用了.html
扩展名,那么预编译的模板函数将通过Handlebars.templates['box-template.html']
属性可用。
如您所见,使用模板提供程序函数有助于将现有应用程序迁移到预编译的模板,因为它允许我们封装检索模板的方式。移动到预编译模板只需要将getEmbeddedTemplate()
更改为如下内容:
function getEmbeddedTemplate(templateName) {
return Handlebars.templates[templateName];
}
有关车把中模板预编译的更多信息,请阅读以下文档:http://handlebarsjs.com/precompilation.html 。
掌握客户端模板的最后一步是开发实践,它允许我们动态加载模板,并在已经加载的网页中使用它们。与将所有可用模板作为<script>
标记嵌入每个页面的 HTML 源中的方法相比,这种方法可以实现更具可伸缩性的实现。
该技术的关键要素是仅当呈现网页时需要加载每个模板,通常是在用户操作之后。这种方法的主要好处是:
- 由于页面的 HTML 较小,因此初始页面加载时间缩短。如果我们的应用程序有很多只在特定情况下使用的模板,例如,在特定的用户交互之后,那么从页面大小的减少中获得的收益会更大。
- 用户仅下载实际要使用的模板。这样,可以减少每个页面加载的总下载资源的大小。
- 对已加载模板的后续请求不会导致额外下载,因为浏览器的 HTTP 缓存机制将返回缓存的资源。此外,由于浏览器缓存用于所有 HTTP 请求,而不管它们来自哪个页面,因此用户在使用我们的 web 应用程序时只需下载一次所需的模板。
由于其对用户体验和可伸缩性的好处,该技术被最流行的 web 邮件和社交网站广泛使用,其中各种 HTML 模板和 JavaScript 模块根据用户操作动态加载。
有关如何使用 jQuery 在页面上动态加载 JavaScript 模块的更多信息,请阅读中的$.getScript()
方法文档:https://api.jquery.com/jQuery.getScript/ 。
为了说明这种技术,我们将更改informationBox
模块的Underscore.js
和Handlebars.js
实现,以便它使用 AJAX 请求获取仪表板的框模板。
让我们继续分析Underscore.js
实施所需的更改:
var templateCache = {};
function getAjaxTemplate(templateName) {
var compiledTemplate = templateCache[templateName];
if (compiledTemplate) {
return $.Deferred().resolve(compiledTemplate);
}
return $.ajax({
mimeType: 'text/html',
url: templateName + '.html'
}).then(function(template) {
templateCache[templateName] = _.template(template);
return templateCache[templateName];
});
}
正如您在上面的代码中所看到的,我们已经实现了getAjaxTemplate()
函数,作为一种解耦机制,该机制负责从使用它的实现中获取模板。此实现与我们前面使用的getEmbeddedTemplate()
函数有很多共同之处,主要区别在于getAjaxTemplate()
函数是异步的,因此返回承诺。
getAjaxTemplate()
函数首先检查请求的模板是否已经存在于其缓存中,作为减少对服务器的 HTTP 请求的额外尝试。如果在缓存的中找到了模板,那么它将作为已解决承诺的一部分返回,否则我们将使用$.ajax()
方法启动 AJAX 请求以从服务器检索它。像以前一样,我们需要有一个关于模板 HTML 文件的命名和用于在服务器中存储它们的路径的约定。在我们的示例中,我们在与网页本身相同的目录中查找,只是附加了.html
文件扩展名。在某些情况下,根据所使用的 web 服务器,另一个需要关注的问题是将资源的mimeType
定义为text/html
。
当 AJAX 请求完成时,then()
方法以模板的内容作为字符串参数执行,用于生成编译后的模板函数。我们的实现最终将编译后的模板函数作为链式承诺的结果返回,并将其添加到缓存中。由于getAjaxTemplate()
函数是异步的,我们还必须更改openNew()
方法的实现,并使用返回的模板函数在then()
回调中移动所有代码。除此之外,实现保持不变,并以与以前完全相同的方式使用模板函数。
dashboard.informationBox.openNew = function(itemName) {
var templatePromise = getAjaxTemplate('box-template');
templatePromise.then(function(boxCompiledTemplate) {
var boxHtml = boxCompiledTemplate({
itemName: itemName
});
var $box = $(boxHtml).appendTo($boxContainer); box);
/* ... */
});
};
当重新实现getAjaxTemplate()
函数以使用Handlebars.js
时,得到的代码与以前基本相同。唯一的区别在于调用了Handlebars.compile()
方法而不是Undescore.js
等效方法。这是一个额外的好处,因为许多客户端模板引擎相互影响,并且在模板函数的使用方式方面已经融合到一个非常相似的 API 中,这主要是因为用户对现有实现的积极反馈。
function getAjaxTemplate(templateName) {
/* …same as before... */
return $.ajax({ /* …same as before... */ }).then(function(template) {
templateCache[templateName] = Handlebars.compile(template);
return templateCache[templateName];
});
}
请记住,当页面通过文件系统加载时,$.ajax()
方法可能在某些浏览器中不起作用,但在使用 Apache、IIS 或 nginx 等 web 服务器提供服务时,该方法可以正常工作。
尽管这种技术减少了每个网页的总下载量,但它也不可避免地增加了 HTTP 请求的数量。此外,如果页面的初始呈现需要模板,则延迟加载每个模板的做法有时会增加用户必须等待的时间。
在惰性加载和将模板嵌入<script>
标记之间平衡加载模板的方式通常会带来两方面的好处。这种混合方法被业界视为最佳实践,因为它允许我们根据需求对每个实现进行微观管理和微调。根据这种做法,页面主要内容表示所需的模板嵌入到其 HTML 中,而其余模板在需要时利用浏览器缓存延迟交付。
这种模板提供程序函数的实现留给读者作为练习。作为提示,这些方法必须是异步的,因为当在页面的<script>
标记中找不到所请求的模板时,它必须继续并发出 AJAX 请求,以便从服务器检索它。
请记住,通常最好在服务器端生成页面的完整初始 HTML 内容,而不是使用客户端模板。这不仅可以缩短初始页面内容的加载时间,还可以防止当 JavaScript 不可用或出现错误时用户看到空页面。
在本章中,我们学习了如何使用两个最常见的客户端模板库:Underscore.js
和Handlebars.js
。我们还学习了它们如何让我们更快地创建复杂的 HTML 模板,同时使我们的实现更易于阅读和理解。然后,我们继续分析它们的约定,评估它们的特性,并通过示例了解如何在我们的实现中有效地使用它们。
在完成本章之后,我们现在能够通过使用可读模板并利用模板库的独特特性,在浏览器中高效地生成复杂的 HTML 结构。
在下一章中,我们将学习如何创建 jQuery 插件,将应用程序的部分抽象为可重用和可扩展的实现。我们将介绍用于开发 jQuery 插件的最广泛使用的模式,并分析它们各自帮助解决的实现问题。**