十三、使用客户端模板

基于 JavaScript 的单页面 web 应用(其中页面的某些部分会动态更新以响应服务器端数据更新或用户操作)为用户提供了一种体验,这种体验与以前只保留给本机桌面和移动应用的体验非常相似,从而避免了为更新页面的各个部分或向当前页面添加新的用户界面组件而刷新整个页面的需要。在本章中,我们将研究更新当前页面的选项,同时保持要显示的数据和呈现数据的 DOM 结构之间的明显分离。

动态更新页面内容

我们知道,为了通过 JavaScript 在页面上更新或创建 HTML 内容,我们使用文档对象模型(DOM)来改变属性和元素树,以便影响所需的内容变化。这适用于简单和小型的页面更新,但是扩展性不好,因为每个元素需要大量的 JavaScript 代码,并且可能需要复杂和耗时的查找来定位要更新的确切元素。如果在 JavaScript 代码不知道的情况下更新了 HTML 页面结构,我们也有可能找不到需要更新的元素。

我们可以使用元素的innerHTML属性来动态访问和更新该元素中的 HTML,就像它是一个普通的文本字符串一样,而不是逐个节点地操作 DOM 树。唯一的问题是,当复杂的 HTML 结构表示为字符串时,在其中插入动态字符串和其他数据的代码可能难以理解,这使得维护和开发更加困难,这是专业 JavaScript 开发人员不惜一切代价想要避免的。清单 13-1 显示了这样一个例子,文本在添加到页面之前被插入到一长串 HTML 中。

清单 13-1。将 JavaScript 数据与 HTML 字符串组合在一起

var firstName = "Den",

lastName = "Odell",

company = "AKQA",

city = "London",

email = "denodell@me.com",

divElem = document.createElement("div");

// Applying data and strings to HTML structures results in a complicated mess of difficult to

// maintain code

divElem.innerHTML = "<p>Name: <a href=\"mailto:" + email + "\">" + firstName + " " + lastName +

"</a><br>Company: " + company + "</p><p>City: " + city + "</p>";

// Add the new <div> DOM element to the end of the current HTML page once loaded

window.addEventListener("load", function() {

document.body.appendChild(divElem);

}, false);

尽管这种方法很复杂,但是确实需要将 JavaScript 数据(可能通过 Ajax 加载,也可能不通过 Ajax 加载)与动态显示在页面上的 HTML 文本字符串结合起来。在这一章中,我们将讨论解决这个问题的可行方案,主要集中在客户端 HTML 模板解决方案,它允许页面动态更新,同时保持我们希望呈现的数据与用于适当标记它的 HTML 分开。作为专业的 JavaScript 开发人员,这种关注点的分离对我们来说非常重要,因为它可以随着我们的应用的增长而扩展,并且为我们自己和其他项目团队成员带来最小的困惑。

通过 Ajax 动态加载

动态更新页面内容的最简单的解决方案是在服务器端执行数据与 HTML 代码的组合,通过一个简单的 Ajax 调用将组合的 HTML 代码作为字符串返回,我们可以简单地将它放在页面上,也许替换现有元素的内容。当然,这本身并不能解决问题;我们只是将问题从客户端转移到服务器端,然后需要一个合适的解决方案。这样的服务器端模板解决方案有很多,比如 Smarty(对于 PHP,http://bit . ly/Smarty _ template),Liquid(对于 Ruby,http://bit . ly/Liquid _ template),Apache Velocity(对于 Java,http://bit . ly/Velocity _ template),Spark(对于 ASP.NET,http://bit . ly/Spark _ template)。我们只需要点击服务器提供的特定 web 服务 URL,然后返回一个 HTML 字符串,使用元素的innerHTML属性直接放入页面上的元素中。

这种技术的明显优势在于,它只需要在浏览器中运行很少的 JavaScript 代码。我们只需要一个函数来从服务器加载所需的 HTML,并将其放在页面上指定的元素中。清单 13-2 显示了一个这样的函数的例子,它请求一个 HTML 字符串并把它附加到当前页面。

清单 13-2。通过动态加载 HTML 并用响应填充当前页面

// Define a method to load a string of HTML from a specific URL and place this within a given

// element on the current page

function loadHTMLAndReplace(url, element) {

// Perform an Ajax request to the given URL and populate the given element with the response

var xhr = new XMLHttpRequest(),

LOADED_STATE = 4,

OK_STATUS = 200;

xhr.onreadystatechange = function() {

if (xhr.readyState !== LOADED_STATE) {

return;

}

if (xhr.status === OK_STATUS) {

// Populate the given element with the returned HTML

element.innerHTML = xhr.responseText;

}

};

xhr.open("GET", url);

xhr.send();

}

// Load the HTML from two specific URLs and populate the given elements with the returned markup

loadHTMLAndReplace("/ajax/ticket-form.html", document.getElementById("ticket-form"));

loadHTMLAndReplace("/ajax/business-card.html", document.getElementById("business-card"));

这种技术的缺点是,应用需要频繁的可视化更新来响应变化的数据,最终会从服务器下载大量多余的信息,因为它反映了更新的数据及其周围的标记,而我们真正想要显示的是已经变化的数据。显然,如果数据周围的标记每次都保持不变,就会有冗余数据被下载,这将导致每次下载量更大,因此可能会更慢。根据您的应用,这可能是也可能不是一个大问题,但请始终考虑您的用户,特别是那些传统上速度较慢的移动连接用户,他们可能会为下载的兆字节数据付费,并可能会因为这样的决定而遭受损失。

用一个单独的 HTML 块作为模板效率会高得多,Ajax 请求服务器只提供原始数据(可能是 JSON 格式的),在相关位置填充该模板以产生结果页面结构来更新显示。这就是客户端模板思想发挥作用并找到其主要用例的地方。

客户端模板

模板只是一个文本字符串,其中包含特定的占位符文本标记,在结果输出到当前页面之前,应该用适当的数据替换这些标记。考虑下面的简单模板,它使用双括号标记模式{{}},这在任何其他类型的文本字符串中都不常见,用来表示要替换的文本的位置,以及其值应该用于替换标记的数据变量的名称:

Template:

<p>

Name: <a href="mailto:{{email}}">{{firstName}} {{lastName}}</a><br>

Company: {{company}}

</p>

<p>City: {{city}}</p>

通过将该模板与存储在 JavaScript 数据对象中的值相结合,如下所示:

Data:

{

"firstName": "Den",

"lastName": "Odell",

"email": "denodell@me.com",

"company": "AKQA",

"city": "London"

}

我们将产生一个字符串,然后我们可以输出到我们的页面,包含我们希望显示的文本。通过将模板本地存储在我们的页面中,并且只需要通过 Ajax 更新数据,我们减少了下载多余或重复数据的需要:

Output:

<p>

Name: <a href="mailto:denodell@me.com">Den Odell</a><br>

Company: AKQA

</p>

<p>City: London</p>

不同的模板解决方案使用不同的标记文本模式来表示应该提供数据的点。虽然理论上可以使用任何标记,但重要的是要确保您的标记足够明显,这样它们通常不会出现在您希望显示的模板中的任何其他文本中,否则它们会被意外替换。

在本章的剩余部分,我们将看看 web 应用中客户端模板的一些解决方案,包括其他专业 JavaScript 开发人员使用的一些流行的第三方开放模板库。

没有库的客户端模板

因为客户端模板化是通过字符串替换实现的,所以我们可以围绕用于执行替换的正则表达式,用几行 JavaScript 编写一个非常基本的实现,使用 DOM 元素的innerHTML属性将结果 HTML 或文本字符串附加到我们的页面。清单 13-3 显示了这样一个模板解决方案的例子,它用 JavaScript 对象的属性值替换模板字符串中特殊格式的标记,产生一个 HTML 字符串,然后添加到当前页面。

清单 13-3。通过字符串替换的基本客户端模板

// Define the HTML template to apply data to, using {{ ... }} to denote the data property name

// to be replaced with real data

var template = "<p>Name: <a href=\"mailto:{{email}}\">{{firstName}} {{lastName}}</a><br>Company: {{company}}</p><p>City: {{city}}</p>",

// Define two data objects containing properties to be inserted into the HTML template using

// the property name as key

me = {

firstName: "Den",

lastName: "Odell",

email: "denodell@me.com",

company: "AKQA",

city: "London"

},

bill = {

firstName: "Bill",

lastName: "Gates",

email: "bill@microsoft.com",

company: "Microsoft",

city: "Seattle"

};

// Define a simple function to apply data from a JavaScript object into a HTML template,

// represented as a string

function applyDataToTemplate(templateString, dataObject) {

var key,

value,

regex;

// Loop through each property name in the supplied data object, replacing all instances of

// that name surrounded by {{ and }} with the value from the data object

for (key in dataObject) {

regex = new RegExp("{{" + key + "}}", "g");

value = dataObject[key];

// Perform the replace

templateString = templateString.replace(regex, value);

}

// Return the new, replaced HTML string

return templateString;

}

// Outputs:

// <p>Name: <a href="mailto:denodell@me.com">Den Odell</a><br>Company: AKQA</p>

//     <p>City: London</p>

alert(applyDataToTemplate(template, me));

// Outputs:

// <p>Name: <a href="mailto:bill@microsoft.com">Bill Gates</a><br>Company: Microsoft</p>

//     <p>City: Seattle</p>

alert(applyDataToTemplate(template, bill));

对于简单的模板和 JavaScript 数据,这种解决方案工作得很好;但是,如果需要迭代数组或数据对象,或者添加逻辑来根据某些数据属性的值显示或隐藏不同的部分,这种解决方案是不够的,需要进行相当大的扩展来支持这一点。在这种情况下,最好交给预先编写好的、成熟的第三方开源 JavaScript 客户端模板库来完成这项工作。

使用 Mustache.js 的客户端模板

Mustache 是一种无逻辑的模板语言,由 Chris Wanstrath 在 2009 年开发,以最流行的编程语言实现为特色;Mustache.js 是它的 JavaScript 实现。它最初源于 Google Templates(后来被称为 cTemplates),后者被用作生成 Google 搜索结果页面的模板系统。术语无逻辑是指定义的模板结构不包含ifthenelsefor循环语句;但是,它包含一个称为标记的通用结构,允许根据存储在被引用的 JavaScript 数据中的值类型(称为数据散列)来执行这种行为。每个标签由双括号{{}}表示,当从直角看时,看起来很像八字胡,因此得名。你可以通过http://bit . ly/mustache _ Github从它的 Github 项目页面下载 Mustache.js。该库在缩小后仅重 1.8KB,并启用了 gzip 压缩。

让我们开始研究 Mustache.js,使用它来执行我们的初始示例的模板化,以呈现与清单 13-3 中相同的 HTML 输出。我们将把它分成两个代码清单:一个 HTML 页面,如清单 13-4 所示,包含在特殊配置的<script>标签中写出的模板本身,并引用 Mustache.js 库;一个 JavaScript 文件,其内容如清单 13-5 所示,包含应用于模板的数据,并调用 Mustache.js 来执行由此模板产生的 HTML 的呈现。

清单 13-4。包含用于 Mustache.js 的客户端模板的 HTML 页面

<!doctype html>

<html>

<head>

<meta charset="utf-8">

<title>Mustache.js Example</title>

</head>

<body>

<h1>Mustache.js Example</h1>

<!-- Define the template we wish to apply our data to. The "type" attribute

needs to be any non-standard MIME type in order for the element's contents

to be interpreted as plain text rather than executed -->

<script id="template" type="x-tmpl-mustache">

<p>

Name: <a href="mailto:{{email}}">{{firstName}} {{lastName}}</a><br>

Company: {{company}}

</p>

<p>

City: {{city}}

</p>

</script>

<!-- Load the Mustache.js library -->

<script src="lib/mustache.min.js"></script>

<!-- Execute our script to combine the template with our data -->

<script src="Listing13-5.js"></script>

</body>

</html>

清单 13-4 中包含模板的<script>标签被赋予了一个type属性,浏览器不会将其识别为标准的 MIME 类型。这将导致浏览器将其内容视为纯文本,而不是要执行的内容,但不会将标签的内容明显地写到页面上。然后可以通过 JavaScript 引用标签的内容,方法是通过元素的id属性值定位元素并获取其innerHTML属性的内容。清单 13-4 中的 HTML 页面引用了清单 13-5 中的 JavaScript 代码,如下所示,在浏览器中运行时产生了如图 13-1 所示的结果。请注意用于基于两个输入参数产生输出字符串的Mustache.render()方法的使用:模板字符串和 JavaScript 数据散列对象。

清单 13-5。使用 Mustache.js 将数据与 HTML 模板结合起来

// Locate and store a reference to the <script id="template"> element from our HTML page

var templateElement = document.getElementById("template"),

// Extract the template as a string from within the template element

template = templateElement.innerHTML,

// Create two elements to store our resulting HTML in once our template is

// combined with our data

meElement = document.createElement("div"),

billElement = document.createElement("div"),

// Define two objects containing data to apply to the stored template

meData = {

firstName: "Den",

lastName: "Odell",

email: "denodell@me.com",

company: "AKQA",

city: "London"

},

billData = {

firstName: "Bill",

lastName: "Gates",

email: "bill@microsoft.com",

company: "Microsoft",

city: "Seattle"

};

// Use Mustache.js to apply the data to the template and store the result within the

// newly created elements

meElement.innerHTML = Mustache.render(template, meData);

billElement.innerHTML = Mustache.render(template, billData);

// Add the new elements, populated with HTML, to the current page once loaded

window.addEventListener("load", function() {

document.body.appendChild(meElement);

document.body.appendChild(billElement);

}, false);

A978-1-4302-6269-5_13_Fig1_HTML.jpg

图 13-1。

An example HTML page with data populated into templates with Mustache.js

Mustache 模板格式的完整详细文档可以通过http://bit . ly/Mustache _ docs在线查看。这种格式分为四种不同类型的数据表示:变量、节、注释和分部。

变量

Mustache 模板格式允许用一个同名的关联数据属性替换任何由双括号{{}}包围的文本标记,就像我们在清单 13-4 的 HTML 页面中创建的模板一样。在 Mustache 的说法中,双括号内的名称被称为变量或键。

所有变量都被替换为 HTML 转义字符串,这意味着放入变量中的数据字符串中的任何 HTML 都将作为文本写出,而不是解释为 HTML 元素。如果您希望文本被解释为 HTML,您应该用三重括号标记{{{}}}将您的键名括起来,如下所示。如果需要呈现 JavaScript 对象属性的值,可以使用标准的点符号来导航对象层次结构。

Template:

{{name}}

{{{name}}}

From {{address.country}}

Data Hash:

{

name: "Den <strong>Odell</strong>",

address: {

country: "UK"

}

}

Output:

Den &lt;strong&gt;Odell&lt;/strong&gt;

Den <strong>Odell</strong>

From UK

如果模板标记中表示的键名在提供的数据哈希对象中不存在,则输出中的标记将被一个空字符串替换。这不同于清单 13-3 中我们最初的语义模板示例,如果相关的数据值不存在,那么在结果输出中标签将保持不变。

部分

Mustache 模板中的一个部分有围绕模板块的开始和结束标记。然后,这些标签之间的模板块的内容在结果输出中重复一次或多次,这取决于存储在该标签的关联数据键值中的数据类型。标签中使用的关键字名称前面有一个散列字符(#)来表示该部分的开始标签,前面有一个斜线字符(/)来表示该部分的结束标签,例如{{#section}}{{/section}}。有四种类型的节——条件节、迭代器节、函数节和反转节——它们的声明类似,但根据传递给它们的数据类型执行不同的功能。

条件部分

如果被引用的标记键的数据值是布尔类型true,则显示节块的内容;如果选择false,则不显示该部分。这同样适用于falsy值,例如空字符串或空数组——在这些情况下,该部分不会显示。这种行为为我们提供了执行条件if语句的等效操作的能力,如此处所示。只有当isAvailable数据属性的值为truthy时,字符串YES才会包含在输出中。

Template:

Available:

{{#isAvailable}}

YES

{{/isAvailable}}

Data Hash:

{

isAvailable: true

}

Output:

Available: YES

迭代器部分

如果节标记引用的数据属性的格式是包含一个或多个项的数组列表,则开始和结束标记之间的节的内容将为列表中的每个项重复一次,每个单独项的数据将被传递到每次迭代的节中。这为我们提供了执行迭代数据循环的能力,相当于 JavaScript for循环,如下所示:

Template:

<h1>People</h1>

{{#people}}

<p>Name: {{name}}</p>

{{/people}}

Data Hash:

{

people: [

{name: "Den Odell"},

{name: "Bill Gates"}

]

}

Output:

<h1>People</h1>

<p>Name: Den Odell</p>

<p>Name: Bill Gates</p>

职能部门

如果 section 标签引用的数据格式是一个function,那么事情就变得非常有趣了。Mustache.js 将立即执行该函数,并应返回一个函数,该函数将在每次 section 标记引用该函数的名称时执行,并传入两个参数:作为字符串的模板 section 块的文字文本内容(在任何模板替换发生之前),以及一个直接引用 Mustache.js 的内部render()方法的函数,该方法允许以某种方式操作第一个参数中的值,然后将其内容与应用的数据一起输出。这使得基于输入数据创建过滤器、应用缓存或执行其他基于字符串的模板操作成为可能。此行为的一个示例如下所示:

Template:

{{#strongLastWord}}

My name is {{name}}

{{/strongLastWord}}

Data Hash:

{

name: "Den Odell",

strongLastWord: function() {

return function(text, render) {

// Use the supplied Mustache.js render() function to apply the data to the

// supplied template text

var renderedText = render(text),

// Split the resulting text into an array of words

wordArray = renderedText.split(" "),

wordArrayLength = wordArray.length,

// Extract the final word from the array

finalWord = wordArray[wordArrayLength - 1];

// Replace the last entry in the array of words with the final word wrapped

// in a HTML <strong> tag

wordArray[wordArrayLength - 1] = "<strong>" + finalWord + "</strong>";

// Join together the word array into a single string and return this

return wordArray.join(" ");

}

}

}

Output:

My name is Den <strong>Odell</strong>

倒置截面

当您开始认真使用 Mustache 模板时,您会发现需要根据反转条件显示文本或 HTML 块。如果一个数据值代表一个truthy值或包含要迭代的项目,您希望显示一个节块。如果值是falsy或者不包含要迭代的项目,您希望显示另一个块。反转部分允许这种行为,并通过使用脱字符(^)代替标签键名前的哈希(#)字符来表示,如下所示:

Template:

Available:

{{#isAvailable}}

YES

{{/isAvailable}}

{{^isAvailable}}

NO

{{/isAvailable}}

{{#people}}

<p>Name: {{name}}</p>

{{/people}}

{{^people}}

<p>No names found</p>

{{/people}}

Data Hash:

{

isAvailable: false,

people: []

}

Output:

Available: NO

<p>No names``foundT2】

评论

如果您希望在 Mustache 模板中包含不希望输出到结果字符串的开发注释或评论,只需创建一个标签,以双括号和感叹号(!)字符开始,以双右括号结束,如下所示:

Template:

<h1>People</h1>

{{! This section will contain a list of names}}

{{^people}}

<p>No names found</p>

{{/people}}

Data Hash:

{

people: []

}

Output:

<h1>People</h1>

<p>No names found</p>

部分模板

Mustache 支持跨多个<script>标签分离模板的能力,甚至支持在运行时可以组合在一起产生最终结果的分离文件。这允许创建可重复使用的代码片段并分别存储,以便跨多个模板使用。这种包含用于更大模板的代码片段的文件称为部分模板,简称为部分模板。

分部由标准标记中的给定名称引用,名称前面带有大于号(>)字符,表示它是分部模板。想象一个包含以下两个模板的 HTML 页面,这两个模板包含在分别标有templatepeopleid属性值的<script>标签中:

<script id="template" type="x-tmpl-mustache">

<h1>People</h1>

{{>people}}

</script>

<script id="people" type="x-tmpl-mustache">

{{#people}}

<p>Name: {{name}}</p>

{{/people}}

</script>

注意第一个模板中对名为people的分部的引用。尽管这个名字与赋予第二个模板的id属性相匹配,但是两者之间的引用并不是自动的,这需要在 Mustache.js 中进行配置。要做到这一点,你必须将你想在 JavaScript 对象中使用的任何部分传递给Mustache.render()方法的第三个参数,如清单 13-6 所示。第一个参数是主模板,第二个参数是应用于模板的数据。partials JavaScript 对象(第三个参数)中的属性名与模板中用于引用任何 partials 的标记名相关联。请注意,数据可用于组合模板,就好像在将数据应用到模板之前,两个模板已组合成一个文件一样。

清单 13-6。用 Mustache.js 引用分部模板

// Locate and store a reference to the <script id="template"> element from our HTML page

var templateElement = document.getElementById("template"),

// Locate and store a reference to the <script id="people"> element

peopleTemplateElement = document.getElementById("people"),

// Extract the template as a string from within the template element

template = templateElement.innerHTML,

// Extract the "people" template as a string from within the <script> element

peopleTemplate = peopleTemplateElement.innerHTML,

// Create an element to store our resulting HTML in once our template is

// combined with the partial and our data

outputElement = document.createElement("div"),

// Define an object containing data to apply to the stored template

data = {

people: [{

name: "Den Odell"

}, {

name: "Bill Gates"

}]

};

// Use Mustache.js to apply the data to the template, and allow access to the named partial

// templates and store the result within the newly created element

outputElement.innerHTML = Mustache.render(template, data, {

people: peopleTemplate

});

// Add the new element, populated with HTML, to the current page once loaded

window.addEventListener("load", function() {

document.body.appendChild(outputElement);

}, false);

// The resulting HTML will be:

/*

People</h1>

<p>Name: Den Odell</p>

<p>Name: Bill Gates</p>

*/

以这种方式使用分部文件允许创建具有共享组件、导航、页眉和页脚以及可重用代码片段的大型应用,因为分部文件可以从其他分部文件继承代码。这减少了代码的重复,并允许生成和维护高效的模板。唯一要记住的是,当引用它们的主模板被渲染时,所有的分部模板都必须准备好使用。这意味着,如果您选择通过 Ajax 加载模板文件,而不是将它们存储在当前的 HTML 页面中,那么所有的文件都必须在呈现时加载,这样它们就可以传递给主模板的单个Mustache.render()方法。

Mustache.js 是一个非常有用的小程序库,除了简单的变量替换之外,它还支持客户端模板,允许条件和迭代部分,并支持外部分部模板。

使用 Handlebars.js 的客户端模板

Handlebars 一种客户端模板格式,旨在扩展 Mustache 格式的功能。Handlebars 向后兼容 Mustache 模板,但支持额外的功能,包括块助手,这是对 Mustache 部分原则的扩展,用于阐明和改进每个模板块的显示逻辑的行为。

支持这些模板的 Handlebars.js 库可以直接从其主页下载,网址为 http:// handlebarsjs。com ,如图 13-2 所示,当缩小并使用 gzip 压缩时,该库的重量为 13KB,因此与 Mustache.js 相比,要大得多。然而,正如我们将在本节稍后看到的,有一种预编译模板的技术,这样它们可以用于 Handlebars 库的缩减版本,从而产生更具可比性的文件大小。

A978-1-4302-6269-5_13_Fig2_HTML.jpg

图 13-2。

Handlebars.js home page

Handlebars 的用法与 Mustache 非常相似,从 HTML 页面中引用这个库,模板与 JavaScript 数据散列对象相结合,产生所需的输出字符串,插入到当前页面中。模板可以存储在页面本身的<script>标签中,就像我们在 Mustache 中看到的那样,或者使用 Ajax 通过单独的文件加载。同样,数据对象可以直接存储在 JavaScript 文件中,或者通过 Ajax 动态加载。

该库的全局Handlebars对象包含帮助呈现模板的方法;Handlebars.compile()方法将模板字符串作为参数,并返回一个函数。然后可以执行该函数,向它传递用于呈现模板的数据哈希对象,它将返回组合两者的结果字符串,以便在您的页面中使用。清单 13-7 显示了一个简单的例子,它使用了一个基本的页面内模板和本地 JavaScript 数据的compile()方法。

清单 13-7。包含简单把手模板的 HTML 页面

<!doctype html>

<html>

<head>

<meta charset="utf-8">

<title>Handlebars.js Example</title>

</head>

<body>

<h1>Handlebars.js Example</h1>

<div id="result"></div>

<!-- Define the template we wish to apply our data to. The "type" attribute

needs to be any non-standard MIME type in order for the element's contents

to be interpreted as plain text rather than executed -->

<script id="template" type="x-tmpl-handlebars">

<p>

Name: <a href="mailto:{{email}}">{{firstName}} {{lastName}}</a><br>

Company: {{company}}

</p>

<p>

City: {{city}}

</p>

</script>

<!-- Load the Handlebars.js library -->

<script src="lib/handlebars.min.js"></script>

<!-- Execute our script to combine the template with our data. Included

here for brevity, move to an external JavaScript file for production -->

<script>

// Get a reference to the template element and result element on the page

var templateElem = document.getElementById("template"),

resultElem = document.getElementById("result"),

// Get the string representation of the Handlebars template from the template

// element

template = templateElem.innerHTML,

// Define the data object to apply to the template

data = {

firstName: "Den",

lastName: "Odell",

email: "denodell@me.com",

company: "AKQA",

city: "London"

},

// The compile() method returns a function which the data object can be passed to

// in order to produce the desired output string

compiledTemplate = Handlebars.compile(template);

// Combine the data with the compiled template function and add the result to the page

resultElem.innerHTML = compiledTemplate(data);

</script>

</body>

</html>

现在让我们一起看看 Handlebars 模板和 Handlebars.js 库的一些特性,包括片段、帮助器和用于提高性能的模板预编译。

部分的

Handlebars 模板中的部分模板看起来与 Mustache 模板中的部分模板相同,但是两者在将部分模板的细节提供给库进行呈现的方式上有所不同。Handlebars.registerPartial()方法允许使用两个参数将分部注册到手柄:分部的名称和分部模板的字符串表示。必须在从正在呈现的模板中引用分部之前调用此函数。通过多次调用registerPartial()方法可以注册多个分部。就像 Mustache.js 一样,在呈现主模板之前,必须加载并注册所有分部。

助手

Handlebars 用自己的功能丰富的处理程序(称为助手)扩展了 Mustache 部分的概念。这些使您能够迭代数据列表,或执行条件表达式,使用具有清晰名称的帮助器,使模板更容易阅读和理解——这很重要,特别是当您的模板变得更加复杂时,这与 Mustache 有显著的不同。使用 Mustache,在不知道传递给模板的数据类型的情况下,您无法确定一个部分是用作条件块、迭代还是其他什么;在这里,帮助者的名字清楚地表明了区别。有几个内置的助手,Handlebars 让您能够轻松地创建自己的助手,这些助手可以简单地在您的所有模板中重用。助手名称的前面是标签中的散列(#)字符,后面是应用于助手的数据键的名称。在这一节中,我们将看看一些常见的助手,我将解释如何轻松地创建自己的助手来满足特定模板的需求。

with帮手

with助手允许您将不同的数据上下文应用到它所围绕的模板块。这为重复使用点符号导航传递给助手的特定数据属性层次结构提供了一种替代方法。为了从一个节中导航回父上下文,可以在变量名前使用../标记。这里展示了一个简单的with助手的例子,它应该可以让一切变得清晰。

Template:

<h1>{{name}}</h1>

{{#with address}}

<p>{{../name}} lives at: {{street}}, {{city}}, {{region}}, {{country}}</p>

{{/with}}

Data Hash:

{

name: "Den Odell",

address: {

street: "1 Main Street",

city: "Hometown",

region: "Homeshire",

country: "United Kingdom"

}

}

Output:

<h1>Den Odell</h1>

<p>Den Odell lives at: 1 Main Street, Hometown, Homeshire, United Kingdom</p>

each帮手

each助手允许你迭代一个数据列表,比如一个数组或对象。数组项的值,如果不是对象或数组本身,可以通过使用保留的变量名{{this}}来访问。在数组中循环时,原数组中当前项的索引由保留的变量名{{@index}}提供。当迭代一个对象时,当前属性的键的名称由保留的变量名{{@key}}提供。如果提供的数据列表为空,可以添加一个可选的{{else}}部分,允许您提供一个要呈现的部分块,如下例所示:

Template:

<h1>People</h1>

{{#each people}}

<p>Item {{@index}}: {{this}}</p>

{{else}}

<p>No names found</p>

{{/each}}

Data Hash:

{

people: [

"Den Odell",

"Bill Gates"

]

}

Output:

<h1>People</h1>

<p>Item 0: Den Odell</p>

<p>Item 1: Bill Gates</p>

ifunless助手

使用ifunless手柄辅助工具,可以根据某些数据属性的值有条件地显示模板截面块。如果传递给它的值是一个truthy值(除了falseundefinednull,空字符串或空数组之外的任何值),那么if助手将显示相关的节块,而unless助手仅在数据值为falsy时才显示相关的节块。在任一情况下,可以添加可选的{{else}}部分来捕捉反转的情况,如下例所示:

Template:

<h1>People</h1>

{{#if people}}

<p>Item {{@index}}: {{this}}</p>

{{else}}

<p>No names found</p>

{{/if}}

{{#unless isAvailable}}

<p>Not available</p>

{{/unless}}

Data Hash:

{

isAvailable: false,

people: []

}

Output:

<h1>People</h1>

<p>No names found</p>

<p>Not available</p>

log帮手

如果您正在为模板提供一个大型的多级数据对象,当在整个模板中使用多个助手时,要知道您在数据层次结构中的位置,或者在任何特定的点上您有哪些可用的数据,有时会变得令人困惑。幸运的是,Handlebars 提供了一个log调试助手,允许您在模板中的任意点查看可用数据的状态。只需传递您希望查看的数据对象变量的名称,或者使用{{log this}}来显示当前上下文中可用的数据。如果最终页面生成是通过命令行工具处理的,则数据不是被写出到生成的页面本身,而是被写出到命令行;如果生成是在浏览器中实时发生的,则数据被写出到浏览器的开发人员控制台窗口中。

自定义助手

除了内置的帮助器,Handlebars 还提供了创建您自己的自定义帮助器的能力,允许您在模板中提供您需要的确切功能。这些可以在块级工作,以对模板的一部分执行操作,或者在单个数据项级工作,例如格式化特定的数据以供显示。

可以使用Handlebars.registerHelper()方法创建一个定制的帮助器,向其传递两个参数:帮助器的唯一名称,以及在呈现时在模板中遇到帮助器时要执行的函数,该函数对模板中传递给它的数据执行所需的行为。清单 13-8 展示了一些简单的自定义助手,你可能会发现它们对你自己的模板很有用。

清单 13-8。自定义车把助手示例

// The registerHelper() method accepts two arguments - the name of the helper, as it will

// be used within the template, and a function which will be executed whenever the

// helper is encountered within the template. The function is always passed at least one

// parameter, an object containing, amongst others, a fn() method which performs the same

// operation as Handlebars' own render ability. This method takes a data object and

// returns a string combining the template within the block helper with the data in

// this object

// Define a block helper which does nothing other than pass through the data in the

// current context and combine it with the template section within the block

Handlebars.registerHelper("doNothing", function(options) {

// To use the current data context with the template within the block, simply use

// the 'this' keyword

return options.fn(this);

});

// The helper can be passed parameters, if required, listed one by one after the helper

// name within double braces. These are then made available within the function as

// separate input parameters. The final parameter is always the options object, as before

Handlebars.registerHelper("ifTruthy", function(conditional, options) {

return conditional ? options.fn(this) : options.inverse(this);

});

// If more than one or two parameters need to be passed into the helper, named parameters

// can be used. These are listed as name/value pairs in the template when the helper is

// called, and are made available within the options.hash property as a standard

// JavaScript object ready to pass to the options.fn() method and used to render the

// data within

Handlebars.registerHelper("data", function(options) {

// The options.hash property contains a JavaScript object representing the name/value

// pairs supplied to the helper within the template. Rather than pass through the

// data context value 'this', here we pass through the supplied data object to the

// template section within the helper instead

return options.fn(options.hash);

});

// Create a simple inline helper for converting simple URLs into HTML links. Inline helpers

// can be used without being preceded by a hash (#) character in the template.

Handlebars.registerHelper("link", function(url) {

// The SafeString() method keeps HTML content intact when rendered in a template

return new Handlebars.SafeString("<a href=\"" + url + "\">" + url + "</a>");

});

清单 13-8 中给出的自定义助手可以像下面的示例模板中演示的那样使用:

Base Template:

{{#doNothing}}

<h1>Dictionary</h1>

{{/doNothing}}

{{#ifTruthy isApiAvailable}}

<p>An API is available</p>

{{/ifTruthy}}

{{#ifTruthy words}}

<p>We have preloaded words</p>

{{else}}

<p>We have no preloaded words</p>

{{/ifTruthy}}

<dl>

{{#data term="vastitude" definition="vastness; immensity" url="http://dictionary.com/browse/vastitudeT2】

{{>definition}}

{{/data}}

{{#data term="achromic" definition="colorless; without coloring matter" url="http://dictionary.com/browse/achromicT2】

{{>definition}}

{{/data}}

</dl>

Partial "definition" Template

<dt>{{term}} {{link url}}</dt>

<dd>{{definition}}</dd>

Data hash:

{

isApiAvailable: true,

words: []

}

Output:

<h1>Dictionary</h1>

<p>An API is available</p>

<p>We have no preloaded words</p>

<dl>

<dt>vastitude <a href="http://dictionary.com/browse/vastitude">http://dictionary.com/browse/vastitude</a></dtT4】

<dd>vastness; immensity</dd>

<dt>achromic <a href="http://dictionary.com/browse/achromic">http://dictionary.com/browse/achromic</a></dtT4】

<dd>colorless; without coloring``matterT2】

</dl>

预编译模板以获得最佳性能

正如我们已经看到的,Handlebars.js compile()方法获取一个模板字符串,并将其转换为一个函数,然后可以执行该函数,传递数据以应用于模板。结果是显示在页面上的最终标记字符串。如果您知道您的模板在应用运行时不会改变,您可以利用 Handlebars.js 的预编译特性,该特性允许您提前执行这种模板到函数的转换,向您的应用提供一个较小的 JavaScript 文件,其中只包含要应用数据的模板函数。文件大小要小得多,因为它们可以利用许多优化,否则在运行时是不可能的,并且它们只需要在 HTML 页面中运行 Handlebars.js 的精简运行时版本。这个特殊版本的库删除了通过预编译过程变得多余的函数。手柄的运行时版本可以从主页 http:// handlebarsjs 下载。com 和重量在一个更苗条的 2.3 KB 缩小后,当使用 gzip 压缩服务。这在大小上与 Mustache 库相当,但包含了 Handlebars 的额外好处和简单性,因此它是在代码中使用模板的一个很好的解决方案,而不会牺牲文件大小,因此也不会减少下载时间。如果您正在寻找一个解决方案来提供本章中提到的 gzip 压缩版本的库,请访问。com 来定位对所需文件的内容交付网络托管版本的引用。

预编译步骤需要提前进行,在页面上使用模板之前,我们可以利用 Handlebars.js 提供的命令行工具来执行这个步骤。该工具运行在 Node.js 应用框架上,我们将在下一章详细介绍。现在,你可以从 http:// bit. ly/ node_ js 下载该框架,并按照说明进行安装。同时安装节点包管理器(NPM)工具,并允许轻松安装在框架内运行的应用,包括 Handlebars.js 预编译器应用。在命令行中输入以下命令,安装 Handlebars.js 预编译器 1.3.0 版(Mac 和 Linux 用户可能需要在命令前加上sudo,以授予安装该工具的必要权限,从而可以访问机器上的任何文件夹):

npm install –g handlebars@1.3.0

我们明确说明了与 Handlebars.js 主页上可下载的库版本相匹配的工具的版本号。如果两个版本不匹配,我们就不能保证任何预编译模板都能正常工作。

在命令行上导航到包含模板文件的目录,并执行以下命令来预编译模板,用要预编译的模板文件的名称替换templatefile.handlebars:

handlebars templatefile.handlebars

您会注意到,生成的预编译模板函数被直接写到命令行,而不是保存到文件中,这并不是我们所需要的。要将生成的模板保存到一个新文件中,在命令行中添加--output选项,并指定一个扩展名为.js的文件名(因为我们将返回一个 JavaScript 函数供页面使用)。如果我们还添加了--min选项,生成的 JavaScript 文件将被缩小,为我们节省了以后的优化任务。因此,最终的命令如下所示,其中templatefile.handlebars被替换为要预编译的模板的名称,而templatefile.js被替换为预编译输出 JavaScript 模板文件的名称:

handlebars templatefile.handlebars --output templatefile.js --min

让我们创建一个真实的例子来展示如何使用预编译模板。考虑清单 13-9 所示的模板文件。

清单 13-9。要预编译的手柄模板

<dl>

{{#each words}}

<dt>{{term}} <a href="{{url}}">{{url}}</a></dt>

<dd>{{definition}}</dd>

{{else}}

<p>No words supplied</p>

{{/each}}

</dl>

让我们用清单 13-9 中的模板,用handlebars命令行工具预编译它。我们将使用下面的命令,它生成清单 13-10 所示的简化 JavaScript 代码,代表我们的预编译模板:

handlebars Listing13-9.handlebars --output Listing13-10.js --min

清单 13-10。清单 13-9 中模板的预编译版本

!function(){var a=Handlebars.template,t=Handlebars.templates=Handlebars.templates||{};t["Listing13-9"]=a(function(a,t,e,l,n){function r(a,t){var l,n,r="";return r+="\n        <dt>",(n=e.term)?l=n.call(a,{hash:{},data:t}):(n=a&&a.term,l=typeof n===i?n.call(a,{hash:{},data:t}):n),r+=o(l)+' <a href="',(n=e.url)?l=n.call(a,{hash:{},data:t}):(n=a&&a.url,l=typeof n===i?n.call(a,{hash:{},data:t}):n),r+=o(l)+'">',(n=e.url)?l=n.call(a,{hash:{},data:t}):(n=a&&a.url,l=typeof n===i?n.call(a,{hash:{},data:t}):n),r+=o(l)+"</a></dt>\n        <dd>",(n=e.definition)?l=n.call(a,{hash:{},data:t}):(n=a&&a.definition,l=typeof n===i?n.call(a,{hash:{},data:t}):n),r+=o(l)+"</dd>\n    "}function s(){return"\n        <p>No words supplied</p>\n    "}this.compilerInfo=[4,">=1.0.0"],e=this.merge(e,a.helpers),n=n||{};var d,h="",i="function",o=this.escapeExpression,c=this;return h+="<dl>\n    ",d=e.each.call(t,t&&t.words,{hash:{},inverse:c.program(3,s,n),fn:c.program(1,r,n),data:n}),(d||0===d)&&(h+=d),h+="\n</dl>"})}();

一旦预编译,模板就可以在我们的 HTML 页面中引用使用。因为编译阶段不再需要直接发生在浏览器中,所以我们获得了比浏览器内编译更好的性能。将这一点与该解决方案所需的较小下载量结合起来,您可以看到这种技术对于确保大型 web 应用的良好性能是多么有用。

清单 13-11 中的代码显示了一个示例 HTML 页面,我们可以用它将 JavaScript 数据对象插入预编译的模板,并将结果 HTML 字符串写到当前页面。

清单 13-11。引用预编译模板和 Handlebars.js 库的运行时版本的 HTML 页

<!doctype html>

<html>

<head>

<meta charset="utf-8">

<title>Handlebars.js Example</title>

</head>

<body>

<h1>Handlebars.js Example</h1>

<!-- Create an element to store the result of applying the template to the data -->

<div id="result"></div>

<!-- Load the Handlebars.js runtime library, which should be used with

precompliled templates only -->

<script src="lib/handlebars.runtime.min.js"></script>

<!-- Load our precompiled template -->

<script src="Listing13-10.js"></script>

<!-- Plug the data into the template and render onto the page -->

<script>

// Precompiled templates are added as properties to the Handlebars.templates object

// using their original template name as the key (this name was set by the command line

// tool and stored in the Listing13-10.js file)

var template = Handlebars.templates["Listing13-9"],

// The template is a function, which should be passed the data to render within the

// template. The result is the combination of the two, as a String.

result = template({

words: [{

term: "vastitude",

url: "http://dictionary.com/browse/vastitudeT2】

definition: "vastness; immensity"

}, {

term: "achromic",

url: "http://dictionary.com/browse/achromicT2】

definition: "colorless; without coloring matter"

}]

});

// Write the resulting string onto the current page within the <div id="result">

// element. Produces the following result:

/*

<dl>

<dt>vastitude

<a href="``http://dictionary.com/browse/vastitude">http://dictionary.com/browse/vastitude</aT2】

</dt>

<dd>vastness; immensity</dd>

<dt>achromic

<a href="``http://dictionary.com/browse/achromic">http://dictionary.com/browse/achromic</aT2】

</dt>

<dd>colorless; without coloring matter</dd>

</dl>

*/

document.getElementById("result").innerHTML = result;

</script>

</body>

</html>

运行清单 13-11 中的代码会产生如图 13-3 所示的结果。

A978-1-4302-6269-5_13_Fig3_HTML.jpg

图 13-3。

The resulting page from running the code in Listing 13-9 to combine data with a precompiled template

Handlebars 提供了比 Mustache 更具描述性的模板语言,并通过其自定义助手功能提供了可扩展性。为了克服处理这种模板所需的额外库大小,它提供了预编译模板的能力,以便与库的缩减版本一起使用,从而提供改进的性能和与 Mustache tiny 相似的数据占用大小。js 库。正是由于这些原因,Handlebars 在大型 web 应用中得到了广泛的应用。

备选客户端模板库

我们已经详细研究了 Mustache 和 Handlebars 模板,包括我在内的许多人都认为它是最流行、最受支持的 JavaScript 模板解决方案。然而,这些并不是您唯一可用的选项,包括嵌入式 JavaScript (EJS)和下划线. JS 在内的库也提供了类似的模板功能,我将在本节中解释。

嵌入 JavaScript 的客户端模板(EJS)

嵌入式 JavaScript (EJS)是一种开源的 JavaScript 模板语言,专为那些最熟悉 JavaScript 语言并且喜欢以熟悉的代码友好的方式编码模板逻辑的人而设计。它允许使用简单的 JavaScript 格式的if语句、for循环和数组索引从一组输入数据中输出所需的文本字符串。EJS 图书馆可以从主页通过http://bit . ly/ejs-template下载,当缩小并使用 gzip 压缩时,重量只有 2.4 KB。该库拥有广泛的浏览器支持,可以追溯到 Firefox 1.5 和 Internet Explorer 6。

在 EJS 模板中,要执行的代码包含在以<%开始并以%>结束的部分中,这是 Ruby 语言开发人员用于模板化的风格,要输出的变量包装在<%=%>中——注意附加的等号(=)。

EJS 支持添加视图助手的能力,这些助手可以简化常见输出类型的创建,比如 HTML 链接,可以使用命令<%= link_to("link text", "/url") %>创建以下输出:<a href="/url">link text</a>。可用助手的完整列表可在 EJS 维基网站 http:// bit. ly/ ejs_ wiki 上找到。

这里显示了一个简单的 EJS 示例模板:

Template:

<h1><%= title %></h1>

<dl>

<% for (var index = 0; index < words.length; index++) { %>

<dt><%= link_to(words[index].term, words[index].url) %></dt>

<dd><%= words[index].definition %></dd>

<% }

if (!words) { %>

<p>No words supplied</p>

<% } %>

</dl>

Data Hash:

{

title: "EJS Example",

words: [{

term: "vastitude",

url: "http://dictionary.com/browse/vastitudeT2】

definition: "vastness; immensity"

}, {

term: "achromic",

url: "http://dictionary.com/browse/achromicT2】

definition: "colorless; without coloring matter"

}]

}

Output:

<h1>EJS Example</h1>

<dl>

<dt><a href="http://dictionary.com/browse/vastitude">vastitude</a></dtT2】

<dd>vastness; immensity</dd>

<dt><a href="http://dictionary.com/browse/achromic">achromic</a></dtT2】

<dd>colorless; without coloring matter</dd>

</dl>

与 Mustache.js 和 Handlebars.js 相比,EJS 的一个优势是,它不需要任何特殊代码就可以将远程 JSON 文件中的 JavaScript 数据应用到存储在另一个远程文件中的模板,这需要使用 Mustache.js 和 Handlebars.js 编写额外的 Ajax 代码

典型地,EJS“类”的实例化是通过将 URL 传递给外部模板文件以异步加载,然后执行其render()update()方法,这取决于要应用于模板的数据是否已经存在并加载到 JavaScript 中,在这种情况下,render()方法被传递数据对象并返回输出字符串。向update()方法传递了两个参数,一个是 HTML 页面元素的id,用于在其中呈现结果模板,另一个是 JSON 数据文件的 URL,用于在将数据应用到模板之前异步加载。

new EJS({url: "/templatefile.ejs"}).render(dataObject);

new EJS({url: "/templatefile.ejs"}).update("pageElementId", "/datafile.json");

如果您喜欢使用逻辑与您熟悉的 JavaScript 文件非常相似的模板,那么 EJS 可能是满足您需求的最佳模板解决方案。

下划线. js

Underscore.js 是一个 JavaScript 库,包含 80 多个有用的帮助函数,用于处理代码中的数据集合、数组、对象和函数,以及一系列实用函数,其中一个是专门针对基本模板的。该库可以通过 http:// bit. ly/ u-js 从其主页下载,大小为 5 KB,采用 gzip 编码。当包含在页面中时,它通过下划线(_)全局变量提供对其方法的访问。如果您在代码中使用了 Backbone.js MVC 库(http://bit . ly/backbone _ MVP,您可能会认出 Underscore.js,因为它依赖于这个库。

模板化是通过下划线. js _.template()方法实现的,该方法被传递一个模板字符串并返回一个可以执行的函数,传递用于以 JavaScript 对象的形式呈现模板的数据,与模板化通过 Handlebars.js 实现的方式非常相似。与 EJS 类似,<%%>分隔符表示要执行的代码,<%=%>分隔符表示要写出到结果字符串的变量。除了像iffor这样的 JavaScript 命令,您还可以设置变量并访问整个下划线. js 库,以便在您的模板中利用它的其他实用方法。

一个简单的下划线模板可能如下所示,使用下划线. js _.each()方法迭代数据列表,而不是使用for循环:

Template:

<h1><%= title %></h1>

<dl>

<% _.each(words, function(word) { %>

<dt><a href="<%= word.url %>"><%= word.term %></a></dt>

<dd><%= word.definition %></dd>

<% }

if (!words) { %>

<p>No words supplied</p>

<% } %>

</dl>

Data Hash:

{

title: "Underscore.js Example",

words: [{

term: "vastitude",

url: "http://dictionary.com/browse/vastitudeT2】

definition: "vastness; immensity"

}, {

term: "achromic",

url: "http://dictionary.com/browse/achromicT2】

definition: "colorless; without coloring matter"

}]

}

Output:

<h1>Underscore.js Example</h1>

<dl>

<dt><a href="http://dictionary.com/browse/vastitude">vastitude</a></dtT2】

<dd>vastness; immensity</dd>

<dt><a href="http://dictionary.com/browse/achromic">achromic</a></dtT2】

<dd>colorless; without coloring matter</dd>

</dl>

要在 HTML 页面上使用下划线模板,请引用库和模板,这需要通过 Ajax 手动加载,或者直接从 HTML 页面或 JavaScript 变量引用。然后执行_.template()方法将模板字符串编译成函数,然后可以用数据调用该函数。

var template = _.template(templateString),

output = template(data);

如果您需要在模板中添加一些熟悉的 helper 方法,或者您已经在应用中使用了 Backbone.js MVC 库,那么您可能会发现 Underscore.js 是满足您需求的最佳模板解决方案。

考虑渐进增强

在这一章中,我们已经了解了客户端模板解决方案,它允许您动态加载特殊格式的模板,将它们与 JavaScript 数据结合,然后将结果输出附加到当前 HTML 页面。这使得 web 应用用户无需刷新页面即可获取和显示新内容,就像桌面应用一样。然而,动态内容加载带来了一个警告:构建一个完全通过 JavaScript 呈现和更新显示的 web 应用意味着意外的页面刷新可能会将整个应用重置回其初始视图状态,并且搜索引擎可能会很难抓取通过应用表示的内容,因为不是所有的应用都可以处理 JavaScript。这也违背了网络上的 URL 原则:URL 代表一个对象或一段内容,而不是一个完整的应用。

使用渐进式增强的原则构建您的 web 应用,其中 HTML 链接和表单在被跟踪时会转到单独且不同的 URL,JavaScript 用于防止这些链接和表单导致页面刷新和分层,从而改善用户体验。执行 Ajax 调用并动态加载模板和数据,使用 HTML5 历史 API(http://bit . ly/History API)更新地址栏中的 URL,以匹配表示加载的新数据或显示的页面部分的 URL。如果用户在浏览器中意外刷新了页面,他们将被带到地址栏中更新后的 URL 所代表的内容,并保持在他们之前到达的应用中的相同位置。当 JavaScript 被禁用时,就像大多数搜索引擎爬虫一样,链接仍然工作,允许内容和站点数据像你希望的那样被爬行,在关键点直接创建到你的应用的链接。

摘要

在本章中,我介绍了客户端模板作为一种解决方案,用于构建具有动态更新页面内容的大型 web 应用,将专门标记的模板与 JavaScript 数据相结合,生成包含在当前页面中的 HTML。我已经介绍了流行的 Mustache 和 Handlebars 模板语言,以及它们相关的 JavaScript 库,还有一些替代语言,比如嵌入式 JavaScript (EJS)和下划线. JS

在下一章中,我将介绍 Node.js,这是一个为 JavaScript 语言构建的应用框架,它允许专业 JavaScript 开发人员编写服务器端代码和服务器软件来支持他们的 web 应用,从而将完整的开发堆栈带入 JavaScript 开发人员的领域。