通常,Underscore 和 Handlebars 并不是真正的替代品。 Underscore 是一个具有通用功能实用程序的工具包,可帮助您编写更短、更易于维护的函数式代码。另一方面,Handlebars 是一个完全致力于模板渲染的库,它可以帮助您编写更简洁、更易于维护的模板。
使用 Underscore 时,您可能会发现它的函数在整个 JavaScript 代码中到处都被调用,而 Handlebars 仅在您将呈现模板的地方被调用。出于这个原因,这些库通常根本不会发生冲突。完全有可能编写一个依赖于两者的应用程序(事实上,我在大多数应用程序中已经这样做了一段时间)。只需将两个库链接到您的页面,
<script src="https://cdn.jsdelivr.net/npm/underscore@1.12.0/underscore-min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/handlebars@4.7.7/dist/handlebars.js"></script>
或将两者都作为模块导入,
import _ from 'underscore';
import * as Handlebars from 'handlebars';
或对 AMD 或 CommonJS 等较旧的模块格式执行相同操作。
不过,Underscore 确实有一个 template 函数,这是一个非常小的模板实现,可以用作 Handlebars 等库的替代方案。在您的应用程序的旧版本中似乎就是这种情况,并且由于 Underscore 的最小模板语言与 Handlebars 的模板语言不同而导致冲突。这些库之间的编译和渲染步骤也有些不同。
比较模板语言
这两种模板语言都允许您在一段文本中插入特殊标签,以使该文本的某些部分具有参数化、条件化或重复性。但是它们支持的标签是不同的。
下划线:
-
<%= expression %> 将在文本中插入 JavaScript expression 的字符串值。这称为插值标记。
-
<%- expression %> 会做同样的事情,但 HTML 转义。
-
<% code %> 允许您编写任意 JavaScript code,这将使模板的某些部分成为有条件的或重复的。通常,您会发现一个这样的标签类似于<% for (...) { %>,然后在模板的下方,有一个匹配的<% } %>。这两个代码标签之间的模板部分是一个循环,将由for 的逻辑重复。同样,您可能会发现<% if (...) { %>...<% } %> 使模板的... 部分成为条件。 (老实说,这很丑陋,但它有助于实现最小化。Underscore 的template module 只有一页。)
- 在
<% code %> 的code 部分中,您可能偶尔会找到print(expression)。这是一种简写方式,旨在避免中断代码标签,插入带有expression 的插值标签,然后立即使用新的代码标签继续。换句话说,<% code1 print(expression) code2 %> 是 <% code1 %><%= expression %><% code2 %> 的简写。
车把:
-
{{name}} 在模板中插入带有键 name 的属性的 HTML 转义字符串值。
-
{{{name}}} 做同样的事情,但没有 HTML 转义。
-
仅当满足
condition 时,{{#if condition}}...{{/if}} 才会插入 ... 部分。
-
{{#each name}}...{{/each}} 将为name 的每个元素或属性重复...。 name 成为... 的上下文;也就是说,如果您在... 中写入{{otherName}},Handlebars 将尝试查找otherName 作为name 标识的对象的属性。
-
{{#name}}...{{/name}} 是 Handlebars 继承自 Mustache 的符号。当name 是一个数组时,它的行为类似于{{#each name}},否则类似于{{#if name}}(如果它是一个对象,也将上下文更改为name)。这背后的想法(在 Mustache 中)是使模板更具声明性,或者作者称之为“无逻辑”。
- 还有更多的标签,我现在不再赘述。
将下划线模板转换为把手
由于 Underscore 允许在模板中插入任意 JavaScript 代码,因此并不总是可以将 Underscore 模板转换为等效的 Handlebars 模板。然而,幸运的是,模板并不真正需要 JavaScript 的全部表达能力,因此 Underscore 模板的编写方式很可能可以移植到更具限制性的模板语言(“幸运的是偶然”)。如果可能,大多数情况下以下策略就足够了:
- 将出现在任何
<%...%> 标记内的所有print(_.escape(expression)) 替换为%><%- expression %><%。
- 将
<%...%> 中出现的任何其他print(expression) 替换为%><%= expression %><%。
- 用
{{expression}} 替换所有出现的<%- expression %>(包括在第1 步之前已经存在的)。
- 用
{{{expression}}} 替换所有出现的<%= expression %>(包括在第2 步之前已经出现的)。
- 如果您在任何地方发现
var name = otherName.propertyName 形式的别名(except 在for (...) 语句的括号内),请用otherName.propertyName 替换name,只要此变量在范围内并删除变量。 不要对 for (...) 内的循环变量执行此操作;这些在第 7 步中被替换。
- 如果您发现
<% if (condition1) { %>...<% } else if (condition2) { %>...<% } else ... if (conditionN) { %>...<% } else { %>...<% } %> 形式的任何模式,请从最后一个最里面的块开始,然后从那里向外工作,如下所示:
- 将最后的
<% } else { %> 替换为 {{else}}(Handlebars 认为这是一个特殊的符号)。
- 将最终中间体
<% } else if (conditionN) { %>...<% } %> 替换为{{else}}{{#if conditionN }}...{{/if}}<% } %>。重复此步骤,直到不再有else if。注意最后的<% } %> 保持原位;您将为每个中间 else if 在其前面插入一个额外的 {{/if}}。
- 将最外层的
<% if (condition1) { %>...<% } %> 替换为{{#if condition1}}...{{/if}}。这一次,最后的<% } %> 消失了。
- 替换循环,再次从最里面的表达式开始,然后从那里向外工作:
- 用符号
{{#each objectName}}...{{/each}} 替换<% _.each(objectName, function(valueName, keyName, collectionName) { %>...<% }) %> 形式的对象上的函数循环。检查... 中嵌套的{{}}/{{{}}}/{{#if}}/... 标签是否出现keyName、collectionName 或objectName,并将它们替换为@key4 和@987654分别为..(这不是错字;collectionName 和objectName 都应替换为..,因为它们指的是同一个对象)。请注意,在 Underscore 版本中传递给 _.each 的函数可能需要更少的参数,在这种情况下,collectionName 甚至 keyName 可能不存在;无论如何,替换都是一样的。
- 用符号
{{#each arrayName}}...{{/each}} 替换<% _.each(arrayName, function(valueName, indexName, collectionName) { %>...<% }) %> 形式的数组上的函数循环。检查嵌套的{{}}/{{{}}}/{{#if}}/... 中的indexName、collectionName 或arrayName 标记,并用@index4 和@987654 替换它们.. 分别。同样,在 Underscore 版本中传递给 _.each 的函数可能需要更少的参数;无论如何,替换都是一样的。
- 用符号
{{#each objectName}}...{{/each}} 替换<% for (var keyName in objectName) { %>...<% } %> 形式的对象上的过程循环。检查嵌套{{}}/{{{}}}/{{#if}}/... 内的keyName、objectName[keyName] 或objectName 标记,并将它们替换为@key4 和@987654 ..。
- 用符号
{{#each arrayName}}...{{/each}} 替换<% for (var indexName = 0; indexName < arrayName.length; ++indexName) { %>...<% } %> 形式的数组上的过程循环。检查嵌套{{}}/{{{}}}/{{#if}}/... 内的indexName、arrayName[indexName] 或arrayName 标记,并用@index4 和@987654 替换它们..。
- 修复表达式语法:
- 如果您在上一步中创建了
...propertyName 形式的表达式,其中前两个句点 .. 最初是一个名称(objectName 或 arrayName,如步骤 7 所述),请将其替换为 @ 987654457@。这种形式的路径可能更长,例如../../propertyName。
-
name[index1][index2] 形式的子表达式应转换为 name.[index].[index2](注意句点)。
- 检查您翻译的所有
expressions 和conditions 是否可以由Handlebars 按原样评估。根据经验,Handlebars 只能直接评估当前上下文的(嵌套)属性名称(例如 keyName 或 keyName.subProperty)以及它识别的一些特殊符号,例如 @key、@index、@987654467 @、this 和 ..。使用helpers 来评估不仅仅是某些对象名称和@root 或.. 所需的锚名称的表达式:
- 请注意,
this.propertyName 仅等同于 propertyName,因为 this 指的是当前上下文。
- 当将像
{a: foo, b: {c: baz}} 这样的对象传递给模板时,这个最外层对象的属性总是可以用@root.a、@root.b.c 等来引用。请注意,该对象可能已在原始 Underscore 模板中指定了自己的名称;在这种情况下,此名称本身可以替换为 @root。
-
.. 用于引用循环内的父上下文,正如我们在步骤 7-8 中看到的那样。有时,原始 Underscore 模板中的循环可能会直接通过关闭父上下文的属性来引用它;在这种情况下,您可以根据需要在该属性的名称前加上 ../ 前缀,以帮助 Handlebars 找到正确的属性。
- 在之前的转换之后,您可能有剩余的空
<% %> 标签;这些可以安全移除。
如果在上述步骤之后,您的模板中仍然有 <% code %> 符号,或者 Handlebars 无法计算的表达式,您可能需要使用 Handlebars 语言中的其他工具或创建特殊的解决方法。如果你很倒霉,模板根本无法翻译,但大多数时候还是有办法的。
演示:您的模板
在此处重复您的问题中的下划线模板:
<p id="shipping-rates-feedback" <% if (success) { %> class="success" <% } else { %> class="error" <% } %>>
<% if (success) { %>
<% if (rates.length > 1) { %>
There are <%- rates.length %> shipping rates available for <%- address %>, starting at <%= rates[0].price %>.
<% } else if (rates.length == 1) { %>
There is one shipping rate available for <%- address %>.
<% } else { %>
We do not ship to this destination.
<% } %>
<% } else { %>
<%- errorFeedback %>
<% } %>
</p>
<ul id="shipping-rates">
<% for (var i=0; i<rates.length; i++) { %>
<li><%- rates[i].name %> at <%= rates[i].price %></li>
<% } %>
</ul>
按照上述算法,使用表达式rates.[1]而不是rates.length > 1(因为Handlebars无法评估开箱即用的比较),我们成功获得了以下Handlebars模板:
<p id="shipping-rates-feedback" {{#if success}} class="success" {{else}} class="error" {{/if}}>
{{#if success}}
{{#if rates.[1]}}
There are {{rates.length}} shipping rates available for {{address}}, starting at {{{rates.[0].price}}}.
{{else}}{{#if rates}}
There is one shipping rate available for {{address}}.
{{else}}
We do not ship to this destination.
{{/if}}{{/if}}
{{else}}
{{errorFeedback}}
{{/if}}
</p>
<ul id="shipping-rates">
{{#each rates}}
<li>{{name}} at {{{price}}}</li>
{{/each}}
</ul>
您可能还会发现其他需要翻译的模板。您可以对其他模板采用相同的方法。
最后一点:嵌入在 HTML 中的模板
您的主题在页面中包含具有以下符号的模板。
<script id="shipping-calculator-response-template" type="text/template">
// the template
</script>
重要的是要认识到,尽管这是一个<script> 标记,但浏览器实际上并没有将内容解释为脚本。相反,因为标签有一个浏览器不知道的type,浏览器只是将标签原样留在 DOM 中并继续解释下一个元素。这是在 HTML 页面中嵌入任意文本数据的常用技巧,以便稍后可以由脚本获取,而无需用户查看。在这种特殊情况下,您的 JavaScript 中的某处会执行类似的操作
var templateText = document.querySelector('#shipping-calculator-response-template').textContent;
然后将templateText 传递给Handlebars 以便对其进行处理。这也是为什么用 Underscore 替换 Handlebars 并不能解决您的问题的原因;该脚本仍会尝试将模板传递给 Handlebars。最后,在您的特定情况下,可能不需要放回下划线引用。