如何将 Underscore 模板转换为 Handlebars 模板?

How to translate an Underscore template to a Handlebars template?

我正在升级使用旧主题的 Shopify 商店。

在(旧的)购物车页面中是 'Shipping Estimator' 的代码,他们希望在新主题中重复使用该代码(因为它运行良好)。 我已经复制了相关文件,但在执行并按下计算按钮时,我们会显示以下内容:

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) { %> ....

这来自以下代码:

<script id="shipping-calculator-response-template" type="text/template">
  <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) { %>
    ... 
</script>

所以,我猜脚本并不是 recognise/treated 和 'text/template'

新主题包含对以下内容的引用:

<script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.10/handlebars.min.js"></script>

旧主题:

<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js" type="text/javascript"></script>

所以我把handlebar注释掉了,换成了underscore。但是还是一样的结果。

我是在正确的轨道上,还是以上无关紧要?

我需要从 Underscore 破译的完整代码 - 以及 HandleBars 的重新编码如下:

<script id="shipping-calculator-response-template" type="text/template">
  <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> 
</script>

如果我们能让它发挥作用,会有很多 shopify 商家会很高兴 ;)

通常情况下,Underscore and Handlebars并不是真正的替代品。 Underscore 是一个具有通用函数式实用程序的工具包,可帮助您以函数式风格编写更短、更易于维护的代码。另一方面,Handlebars 是一个完全致力于模板渲染的库,可帮助您编写更简洁、更易于维护的模板。

使用 Underscore 时,您可能会发现它的函数在整个 JavaScript 代码中随处被调用,而 Handlebars 仅在您将呈现模板的地方被调用。因此,这些库通常根本不冲突;完全有可能编写一个依赖于两者的应用程序(事实上,我在我的大多数应用程序中已经这样做了一段时间)。只需 link 将两个库都放入您的页面,

<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)。这是一个 shorthand 的意思是为了避免必须打破代码标签,插入一个带有 expression 的插值标签,然后立即使用新的代码标签恢复。换句话说,<% code1 print(expression) code2 %> 是 shorthand for <% code1 %><%= expression %><% code2 %>.

车把:

  • {{name}} 在模板中插入 属性 的 HTML 转义字符串值,键为 name
  • {{{name}}} 做同样的事情,但是 没有 HTML-escaping.
  • {{#if condition}}...{{/if}} 将仅在满足 condition 时插入 ... 部分。
  • {{#each name}}...{{/each}} 将为每个元素重复 ...name 的 属性。 name 成为 ... 上下文 ;也就是说,如果您在 ... 中写入 {{otherName}},Handlebars 将尝试将 otherName 查找为由 name.[=268= 标识的对象的 属性 ]
  • {{#name}}...{{/name}} 是 Handlebars 继承自 Mustache 的符号。当 name 是一个数组时,它的行为类似于 {{#each name}},否则类似于 {{#if name}}(如果它是一个对象,也会将上下文更改为 name)。这背后的想法(在 Mustache 中)是使模板更具声明性,或者像作者所说的那样“无逻辑”。
  • 还有更多的标签,我现在不会进入。

将 Underscore 模板转换为 Handlebars

由于 Underscore 允许在模板中插入任意 JavaScript 代码,因此并不总是可以将 Underscore 模板转换为等效的 Handlebars 模板。然而,幸运的是,模板并不真正需要 JavaScript 的完整表达能力,因此 Underscore 模板很可能以 可以 移植到更具限制性的方式编写模板语言(“偶然幸运”)。如果可能,以下策略在大多数情况下就足够了:

  1. 将出现在任何 <%...%> 标签内的任何 print(_.escape(expression)) 替换为 %><%- expression %><%
  2. <%...%> 中出现的任何其他 print(expression) 替换为 %><%= expression %><%
  3. {{expression}}.
  4. 替换所有出现的 <%- expression %>(包括在第 1 步之前已经存在的那些)
  5. {{{expression}}}.
  6. 替换所有出现的 <%= expression %>(包括在第 2 步之前已经存在的那些)
  7. 如果您在任何地方找到形式为 var name = otherName.propertyName 的别名(exceptfor (...) 语句的括号内),请将 otherName.propertyName 替换为name 这个变量在范围内的任何地方并删除该变量。 不要for (...) 中使用循环变量这样做;这些在步骤 7 中被替换。
  8. 如果您找到 <% 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}}。这一次,最后的 <% } %> 消失了。
  9. 替换循环,再次从最里面的表达式开始,然后从那里向外工作:
    • 用符号 {{#each objectName}}...{{/each}} 替换 形式 <% _.each(objectName, function(valueName, keyName, collectionName) { %>...<% }) %> 对象 的功能循环。检查 ... 中嵌套的 {{}}/{{{}}}/{{#if}}/{{#each}} 标签是否出现 keyNamecollectionNameobjectName 并分别用 @key.... 替换它们(这不是错字;collectionNameobjectName 都应替换为 .. 因为它们指的是同一个对象)。请注意,在 Underscore 版本中传递给 _.each 的函数可能需要更少的参数,在这种情况下 collectionName 甚至 keyName 可能不存在;无论如何,替换工作都是一样的。
    • 用符号 {{#each arrayName}}...{{/each}} 替换 形式 <% _.each(arrayName, function(valueName, indexName, collectionName) { %>...<% }) %> 数组 上的函数循环。检查 ... 中嵌套的 {{}}/{{{}}}/{{#if}}/{{#each}} 标签是否出现 indexNamecollectionNamearrayName 并分别用 @index.... 替换它们。同样,在 Underscore 版本中传递给 _.each 的函数可能需要更少的参数;无论如何,替换工作都是一样的。
    • 用符号 {{#each objectName}}...{{/each}} 替换 对象 形式 <% for (var keyName in objectName) { %>...<% } %> 的程序循环。检查 ... 中嵌套的 {{}}/{{{}}}/{{#if}}/{{#each}} 标签是否出现 keyNameobjectName[keyName]objectName 并分别替换为 @keythis..
    • 用符号 {{#each arrayName}}...{{/each}} 替换 数组 形式 <% for (var indexName = 0; indexName < arrayName.length; ++indexName) { %>...<% } %> 的程序循环。检查 ... 中嵌套的 {{}}/{{{}}}/{{#if}}/{{#each}} 标签是否出现 indexNamearrayName[indexName]arrayName 并分别替换为 @indexthis..
  10. 修复表达式语法:
    • 如果您在上一步中创建了 ...propertyName 形式的表达式,其中前两个句点 .. 最初是一个名称(objectNamearrayName 作为在步骤 7) 中有描述,用 ../propertyName 替换它。这种形式的路径可能更长,例如 ../../propertyName.
    • name[index1][index2] 形式的子表达式应转换为 name.[index].[index2](注意句点)。
  11. 检查您翻译的所有 expressioncondition 是否可以被 Handlebars 按原样评估。根据经验,Handlebars 只能直接评估(嵌套)属性 当前上下文的名称(例如 keyNamekeyName.subProperty)以及它识别的一些特殊符号,例如 [= 90=、@index@rootthis..。使用 helpers 来评估不仅仅是某些对象名称的表达式和 @root.. 所必需的锚点名称:
    • 请注意 this.propertyName 等同于 propertyName,因为 this 指的是当前上下文。
    • 当向模板传递像{a: foo, b: {c: baz}}这样的对象时,这个最外层对象的属性总是可以用@root.a@root.b.c等引用。请注意,这个对象可能在原始 Underscore 模板中被赋予了自己的名称;在这种情况下,此名称本身可以替换为 @root.
    • .. 用于在循环中引用父上下文,正如我们在步骤 7-8 中看到的那样。有时,原始 Underscore 模板中的循环可能会通过关闭直接引用父上下文的 属性 ;在这种情况下,您可以根据需要在 属性 的名称前加上 ../ 来帮助 Handlebars 找到正确的 属性。
  12. 您可能在之前的转换后留下空的 <% %> 标签;这些可以安全地移除。

如果在执行上述步骤后,您的模板中仍有 <% code %> 符号,或 Handlebars 无法计算的表达式,您可能需要使用 Handlebars 语言的其他工具或创建特殊的解决方法。如果你很倒霉,模板根本无法翻译,但大多数时候会有办法的。

演示:您的模板

在此处重复您问题中的 Underscore 模板:

<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。最后,在您的特定情况下,可能没有必要放回 Underscore 参考。