Spacebars

Meteor 的 `spacebars` 包的文档。

Spacebars 是一种受 Handlebars 启发的 Meteor 模板语言。它在精神和语法上与 Handlebars 有些相似,但它经过专门设计,在编译时生成响应式的 Meteor 模板。

入门

Spacebars 模板由 HTML 与模板标签交织而成,模板标签用 {{}}(两个花括号)分隔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template name="myPage">
<h1>{{pageTitle}}</h1>

{{> nav}}

{{#each posts}}
<div class="post">
<h3>{{title}}</h3>
<div class="post-content">
{{{content}}}
</div>
</div>
{{/each}}
</template>

如上例所示,模板标签主要有四种类型

  • {{pageTitle}} - 双括号模板标签用于插入一段文本。文本会自动进行安全处理。它可以包含任何字符(如 <),并且永远不会生成 HTML 标签。

  • {{> nav}} - 包含模板标签用于按名称插入另一个模板。

  • {{#each}} - 块模板标签以拥有一个内容块为特点。块标签 #if#each#with#unless 是内置的,并且也可以定义自定义的块标签。一些块标签,如 #each#with,会为评估其内容建立新的数据上下文。在上例中,{{title}}{{content}} 很可能指的是当前帖子的属性(虽然它们也可能指的是模板助手)。

  • {{{content}}} - 三括号模板标签用于插入原始 HTML。使用这些标签时要小心!确保 HTML 是安全的,要么是自己生成 HTML,要么在来自用户输入的 HTML 上进行消毒。

响应式模型

Spacebars 模板会以细粒度级别响应数据变化进行响应式更新。

每个模板标签的 DOM 都会在它评估为新值时自动更新,同时尽可能避免不必要的重新渲染。例如,双括号标签会在其文本值更改时替换其文本节点。#if 仅在条件从真值变为假值或反之时才重新渲染其内容。

标识符和路径

Spacebars 标识符要么是 JavaScript 标识符名称,要么是包含在方括号([])中的任何字符串。还有特殊的标识符 this(或等效地,.)和 ..。如果要使用以下内容作为路径的第一个元素,则需要使用方括号:elsethistruefalsenull。在 JavaScript 关键字和保留字(如 varfor)周围不需要使用方括号。

Spacebars 路径是一系列用 ./ 分隔的一个或多个标识符,例如 foofoo.barthis.name../titlefoo.[0](数字索引必须包含在方括号中)。

名称解析

路径中的第一个标识符以两种方式之一解析

  • 索引当前数据上下文。标识符 foo 指的是当前数据上下文对象的 foo 属性。

  • 作为模板助手。标识符 foo 指的是从当前模板可以访问的助手函数(或常量值)。

模板助手优先于数据上下文属性。

如果路径以 .. 开头,则使用包含数据上下文而不是当前数据上下文。包含数据上下文可能是当前 #each#with 或模板包含外部的数据上下文。

路径评估

在评估路径时,第一个标识符之后的标识符用于索引到迄今为止的对象中,就像 JavaScript 的 . 一样。但是,在尝试索引到非对象或未定义的值时,永远不会抛出错误。

此外,Spacebars 会为您调用函数,因此 {{foo.bar}} 可能意味着 foo().barfoo.bar()foo().bar(),具体取决于情况。

同样,如果访问的对象包装在一个 Promise 中,Spacebars 也将在 Promise 中延迟路径评估。也就是说,{{foo.bar}} 将评估为 foo().then(x => x.bar)。处于挂起状态和拒绝状态都将导致 undefined

助手参数

助手参数可以是任何路径或标识符,也可以是字符串、布尔值或数字字面量,或者为 null。

双括号和三括号模板标签接受任意数量的位置参数和关键字参数

1
{{frob a b c verily=true}}

调用

1
frob(a, b, c, Spacebars.kw({verily: true}))

Spacebars.kw 构造一个 instanceof Spacebars.kw 的对象,其 .hash 属性等于其参数。

助手的实现可以将当前数据上下文访问为 this

如果任何参数是 Promise,Spacebars 将在所有参数都解析后延迟调用。也就是说,{{foo x y z}} 将评估为 Promise.all([x, y, z]).then(args => foo(...args))。处于挂起状态和拒绝状态都将导致 undefined

包含和块参数

包含标签 ({{> foo}}) 和块标签 ({{#foo}}) 接受单个数据参数或不接受参数。任何其他形式的参数将被解释为对象规范嵌套助手

  • 对象规范:如果只有关键字参数,如 {{#with x=1 y=2}}{{> prettyBox color=red}},则关键字参数将组装成一个数据对象,其属性以关键字命名。

  • 嵌套助手:如果有一个位置参数,后面跟着其他(位置或关键字参数),则第一个参数将使用正常的助手参数调用约定在其他参数上调用。

模板标签放置限制

与纯粹基于字符串的模板系统不同,Spacebars 是 HTML 意识的,并且设计为自动更新 DOM。因此,您不能使用模板标签插入不独立存在的 HTML 字符串,例如孤立的 HTML 开始标签或结束标签,或者不能轻松修改的 HTML 字符串,例如 HTML 元素的名称。

HTML 中允许放置模板标签的主要位置有三个

  • 在元素级别(即任何 HTML 标签可以出现的地方)
  • 在属性值中
  • 在开始标签中,位于属性名称/值对的位置

模板标签的行为受其在 HTML 中的位置影响,并且并非所有标签都可以在所有位置使用。

双括号标签

位于元素级别或属性值中的双括号标签通常评估为字符串。如果它评估为其他内容,则该值将被强制转换为字符串,除非该值为 nullundefinedfalse,在这种情况下,将不会显示任何内容。Promise 也受支持 - 请参见下文。

从助手返回的值必须是纯文本,而不是 HTML。(也就是说,字符串应该包含 <,而不是 &lt;。)如果模板被渲染为 HTML,Spacebars 会执行任何必要的转义。

异步内容

值可以包装在 Promise 中。在这种情况下,它将在处于挂起状态或拒绝状态时被视为 undefined。一旦解析,将使用结果值。要更细粒度地处理未解析状态,请使用 #let 和异步状态助手(例如,@pending)。

请注意,以这种方式渲染的值在每次 Promise 更改时都会创建新的 View 对象,这可能会导致闪烁,即在再次出现之前短暂消失。我们计划在下一个版本中进行优化。作为解决方法,请使用 #let 来解包值。

SafeString

如果位于元素级别的双括号标签评估为使用 Spacebars.SafeString("<span>Some HTML</span>") 创建的对象,则 HTML 将插入当前位置。调用 SafeString 的代码断言此 HTML 是安全的,可以插入。

在属性值中

双括号标签可以是 HTML 属性值的一部分,也可以是全部

1
<input type="checkbox" class="checky {{moreClasses}}" checked={{isChecked}}>

完全由返回 nullundefinedfalse 的模板标签组成的属性值被认为是缺失的;否则,该属性被认为是存在的,即使其值为为空。

异步属性

值可以包装在 Promise 中。在这种情况下,它将在处于挂起状态或拒绝状态时被视为 undefined。一旦解析,将使用结果值。要更细粒度地处理未解析状态,请使用 #let 和异步状态助手(例如,@pending)。

动态属性

双括号标签可以在 HTML 开始标签中使用,以指定任意一组属性

1
2
3
<div {{attrs}}>...</div>

<input type=checkbox {{isChecked}}>

该标签必须评估为一个对象,该对象用作属性名称和值字符串的字典。为了方便起见,该值也可以是字符串或 null。空字符串或 null 将扩展为 {}。非空字符串必须是属性名称,并且扩展为具有空值的属性;例如,"checked" 扩展为 {checked: ""}(就 HTML 而言,这意味着复选框被选中)。

总结一下

返回值等效的 HTML
""null{}
"checked"{checked: ""}checked
{checked: "", 'class': "foo"}checked class=foo
{checked: false, 'class': "foo"}class=foo
"checked class=foo"错误,字符串不是属性名称

您可以将多个动态属性标签与其他属性结合使用

1
<div id=foo class={{myClass}} {{attrs1}} {{attrs2}}>...</div>

来自动态属性标签的属性在正常属性之后从左到右组合,后面的属性值会覆盖之前的属性值。不会以任何方式合并同一个属性的多个值,因此,如果 attrs1 指定了 class 属性的值,它将覆盖 {{myClass}}。与往常一样,如果 myClassattrs1attrs2 发生响应式变化,Spacebars 会重新计算元素的属性。

异步动态属性

动态属性可以包装在 Promise 中。在这种情况下,它们将在处于挂起状态或拒绝状态时被视为 undefined。一旦解析,将使用结果值。要更细粒度地处理未解析状态,请使用 #let 和异步状态助手(例如,@pending)。

三括号标签

三括号标签用于将原始 HTML 插入模板

1
2
3
<div class="snippet">
{{{snippetBody}}}
</div>

插入的 HTML 必须由平衡的 HTML 标签组成。例如,您不能插入 "</div><div>" 来关闭现有 div 并打开一个新的 div。

此模板标签不能在属性中使用,也不能在 HTML 开始标签中使用。

异步内容

原始 HTML 可以用 Promise 包装。在这种情况下,如果处于挂起或拒绝状态,它将不会呈现任何内容。解析后,将使用结果值。要更精细地处理未解析状态,请使用 #let 和异步状态帮助程序(例如,@pending)。

包含标签

包含标签采用 {{> templateName}}{{> templateName dataObj}} 的形式。其他参数形式是构建数据对象的语法糖(请参见包含和块参数)。

包含标签在当前位置插入给定模板的实例。如果有参数,它将成为数据上下文,就像使用以下代码一样

1
2
3
{{#with dataObj}}
{{> templateName}}
{{/with}}

包含标签不仅可以命名模板,还可以指定一个路径,该路径评估为模板对象,或者评估为返回模板对象的函数。

请注意,上面两点以一种可能会令人惊讶的方式相互作用!如果 foo 是一个返回另一个模板的模板帮助器函数,那么 {{>foo bar}}首先bar 推送到数据上下文堆栈上,然后调用 foo(),这是由于这行代码的展开方式如上所示。您需要使用 Template.parentData(1) 来访问原始上下文。这与常规帮助器调用(如 {{foo bar}})不同,在常规帮助器调用中,bar 作为参数传递,而不是被推送到数据上下文堆栈上。

返回模板的函数

如果包含标签解析为函数,则该函数必须返回模板对象或 null。该函数将以反应式方式重新运行,如果其返回值发生变化,则该模板将被替换。

块标签

块标签调用内置指令或自定义块帮助器,传递一个模板内容块,该块可能被指令或帮助器实例化一次、多次或根本不实例化。

1
2
3
{{#block}}
<p>Hello</p>
{{/block}}

块标签还可以指定“else”内容,该内容由特殊模板标签 {{else}} 与主内容隔开。

块标签的内容必须由具有平衡标签的 HTML 组成。

块标签可以在属性值内使用

1
2
3
<div class="{{#if done}}done{{else}}notdone{{/if}}">
...
</div>

您可以链接块标签

1
2
3
4
5
6
7
{{#foo}}
<p>Foo</p>
{{else bar}}
<p>Bar</p>
{{else}}
<p></p>
{{/foo}}

这等效于

1
2
3
4
5
6
7
8
9
{{#foo}}
<p>Foo</p>
{{else}}
{{#bar}}
<p>Bar</p>
{{else}}
<p></p>
{{/bar}}
{{/foo}}

If/Unless

#if 模板标签根据其数据参数的值渲染其主内容或其“else”内容。任何虚假的 JavaScript 值(包括 nullundefined0""false)以及空数组都被认为是假的,而任何其他值都被认为是真值。

1
2
3
4
5
{{#if something}}
<p>It's true</p>
{{else}}
<p>It's false</p>
{{/if}}

#unless 只是 #if 的条件反转。

异步条件

条件可以包装在 Promise 中。在这种情况下,如果处于挂起或拒绝状态,#if#unless 都会不呈现任何内容。解析后,将使用结果值。要更精细地处理未解析状态,请使用 #let 和异步状态帮助程序(例如,@pending)。

With

#with 模板标签为其内容建立一个新的数据上下文对象。数据上下文对象的属性是 Spacebars 在解析模板标签名称时查找的位置。

1
2
3
4
{{#with employee}}
<div>Name: {{name}}</div>
<div>Age: {{age}}</div>
{{/with}}

我们可以利用块标签的对象规范形式来定义一个具有我们命名属性的对象

1
2
3
{{#with x=1 y=2}}
{{{getHTMLForPoint this}}}
{{/with}}

如果 #with 的参数是虚假的(与 #if 的规则相同),则不会渲染内容。可以提供一个“else”块,该块将被代替渲染。

如果 #with 的参数是字符串或其他非对象值,则当传递给帮助器时,它可能会被提升为 JavaScript 包装对象(也称为装箱值),因为 JavaScript 传统上只允许对象作为 this。使用 String(this) 获取未装箱的字符串值,或使用 Number(this) 获取未装箱的数字值。

Each

#each 模板标签接受一个序列参数,并为序列中的每个项目插入其内容,并将数据上下文设置为该项目的价值

1
2
3
4
5
<ul>
{{#each people}}
<li>{{name}}</li>
{{/each}}
</ul>

#each 的较新变体不会更改数据上下文,但会引入一个新变量,该变量可以在主体中使用来引用当前项目

1
2
3
4
5
<ul>
{{#each person in people}}
<li>{{person.name}}</li>
{{/each}}
</ul>

参数通常是 Meteor 游标(例如,collection.find() 的结果),但它也可以是普通的 JavaScript 数组、nullundefined

可以提供一个“else”部分,如果序列中始终没有项目,则使用该部分(没有新的数据上下文)。

您可以在 #each 的主体中使用特殊变量 @index 来获取当前渲染的值在序列中的 0 索引。

异步序列

序列参数可以包装在 Promise 中。在这种情况下,如果处于挂起或拒绝状态,#each 将渲染“else”。解析后,将使用结果序列。要更精细地处理未解析状态,请使用 #let 和异步状态帮助程序(例如,@pending)。

Each 的反应式模型

#each 的参数更改时,DOM 始终更新以反映新序列,但有时确切的实现方式非常重要。当参数是 Meteor 活动游标时,#each 可以访问序列的精细更新(添加、删除、移动和更改回调),并且所有项目都是由唯一 ID 标识的文档。只要游标本身保持不变(即查询没有改变),就很容易判断 DOM 在游标内容发生变化时如何更新。每个文档的渲染内容只要文档在游标中就会一直存在,当文档重新排序时,DOM 会重新排序。

如果 #each 的参数在不同的游标对象之间或在可能没有明确标识的普通 JavaScript 对象数组之间以反应式方式更改,那么情况会变得更加复杂。#each 的实现试图在不做太多昂贵工作的情况下变得智能。具体来说,它试图使用以下策略识别旧数组和新数组或游标之间的项目

  1. 对于具有 _id 字段的对象,使用该字段作为标识键
  2. 对于没有 _id 字段的对象,使用数组索引作为标识键。在这种情况下,追加操作很快,但前置操作更慢。
  3. 对于数字或字符串,使用它们的值作为标识键。

如果存在重复的标识键,则所有重复的键(第一个键除外)将被随机键替换。使用具有唯一 _id 字段的对象是完全控制渲染元素标识的方式。

Let

#let 标签为给定表达式创建一个新的别名变量。虽然它不会更改数据上下文,但它允许在模板中使用简写来引用表达式(帮助器、数据上下文、另一个变量)。

1
2
3
{{#let name=person.bio.firstName color=generateColor}}
<div>{{name}} gets a {{color}} card!</div>
{{/let}}

以这种方式引入的变量优先于模板的名称、全局帮助器、当前数据上下文的字段以及先前以相同名称引入的变量。

此外,#let 能够解开 Promise 对象。也就是说,如果任何绑定都指向一个 Promise 对象,那么绑定的值将不会是 Promise 对象,而是解析后的值。挂起状态和拒绝状态都会导致 undefined

异步状态

有三个全局帮助器用于查询绑定的 Promise 的状态

  • @pending,它检查任何给定的绑定是否仍然处于挂起状态。
  • @rejected,它检查任何给定的绑定是否已拒绝。
  • @resolved,它检查任何给定的绑定是否已解析。
1
2
3
4
5
6
7
8
9
10
11
{{#let name=getNameAsynchronously}}
{{#if @pending 'name'}}
We are fetching your name...
{{/if}}
{{#if @rejected 'name'}}
Sorry, an error occured!
{{/if}}
{{#if @resolved 'name'}}
Hi, {{name}}!
{{/if}}
{{/let}}

它们都接受一个要检查的名称列表。不传递任何参数与传递最内层 #let 中的所有绑定相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
{{#let name=getNameAsynchronously}}
{{#let color=getColorAsynchronously}}
{{#if @pending}}
We are fetching your color...
{{/if}}
{{#if @rejected 'name'}}
Sorry, an error occurred while fetching your name!
{{/if}}
{{#if @resolved 'color' 'name'}}
{{name}} gets a {{color}} card!
{{/if}}
{{/let}}
{{/let}}

异步同步

绑定不是同步的。这意味着绑定存储的是最新解析的值,而不是最新 Promise 的值。如果解析时间不同(例如,涉及网络),则可能会导致 UI 不同步。在下面的示例中,渲染的文本不保证是最新 getName 执行的结果。

1
2
3
4
5
<template name="example">
{{#let name=getName}}
Hi, {{name}}!
{{/let}}
</template>
1
2
3
4
5
6
7
Template.example.helpers({
async getName() {
const userId = Meteor.userId(); // Reactive data source.
const profile = await fetch(/* ... */); // Async operation.
return profile.name;
},
});

如果需要明确的解析顺序,请考虑使用外部同步机制,例如,一个挂起异步操作的队列。

自定义块帮助器

要定义自己的块帮助器,只需声明一个模板,然后使用 {{#someTemplate}}(块)而不是 {{> someTemplate}}(包含)语法调用它。

当模板作为块帮助器调用时,它可以使用 {{> Template.contentBlock}}{{> Template.elseBlock}} 来包含传递给它的块内容。

以下是一个简单的块帮助器,它将内容包装在 div 中

1
2
3
4
5
<template name="note">
<div class="note">
{{> Template.contentBlock}}
</div>
</template>

您可以像这样调用它

1
2
3
{{#note}}
Any content here
{{/note}}

以下是用 #if 实现 #unless 的示例(暂时忽略 unless 是内置指令的事实)

1
2
3
4
5
6
7
<template name="unless">
{{#if this}}
{{> Template.elseBlock}}
{{else}}
{{> Template.contentBlock}}
{{/if}}
</template>

请注意,#unless 的参数(条件)成为 unless 模板中的数据上下文,并通过 this 访问。但是,如果此数据上下文对 Template.contentBlock 可见,那么它将无法很好地工作,而 Template.contentBlock 是由 unless 的用户提供的。

因此,当您包含 {{> Template.contentBlock}} 时,Spacebars 会隐藏调用模板的数据上下文,以及模板中由 #each#with 建立的任何数据上下文。它们对内容块不可见,即使通过 .. 也不可见。换句话说,就数据上下文堆栈而言,{{> Template.contentBlock}} 包含就像在调用 {{#unless}} 的位置发生一样。

您可以将参数传递给 {{> Template.contentBlock}}{{> Template.elseBlock}} 以使用您选择的数据上下文调用它。您也可以使用 {{#if Template.contentBlock}} 来查看当前模板是被调用为块帮助器还是包含。

注释标签

注释模板标签以 {{! 开头,可以包含除 }} 之外的任何字符。注释在编译时会被删除,并且永远不会出现在编译后的模板代码或生成的 HTML 中。

1
2
3
4
{{! Start of a section}}
<div class="section">
...
</div>

注释标签也有“块注释”形式。块注释可以包含 {{}}

1
2
3
4
{{!-- This is a block comment.
We can write {{foo}} and it doesn't matter.
{{#with x}}This code is commented out.{{/with}}
--}}

注释标签可以在允许使用其他模板标签的任何位置使用。

嵌套子表达式

有时,对帮助器调用的参数最好用另一个表达式的返回值来表示。为了实现这一点以及其他情况,可以使用括号来表示嵌套表达式的计算顺序。

1
{{capitalize (getSummary post)}}

在这个示例中,getSummary 帮助器调用的结果将传递给 capitalize 帮助器。

子表达式可用于计算关键字参数,

1
{{> tmpl arg=(helper post)}}

HTML 方言

Spacebars 模板是用标准 HTML编写的,并扩展了其他语法(即模板标签)。

Spacebars 在编写过程中会验证您的 HTML 代码,并在您违反基本 HTML 语法,导致无法确定代码结构的情况下抛出编译时错误。

与 Web 浏览器不同,Spacebars 不会宽容处理格式错误的标记。尽管最新的 HTML 规范标准化了浏览器如何从解析错误中恢复,但这些情况仍然不是有效的 HTML。例如,浏览器可能会从一个不构成完整 HTML 标签的裸 < 中恢复,而 Spacebars 则不会。然而,XHTML 时代的限制已经消失;属性值不必加引号,标签也不区分大小写,例如。

您必须关闭所有 HTML 标签,除了那些指定没有结束标签的标签,例如 BR、HR、IMG 和 INPUT。您可以将这些标签写成 <br> 或等效的 <br/>

HTML 规范允许省略一些额外的结束标签,例如 P 和 LI,但 Spacebars 目前不支持此功能。

.html 文件中的顶级元素

严格来说,<template> 元素不是 Spacebars 语言的一部分。Meteor 中的 foo.html 模板文件包含以下一个或多个元素

  • <template name="myName"> - <template> 元素包含一个 Spacebars 模板(如本文档其余部分定义的),它将被编译为 Template.myName 组件。

  • <head> - 将插入到默认 HTML 模板页面 <head> 元素中的静态 HTML 代码。不能包含模板标签。如果 <head> 多次使用(可能在不同的文件中),则所有 <head> 元素的内容将被连接起来。

  • <body> - 将插入到主页面 <body> 中的模板。它将被编译为 Template.body 组件。如果 <body> 多次使用(可能在不同的文件中),则所有 <body> 元素的内容将被连接起来。

转义花括号

要插入一个字面 {{{{{ 或任何数量的花括号,在它后面添加一个竖线。因此 {{| 将显示为 {{{{{| 将显示为 {{{,等等。

在 GitHub 上编辑