三、显示数据
为了呈现视图,Aurelia 依赖于两个核心库:aurelia-templating
,它提供了一个丰富且可扩展的模板引擎;以及aurelia-binding
,它是一个现代且自适应的数据绑定库。由于模板引擎依赖于数据绑定的抽象,这意味着可以使用其他数据绑定库而不是 Aurelia 的,因此aurelia-templating-binding
库充当了两者之间的桥梁。此外,aurelia-templating-resources
构建在模板引擎之上,定义了一组标准行为和组件。
在本章中,我们将介绍数据绑定和模板的基础知识。我们将看到 Aurelia 提供的开箱即用的标准行为以及如何在视图中使用它们。
在呈现任何数据之前,必须首先获取该数据。大多数情况下,单页 web 应用依赖于某种 web 服务。因此,我们将了解 FetchAPI 是什么,如何使用 Aurelia 的 Fetch 客户端,以及如何配置它。
最后,在结束本章之前,我们将通过添加视图来显示联系人列表和联系人详细信息,从而在联系人管理应用上实践我们的新知识。
模板基础
模板是一个 HTML 文件,其根元素是template
元素。它必须是有效的 HTML,因为模板引擎依赖浏览器解析文件并从中构建 DOM 树,引擎将遍历、分析并通过行为丰富 DOM 树。
这意味着应用于 HTML 文件的限制适用于任何 Aurelia 模板。例如,table
元素只能包含某些类型的子元素,如thead
、tbody
或tr
。因此,以下模板在大多数浏览器中是非法的:
<template>
<table>
<compose view="table-head.html"></compose>
</table>
</template>
在这里,我们想使用compose
元素插入一个包含表头的视图,我们将在后面的部分中介绍该元素。由于compose
不是table
的有效子级,大多数浏览器在解析 HTML 文件时会丢弃它,因此模板引擎将无法看到它。
为了绕过这些限制,Aurelia 寻找一个特殊的as-element
属性。此属性充当模板引擎元素名称的别名:
<template>
<table>
<thead as-element="compose " view="table-head.html"></thead>
</table>
</template>
在这里,将元素的名称从compose
更改为thead
使其成为合法的 HTML 片段,并添加as-element="compose"
属性告知 Aurelia 的模板引擎将此thead
元素视为compose
元素。
查看资源
视图资源是可供模板引擎使用的构件,因此模板可以使用它们。例如,自定义元素或值转换器就是资源。
正如我们在前几章中看到的,资源可以全局加载,例如通过应用configure
方法、插件或功能加载。这些资源可用于应用中的每个模板。
本地加载资源
除了全局资源外,每个模板都有自己的一组资源。需要使用非全局可用资源的模板必须首先加载该资源。这是使用require
元素完成的:
src/some-module/some-template.html
<template>
<require from="some-resource"></require>
<!-- at this point, some-resource is available to the template -->
</template>
from
属性必须是要加载的资源的路径。在前面的示例中,路径是相对于代码根的,通常是src
目录。这意味着some-resource
将直接坐在src
中。但是,通过使用./
前缀,可以相对于当前模板文件所在的目录创建路径:
src/some-module/some-template.html
<template>
<require from="./some-resource"></require>
</template>
在本例中,some-resource
应位于src/some-module
目录中。
此外,还可以指定一个as
属性。它用于更改资源的本地名称,以解决与其他资源的名称冲突,例如:
<template>
<require from="some-resource" as="another-resource"></require>
</template>
在本例中,some-resource
在模板中作为another-resource
提供。
资源类型
默认情况下,资源应该是 JS 文件,在这种情况下,路径应该排除.js
扩展名。例如,要加载从sort.js
文件导出的值转换器,模板只需要sort
。无论资源类型、值转换器、绑定行为、自定义元素等如何,这都是正确的,但用作自定义元素的模板除外。
稍后我们将看到如何创建自定义元素。我们还将看到,当组件没有行为时,如何在没有视图模型的情况下创建仅模板的组件。在这种情况下,当作为资源加载时,必须使用其完整文件名(包括扩展名)引用仅模板组件。例如,要加载名为menu.html
的纯模板组件,我们需要menu.html
而不仅仅是menu
。否则,模板引擎将不知道它正在查找 HTML 文件而不是 JS 文件,并将尝试加载menu.js
。当我们开始将应用分解为组件时,我们将看到这方面的真实示例。
加载 CSS
除了本地加载模板资源外,require
元素还可用于加载样式表:
src/my-component.html
<template>
<require from="./my-component.css"></require>
</template>
在本例中,my-component.css
样式表将被加载并添加到文档的头部。
此外,as="scoped"
属性可用于将样式表范围限定到组件:
src/my-component.html
<template>
<require from="./my-component.css" as="scoped"></require>
</template>
在第二个示例中,如果my-component
使用 ShadowDOM 并且浏览器支持它,则样式表将被注入 ShadowDOM 根目录中。否则,它将被注入组件的视图中,scoped
属性将被设置为style
元素。
注
ShadowDOM 是一个 API,允许我们在 DOM 中创建独立的子树。这样的子树可以单独加载自己的样式表和 JavaScript,而不会与周围的文档发生冲突。这项技术是无痛 web 组件开发的核心,但在撰写本文时,浏览器仍不广泛支持这项技术。
style
元素上的scoped
属性告诉浏览器将样式表的范围限制为包含元素及其子元素。这可以防止样式干扰文档的其余部分,而不必使用 ShadowDOM 根。它是 ShadowDOM 的有用替代品,但浏览器仍然不广泛支持它。
数据绑定
数据绑定是使用表达式将模板元素链接到数据模型(JS 对象)的操作。此数据模型称为绑定上下文。Aurelia 使用此上下文向其模板公开组件视图模型的属性和方法。此外,以下部分中描述的一些行为将信息添加到它们的绑定上下文中。
绑定方式
数据绑定支持三种不同的模式:
- 单向:初始计算表达式,并在视图中应用和呈现指令。观察表达式时,只要其值发生变化,就可以对其重新求值,并且指令可以更新视图。它仅以一种方式更改流,即从模型到视图。
-
Two-way: Similar to one-way, but the updates flow both ways: if the template element, such as an
input
, changes from user interaction, the model is updated. It changes flow both ways, from the model to the view, and from the view to the model.注
当然,双向绑定限制了可以绑定到的表达式类型。只有可分配表达式(通常,可以在 JavaScript 分配指令中的 equal(
=
)运算符左侧使用的表达式)可以用于双向绑定。例如,不能双向绑定到条件三元表达式或方法调用。 -
一次性:初始计算表达式并应用指令,但未观察到表达式,因此初始渲染后对模型的任何更改都不会反映在视图上。渲染视图时,绑定仅从模型流向视图一次。
字符串插值
构建模板时最基本的需要是显示文本。这可以通过使用字符串插值来实现:
<template>
<h1>Welcome ${user.name}!</h1>
</template>
与 ES2015 的字符串插值类似,Aurelia 模板中的此类指令对${
和}
之间的表达式进行求值,并将结果作为文本插入 DOM 中。
字符串插值适用于更复杂的表达式:
<template>
<h1>Welcome ${user ? user.name : 'anonymous user'}!</h1>
</template>
在这里,如果用户是在绑定上下文中定义的,我们使用三元表达式来显示用户名,否则使用通用消息。
它也可以在属性中使用:
<template>
<h1 class="${isFirstTime ? ' emphasis' : ''}">Welcome!</h1>
</template>
在本例中,仅当模型的isFirstTime
属性为 truthy 时,我们使用三元表达式将emphasis
CSS 类有条件地分配给h1
元素。
默认情况下,字符串插值指令是单向绑定的。这意味着,只要表达式的值发生更改,就会在文档中重新计算和更新表达式。
数据绑定命令
分析模板中的元素时,模板引擎使用数据绑定命令查找属性。数据绑定命令的后缀为属性,并用点分隔。它指示引擎对此属性执行某种类型的数据绑定。其形式如下:attribute.command="expression"
。
让我们浏览一下 Aurelia 提供的各种绑定命令。
绑定
bind
命令将属性值解释为表达式,并将该表达式绑定到属性本身:
<template>
<a href.bind="url">Go</a>
</template>
在本例中,绑定上下文的url
属性的值将绑定到a
元素的href
属性。
bind
命令是自适应的。它根据目标元素和属性选择绑定模式。默认情况下,它使用单向绑定,除非可以通过用户交互更改目标属性:例如,input
的value
。在这种情况下,bind
执行双向绑定,因此用户引起的更改会反映在模型上。
单向
与bind
类似,该命令执行数据绑定,但不适应其上下文;无论目标的类型如何,绑定都是单向的。
双向
与bind
类似,该命令执行数据绑定,但不适应其上下文,无论目标类型如何,绑定都是双向的。当然,将此命令应用于无法自行更新的属性是无用的。
一次
与bind
类似,此命令执行数据绑定,但强制一次性绑定,这意味着初始渲染后发生的对模型的任何更改都不会反映在视图上。
注
您可能已经推断出,单向和双向绑定提供的一次性绑定比实时绑定轻得多。事实上,由于实时绑定需要观察,因此会消耗更多的 CPU 和内存。在具有数百条数据绑定指令的大型应用中,尽可能使用一次性绑定可以在性能级别上产生巨大的差异。这就是为什么尽可能坚持一次性绑定并仅在必要时使用实时绑定被认为是一种好的做法。
触发器
trigger
命令将事件绑定到表达式,该表达式将在每次触发事件时进行计算。Event
对象作为$event
变量可用于表达式:
<template>
<button click.trigger="open($event)">Open</button>
</template>
在本例中,button
的click
事件将触发对绑定上下文的open
方法的调用,该方法将被传递给Event
对象。当然,使用$event
完全是可选的;这里的点击处理程序可以是open()
,在这种情况下Event
对象将被忽略。
请注意,事件名称的拼写没有任何on
前缀:属性名为click
,而不是onclick
。
代表
当trigger
命令直接在目标元素上附加事件处理程序时,delegate
通过将单个处理程序附加到文档或最近的 ShadowDOM 根来利用事件委派。此处理程序将事件分派到其合法目标,以便对绑定表达式进行计算。
与trigger
一样,Event
对象作为$event
变量可用于表达式,属性名称中必须省略on
前缀。
注
与直接连接到目标元素的事件处理程序相比,事件委派消耗的内存要少得多。就像一次性绑定与实时绑定一样,在较小的应用中,使用委托通常是不明显的,但随着应用的大小的增长,它会对内存占用造成影响。另一方面,在某些场景中,需要将事件处理程序直接附加到元素,特别是在使用禁用的冒泡触发自定义事件时。
电话
call
命令用于将封装表达式的函数绑定到自定义属性或自定义元素的属性。然后,当某个事件发生或满足给定条件时,这些自定义行为可以调用该函数来计算包装表达式。
此外,自定义行为可以传递参数对象,并且此对象上的每个属性都将作为表达式上下文中的变量提供:
<template>
<person-form save.call="createPerson(person)"></person-form>
</template>
在这里,我们可以想象有一个具有save
属性的person-form
自定义元素。在此模板中,我们将person-form
的save
属性绑定到一个函数,该函数包装对模型createPerson
方法的调用,并将表达式范围上的person
变量的值传递给它。
然后,person-form
视图模型会在某个点调用此函数。然后,传递给此函数的参数对象将可用于基础表达式:
this.save({ person: this.somePersonData });
这里,person-form
视图模型调用save
属性上的函数绑定,并向其传递一个person
参数。
显然,这个命令对于本机 HTML 元素是无用的。
当我们讨论定制元素时,我们将看到更多具体的例子。
参考
ref
命令可用于将 HTML 元素或组件部分的引用分配给绑定上下文。如果模板或视图模型需要访问 HTML 元素或模板中使用的某个组件的某个部分,那么它可能非常有用。
在下面的示例中,我们首先使用ref
将模型上的input
元素指定为nameInput
,然后使用字符串插值实时显示该input
的value
:
<template>
<input type="text" ref="nameInput">
<p>Is your name really ${nameInput.value}?</p>
</template>
ref
命令必须用于一组特定属性:
element.ref="someProperty"
(或ref="someProperty"
速记)将创建对 HTML 元素的引用,作为绑定上下文中名为someProperty
的属性- 当放置在具有
some-attribute
自定义属性的元素上时,some-attribute.ref="someProperty"
将创建对此自定义属性的视图模型的引用,作为绑定上下文中名为someProperty
的属性 - 当放置在自定义元素上时,
view-model.ref="someProperty"
将创建对自定义元素视图模型的引用,作为绑定上下文中名为someProperty
的属性 - 当放置在自定义元素上时,
view.ref="someProperty"
将创建对自定义元素的view
实例的引用,作为绑定上下文中名为someProperty
的属性 - 当放置在自定义元素上时,
controller.ref="someProperty"
将创建对自定义元素的Controller
实例的引用,作为绑定上下文中名为someProperty
的属性
绑定文字
模板引擎将没有任何命令的所有属性的值解释为字符串。例如,value="12"
属性将被解释为'12'
字符串。
某些组件可能具有需要特定值类型的属性,例如布尔值、数字,甚至数组或对象。在这种情况下,应该使用数据绑定强制模板引擎将表达式解释为适当的类型,即使表达式是永远不会更改的文本值。例如,value.bind="12"
属性将被解释为数字12
。
类似地,options="{ value: 12 }"
属性将被解释为'{ value: 12 }'
字符串,options.bind="{ value: 12 }"
属性将被解释为具有包含数字12
的value
属性的对象。
当然,当数据绑定到文本值时,最好使用one-time
而不是bind
,以减少应用的内存占用。
使用内置绑定上下文属性
每个绑定上下文都会公开两个属性,这两个属性在某些情况下非常有用:
$this
:自引用属性。它包含对上下文本身的引用。例如,将整个上下文传递给方法或在组合期间将其注入组件中可能很有用。$parent
:引用父绑定上下文的属性。例如,在repeat.for
属性的作用域内,访问由子上下文覆盖的父上下文上的属性可能很有用。它可以链接到绑定上下文树的更高位置。例如,调用$parent.$parent.$parent.name
将尝试访问曾祖父母上下文的name
属性。
绑定到 DOM 属性
一些标准 DOM 属性由 Aurelia 作为属性公开,因此可以进行数据绑定。
innerhtml
innerhtml
属性可用于数据绑定到元素的innerHTML
属性:
<template>
<div innerhtml.bind="htmlContent"></div>
</template>
在本例中,我们可以想象模型的htmlContent
属性将包含 HTML 代码,该代码作为绑定到的innerHTML
属性的数据,将显示在div
中。
但是,此 HTML 不被视为模板,因此模板引擎不会对其进行解释。例如,如果它包含绑定表达式或需要指令,则不会对其求值。
显示用户生成的 HTML 是众所周知的安全风险,因为它可能包含恶意脚本。强烈建议在向任何用户显示此类 HTML 之前对其进行清理。
aurelia-templating-resources
附带一个简单的值转换器(我们将在本章后面看到值转换器是什么),名为sanitizeHTML
,用于此目的。但是,强烈建议您使用更完整的消毒剂,如sanitize-html
,可在找到 https://www.npmjs.com/package/sanitize-html 。
文本内容
textcontent
属性可用于数据绑定到元素的textContent
属性:
<template>
<div textcontent.bind="text"></div>
</template>
在本例中,我们可以想象模型的text
属性将包含一些文本,这些文本作为绑定到div
的textContent
属性的数据,将显示在div
中。
与innerhtml
类似,绑定到textcontent
的文本不被视为模板,因此模板引擎不会对其进行解释。
如前所述,bind
命令尝试检测它应该使用哪种绑定模式。因此,如果元素的contenteditable
属性设置为true
,则textcontent
上的bind
命令(如果有)将使用双向绑定:
<template>
<div textcontent.bind="text" contenteditable="true"></div>
</template>
在本例中,模型的text
属性将绑定到div
的textContent
属性,并显示在div
中。此外,由于div
的内容是可编辑的,因此用户对此内容所做的任何更改都将反映在模型的text
属性上。
风格
style
属性可用于将数据绑定到元素的style
属性。它可以绑定到字符串或对象:
some-component.js
export class ViewModel {
styleAsString = 'font-weight: bold; font-size: 20em;';
styleAsObject = {
'font-weight': 'bold',
'font-size': '20em'
};
}
some-component.html
<template>
<div style.bind="styleAsString"></div>
<div style.bind="styleAsObject"></div>
</template>
此外,style
属性可以与字符串插值一起使用。但是,由于一些技术限制,Internet Explorer 不支持它。为解决此问题,并确保应用与 IE 兼容,在使用字符串插值时应使用css
别名:
<template>
<div css="color: ${color}; background-color: ${bgColor};"></div>
</template>
在这里,div
将其color
和background-color
样式数据绑定到模型的color
和bgColor
属性。
卷轴顶部
scrolltop
属性可用于绑定元素的scrollTop
属性。默认情况下,该属性是双向绑定的,可用于更改元素的水平滚动位置,或将其位置指定给上下文中的属性,以便使用。
向左滚动
scrollleft
属性可用于绑定元素的scrollLeft
属性。默认情况下,此属性是双向绑定的,可用于更改元素的垂直滚动位置,或将其位置指定给上下文中的属性,以便使用。
使用内置行为
核心库aurelia-templating-resources
提供了一套标准行为,建立在aurelia-templating
之上,可以在任何 Aurelia 模板中使用。
秀
show
属性根据元素绑定到的表达式的值控制元素的可见性:
<template>
<p show.bind="hasError">An error occurred.</p>
</template>
在本例中,p
元素只有在模型的hasError
属性为 truthy 时才可见。
该属性的工作原理是在文档头或最近的 ShadowDOM 根中注入一个 CSS 类,并在应该隐藏的元素上添加这个 CSS 类。这个 CSS 类只是将display
属性设置为none
。
隐藏
这与show
类似,但条件相反:
<template>
<p hide.bind="isValid">Form is invalid.</p>
</template>
在本例中,当模型的isValid
属性为 truthy 时,p
元素将被隐藏。
除了反向条件之外,该属性的工作方式与show
完全相同,并且使用相同的 CSS 类。
如果
if
属性与show
非常相似。主要区别在于,当绑定表达式的计算结果为false
值时,它不是简单地隐藏元素,而是从 DOM 中完全删除元素。
<template>
<p if.bind="hasError">An error occurred.</p>
</template>
由于if
属性是一个模板控制器,因此可以将其直接放在嵌套的template
元素上,以控制多个元素的可见性:
<template>
<h1>Some title</h1>
<template if.bind="hasError">
<i class="fa fa-exclamation-triangle"></i>
An error occurred.
</template>
</template>
在本例中,当hasError
为false
时,i
元素及其后面的文本都将从 DOM 中删除
实际上,当条件为 false 时,它所在的元素不会从 DOM 中删除,它自己的行为及其子元素的行为将被解除绑定。这是一个非常重要的区别,因为它对性能有重大影响。
对于下面的示例,让我们假设some-component
非常庞大,显示大量数据,具有许多绑定,并且非常消耗内存和 CPU。
<template>
<some-component if.bind="isVisible"></some-component>
</template>
如果我们在这里将if
替换为show
,那么整个组件层次结构的绑定仍然存在,即使不可见,也会消耗内存和 CPU。使用if
时,当isVisible
变为false
时,组件解除绑定,减少应用中活动绑定的数量。
另一方面,这意味着,当条件变得真实时,必须重新绑定元素及其子元素。在条件经常开关的情况下,最好使用show
或hide
。在if
和show
/hide
之间进行选择主要是为了平衡性能和用户体验之间的优先级,应该有真正的性能测试作为支持。
注
模板控制器是将其所在元素转换为模板的属性。然后,它可以控制该模板的呈现方式。标准属性if
和repeat
是模板控制器。
重复
当与特殊的for
绑定命令一起使用时,repeat
属性可用于为一系列值重复一个元素:
<template>
<ul>
<li repeat.for="item of items">${item.title}</li>
</ul>
</template>
在本例中,li
元素将被重复,并将数据绑定到items
数组中的每个项。
Set
对象也可以是数据绑定对象,而不是数组。
作为模板控制器,repeat
实际上将其所在的元素转换为模板。然后为有界序列中的每个项呈现此模板。对于每个项,都会创建一个子绑定上下文,使用绑定表达式中of
关键字左侧的名称,可以在该子绑定上下文上使用项本身。这意味着两件事:可以根据需要命名 item 变量,也可以在 item 本身的上下文中使用它:
<template>
<ul>
<li repeat.for="person of people"
class="${person.isImportant ? 'important' : ''}">
${person.fullName}
</li>
</ul>
</template>
在本例中,li
元素将插入people
数组中每个项目的ul
元素中。对于每个li
元素,将创建一个子上下文,将当前项作为person
属性公开,如果person
的isImportant
属性对应,则在li
上设置一个important
CSS 类。每个li
元素将包含其person
的fullName
作为文本。
此外,由repeat
创建的子上下文从周围上下文继承,因此li
元素外部可用的任何属性在其内部都可用:
<template>
<ul>
<li repeat.for="person of people"
class="${person === selectedPerson ? 'active' : ''}">
${person.fullName}
</li>
</ul>
</template>
在这里,根绑定上下文公开了两个属性:people
数组和selectedPerson
。当呈现每个li
元素时,除了父上下文之外,每个子上下文都可以访问当前person
。这就是selectedPerson
的li
元素如何拥有active
CSS 类。
repeat
属性默认使用单向绑定,这意味着将观察到有界数组,对其所做的任何更改都将反映在视图上:
如果将一个项添加到数组中,则模板将呈现到另一个视图中,并插入到 DOM 中的适当位置。
如果从数组中删除某个项,则相应的视图元素将从 DOM 中删除。
与地图绑定
repeat
属性可以使用稍微不同的语法处理map
对象:
<template>
<ul>
<li repeat.for="[key, value] of map">${key}: ${value}</li>
</ul>
</template>
这里,repeat
属性将为map
中的每个条目创建一个子上下文,该子上下文具有key
和value
属性,分别与map
条目的key
和value
匹配。
重要的是要记住,这种语法只适用于map
对象。在前面的示例中,如果map
不是Map
实例,则不会在子绑定上下文中定义key
和value
属性。
重复 n 次
当绑定到数字值时,repeat
属性还可以使用标准语法将模板重复给定次数:
<template>
<ul class="pager">
<li repeat.for="i of pageCount">${i + 1}</li>
</ul>
</template>
在本例中,假设pageCount
是一个数字,li
元素将重复等于pageCount
的次数,i
从0
到pageCount - 1
包括在内。
重复模板制作
如果需要重复的内容由多个元素组成,而每个项目没有一个容器,repeat
可以在template
元素上使用:
<template>
<div>
<template repeat.for="item of items">
<i class="icon"></i>
<p>${item}</p>
</template>
</div>
</template>
这里,呈现的 DOM 将是一个包含交替的i
和p
元素的div
元素。
上下文变量
除了当前项本身之外,repeat
还向子绑定上下文添加了其他变量:
$index
:数组中项目的索引$first
:true
如果该项是数组中的第一项;false
否则$last
:true
如果该项是数组中的最后一项;false
否则$even
:true
如果项目索引为偶数;false
否则$odd
:true
如果项目的索引为奇数;false
否则
带属性的
with
属性使用绑定到的表达式创建子绑定上下文。它可用于重新定义模板的一部分范围,以防止长访问路径。
例如,以下模板不使用with
,在访问其属性时会多次遍历person
:
<template>
<div>
<h1>${person.firstName} ${person.lastName}</h1>
<h3>${person.company}</h3>
</div>
</template>
通过将顶部div
元素的范围重新限定为person
,可以简化对其属性的访问:
<template>
<div with.bind="person">
<h1>${firstName} ${lastName}</h1>
<h3>${company}</h3>
</div>
</template>
前面的示例很短,但是您可以想象一个更大的模板如何从中受益。
此外,由于with
创建了一个子上下文,外部作用域可用的所有变量都可以在内部作用域中访问。
焦点属性
focus
属性可用于将元素对文档焦点的所有权数据绑定到表达式。默认情况下使用双向绑定,这意味着当元素获得或失去focus
时,将更新绑定到的变量。
以下代码片段是samples/chapter-3/binding-focus
的摘录:
<template>
<input type="text" focus.bind="hasFocus">
</template>
在前面的示例中,如果hasFocus
是true
,则input
将聚焦于渲染。当hasFocus
更改为false
值时,input
将丢失focus
。另外,如果用户将focus
给予input
,则hasFocus
将被设置为true
。同样,如果用户离开input
,则hasFocus
将设置为false
。
构成要素
组合是实例化组件并将其插入视图中的操作。aurelia-templating-resources
库导出compose
元素,允许我们在视图中动态组合组件。
注
以下章节中的代码片段是samples/chapter-3/composition
的摘录。阅读本节时,您可以并行运行示例应用,以便查看合成的实时示例。
渲染视图模型
可以使用导出其视图模型的 JS 文件的路径组合组件:
<template>
<compose view-model="some-component"></compose>
</template>
在这里,呈现时,compose
元素将加载some-component
视图模型,实例化它,定位它的模板,呈现视图,并将其插入 DOM 中。
当然,view-model
属性可以绑定或使用字符串插值:
<template>
<compose view-model="widgets/${currentWidgetType}"></compose>
</template>
在本例中,compose
元素将根据当前绑定上下文中currentWidgetType
属性的值,显示位于widgets
目录中的组件。当然,这意味着 compose 将在currentWidgetType
更改时交换组件(除非使用一次性绑定)。
此外,view-model
属性可以绑定到视图模型的实例:
src/some-component.js
import {AnotherComponent} from 'another-component';
export class SomeComponent {
constructor() {
this.anotherComponent = new AnotherComponent();
}
}
在这里,一个组件导入并实例化另一个组件的视图模型。在其模板中,compose
元素可以直接绑定到AnotherComponent
的实例:
src/some-component.html
<template>
<compose view-model.bind="anotherComponent"></compose>
</template>
当然,这意味着,如果给anotherComponent
分配了一个新的值,compose
元素将做出相应的反应,并用新的视图替换先前组件的视图。
传递激活数据
呈现组件时,合成引擎将尝试调用组件上的activate
回调方法(如果存在)。与路由器的屏幕激活生命周期方法类似,此方法可以由组件实现,以便在渲染时可以执行操作。它还可用于将激活数据注入组件。
compose
元素还支持model
属性。此属性的值将传递给组件的activate
回调方法(如果有)。
让我们想象一下以下组件:
src/some-component.js
export class SomeComponent {
activate(data) {
this.activationData = data || 'none';
}
}
src/some-component.html
<template>
<p>Activation data: ${activationData}</p>
</template>
当合成时没有任何model
属性,该组件将显示<p>Activation data: none</p>
。但是,在这样构图时会显示<p>Activation data: Some parameter</p>
:
<template>
<compose view-model="some-component" model="Some parameter"></compose>
</template>
当然,model
可以使用字符串插值,也可以进行数据绑定,因此可以将复杂对象传递给组件的activate
方法。
当与未实现activate
方法的组件一起使用时,model
属性的值将被忽略。
呈现模板
compose
元素还可以使用当前绑定上下文简单地呈现模板:
<template>
<compose view="some-template.html"></compose>
</template>
在这里,some-template.html
将使用周围的绑定上下文呈现到视图中。这意味着compose
元素周围可用的任何变量也可用于some-template.html
。
当与view-model
属性一起使用时,view
属性将覆盖组件的默认模板。使用不同的模板重用视图模型的行为非常有用。
值转换器
在数据绑定世界中,在显示期间必须在视图模型和视图之间转换数据,或者在双向绑定更新模型时转换回用户输入,这是非常常见的。
实现这一点的方法之一是在视图模型中使用计算属性来来回转换另一个属性的值。此解决方案的缺点是它不能跨视图模型重用。
在 Aurelia 中,值转换器解决了这一需求。值转换器是可以插入绑定表达式周围的对象。每次绑定需要计算表达式以呈现其结果,或者在双向绑定的情况下更新模型时,转换器充当拦截器并可以转换值。
使用值转换器
值转换器是视图资源。与 Aurelia 中的所有视图资源一样,为了在模板中使用,必须通过configure
函数全局加载或通过require
元素局部加载。
注
如果您不记得如何加载资源,请参阅模板基础部分。
在模板中,可以使用管道(|
操作符将值转换器包装在数据绑定表达式周围:
<template>
<div innerhtml.bind="htmlContent | sanitizeHTML"></div>
</template>
在本例中,我们在innerhtml
属性的绑定中使用内置的sanitizeHTML
值转换器。此值转换器将在绑定过程中通过管道传输,并将从绑定值中清除任何潜在的危险元素。
值转换器实际上不会更改它们所操作的绑定上下文值。它们只是充当拦截器,并为用于呈现的绑定提供替换值。
传递参数
值转换器可以接受参数,在这种情况下,必须使用冒号(:
分隔符在绑定表达式中指定参数。
让我们想象一个名为truncate
的值转换器,它作用于一个字符串值,另外还需要一个length
参数。在求值期间,它将提供的值截断为提供的长度(如果更长),并返回结果。下面是如何使用此转换器:
<template>
<h1>${title | truncate:20}</h1>
</template>
此处,title
如果更长,将被截断为 20 个字符。否则,它将显示不变。
传递多个参数
可以将多个参数传递给值转换器。只需继续使用冒号(:
分隔符即可。例如,如果truncate
可以接受第二个参数,即附加到被截断字符串的省略号,则其传递方式如下:
${title | truncate:20:'...'}
传递上下文变量作为参数
绑定上下文中的变量也可以用作参数,在这种情况下,当这些变量中的任何一个发生变化时,绑定表达式将被重新计算。例如:
some-component.js
export class ViewModel {
title = 'Some title';
maxTitleLength = 2;
}
some-component.html
<template>
<h1>${title | truncate:maxTitleLength}</h1>
</template>
这里,字符串插值的值将取决于视图模型的title
和maxTitleLength
属性。当其中一个发生变化时,表达式将被重新计算,truncate
转换器将被重新执行,视图将被更新。
连锁
值转换器可以链接。在这种情况下,值通过转换器链传输,计算表达式值时从左向右,更新模型时从右向左:
<template>
<h1>${title | truncate:20:'...' | capitalize}</h1>
</template>
在本例中,title
将首先被截断,然后在呈现之前大写。
实现一个值转换器
值转换器是必须至少实现以下方法之一的类:
toView(value: any [, ...args]): any
:在对绑定表达式求值之后,在呈现结果之前调用。value
参数是绑定表达式的值。该方法必须返回转换后的值,该值将传递给下一个转换器或在视图上呈现。fromView(value: any [, ...args]): any
:将绑定目标的值更新模型时调用,然后将值分配给模型。value
参数是绑定目标的值。该方法必须返回转换后的值,该值将传递给下一个转换器或分配给模型。
如果值转换器与参数一起使用,它们将作为附加参数传递给方法。例如,让我们想象一下值转换器的以下用法:
${text | truncate:20:'...'}
在这种情况下,truncate
值转换器的toView
方法应如下所示:
export TruncateValueConverter {
toView(value, length, ellipsis = '...') {
value = value || '';
return value.length > length ? value.substring(0, length) + ellipsis : value;
}
}
在这里,truncate
值转换器的toView
方法除了应用于value
之外,还需要一个length
参数。它还接受名为ellipsis
的第三个参数,该参数具有默认值。如果提供的value
比提供的length
长,则该方法会将其截断,在其上追加ellipsis
,然后返回该新值。如果value
的长度不是太长,它只返回原样。
默认情况下,Aurelia 认为作为资源加载的、名称以ValueConverter
结尾的任何类都是值转换器。值转换器的名称将是类名,不带ValueConverter
后缀,大小写为驼峰。例如,名为OrderByValueConverter
的类将作为orderBy
值转换器提供给模板。
但是,在创建将包含在可重用插件或库中的转换器时,不应依赖此约定。在这种情况下,类应该用valueConverter
装饰符装饰:
import {valueConverter} from 'aurelia-framework';
@valueConverter('truncate')
export Truncate {
// Omitted snippet...
}
这样,即使插件的用户更改了默认命名约定,您的类仍然会被 Aurelia 识别为值转换器。
结合行为
绑定行为是视图资源,类似于值转换器,因为它们应用于表达式。但是,它们拦截绑定操作本身,并可以访问整个绑定指令,因此可以对其进行修改。这开启了许多可能性。
使用绑定行为
要用绑定行为修饰绑定表达式,必须使用&
分隔符将其追加到表达式末尾:
${title & oneTime}
当然,就像值转换器一样,绑定行为可以链接,在这种情况下,它们将从左到右执行:
${title & oneWay & throttle}
如果表达式还使用值转换器,则绑定行为必须位于值转换器之后:
${title | toLower | capitalize & oneWay & throttle}
通过参数
与值转换器一样,可以使用相同的语法将绑定行为传递给参数:
${title & throttle:500}
行为及其参数必须用冒号(:)分隔,参数之间的分隔方式必须相同:
${title & someBehavior:p1:p2}
内置绑定行为
aurelia-templating-resources
库附带了许多绑定行为。让我们来发现它们。
注
以下章节中的代码片段摘自samples/chapter-3/binding-behaviors
。
一次
oneTime
行为使绑定只能单向进行。它可用于字符串插值表达式:
<template>
<em>${quote & oneTime}</em>
</template>
在这里,不会观察到视图模型的quote
属性,因此,如果文本发生更改,则不会更新文本。
此外,Aurelia 附带了其他绑定模式的绑定行为:oneWay
和twoWay
。它们可以像oneTime
一样使用。
油门
throttle
绑定行为可用于限制视图模型更新为双向绑定的速率或视图更新为单向绑定的速率。换句话说,限制为 500 毫秒的绑定将在两个更新通知之间至少等待 500 毫秒。
<template>
${title & throttle}
<input value.bind="value & throttle">
</template>
这里,我们看到了这两个场景的一个示例。第一个throttle
应用于字符串插值表达式,默认情况下是单向的,当视图模型的title
属性更改时,将限制视图中文本的更新。第二个应用于input
的value
属性绑定,默认为双向,当value
更改为input
时,会限制视图模型value
属性的更新。
throttle
行为可以将更新之间的时间间隔作为参数,以毫秒表示。但是,可以忽略此参数,默认情况下将使用 200 毫秒。
<template>
${title & throttle:800}
<input value.bind="value & throttle:800">
</template>
这里,我们有与前面相同的示例,但是绑定将被限制 800 毫秒。
事件也可以被限制。无论是在trigger
还是delegate
绑定命令中使用,都会相应地限制向视图模型发送事件:
<template>
<div mousemove.delegate="position = $event & throttle:800">
The mouse was last moved to (${position.clientX}, ${position.clientY}).
</div>
</template>
在这里,div
元素的mousemove
事件的处理程序将Event
对象分配给视图模型的position
属性。但是,此处理程序将被限制,position
将仅每 800 毫秒更新一次。
您可以在samples/chapter-3/binding-behaviors
中看到throttle
行为的一些示例。
去盎司
debounce
绑定行为也是一种速率限制行为。它确保在给定延迟未发生任何更改之前,不会发送任何更新。
一个常见的用例是自动触发对搜索 API 调用的搜索输入。在每次击键后调用这样一个 API 将效率低下,充其量也会消耗资源。最好在用户停止键入后等待给定的时间间隔,然后再调用搜索 API。这可以使用debounce
完成:
<template>
<input value.bind="searchTerms & debounce">
</template>
在本例中,视图模型将观察searchTerms
属性,并在每次更改时触发搜索。debounce
行为将确保searchTerms
仅在用户停止键入 200 毫秒后更新。
这意味着,当应用于双向绑定时,debounce
会限制视图模型的更新速率。但是,当应用于单向绑定时,它会限制视图的更新速率:
<template>
<input value.bind="text">
${text & debounce:500}
</template>
此处,debounce
应用于字符串插值表达式,因此仅在用户停止输入 500 毫秒后才更新显示的文本。区别在这里很重要。text
属性仍将实时更新。只有字符串插值绑定将被延迟。
与throttle
一样,debounce
可以使用触发器或委托绑定命令应用于事件:
<template>
<div mousemove.delegate="position = $event & debounce:800">
The mouse was last moved to (${position.clientX}, ${position.clientY}).
</div>
</template>
在这里,div
元素的mousemove
事件的处理程序将Event
对象分配给视图模型的position
属性。但是,此处理程序将被取消公告,因此只有当鼠标在div
上停止移动 800 毫秒时,position
才会被更新。
在前面的示例中,您可能注意到,throttle
等debounce
可以将延迟(以毫秒表示)作为参数。省略时,延迟也默认为 200 毫秒。
更新记录器
updateTrigger
绑定行为用于更改触发视图模型更新的事件。隐式地说,这意味着它只能用于双向绑定,并且只能用于支持双向绑定的元素的属性,例如input
的value
、select
的value
或者div
的textcontent
属性和contenteditable="true"
。
使用时,它需要事件名称作为参数,并且至少需要一个:
<template>
<input value.bind="title & updateTrigger:'change':'input' ">
</template>
在这里,视图模型的title
属性将在input
每次触发change
或input
事件时更新。
实际上,change
和input
事件是 Aurelia 中的默认触发器。除了这两个事件外,blur
、keyup
和paste
事件也可以用作触发器。
信号
信号绑定行为允许以编程方式触发绑定更新。当绑定值不可见或必须在特定时间间隔刷新时,这一点特别有用。
让我们想象一个名为timeInterval
的值转换器,它接收Date
对象,计算输入与当前日期和时间之间的间隔,并将该时间间隔作为用户友好的字符串输出,例如a minute ago
、in 2 hours
或3 years ago
。
由于结果取决于当前日期和时间,因此如果不定期刷新,它将很快过时。signal
行为可用于:
src/some-component.html
<template>
Last updated ${lastUpdatedAt | timeInterval & signal:'now'}
</template>
在此模板中,lastUpdatedAt
使用timeInterval
值转换器显示,其绑定由名为now
的signal
修饰。
src/some-component.js
import {inject} from 'aurelia-framework';
import {BindingSignaler} from 'aurelia-templating-resources';
@inject(BindingSignaler)
export class SomeComponent {
constructor(signaler) {
this.signaler = signaler;
}
activate() {
this.handle = setInterval(() => this.signaler.signal('now'), 5000);
}
deactivate() {
clearInterval(this.handle);
}
}
在视图模型中,注入BindingSignaler
实例并将其存储在实例变量中后,activate
方法创建一个间隔循环,每隔 5 秒触发一个名为now
的信号。每次触发信号时,将更新模板中的字符串插值绑定,使显示的时间间隔最多比当前时间晚 5 秒。当然,为了防止内存泄漏,interval 句柄存储在一个实例变量中,并在组件停用时使用clearInterval
函数销毁。
可以将多个信号名称作为参数传递给signal
。在这种情况下,每次触发任何一个信号时都会刷新绑定:
<template>
<a href.bind="url & signal:'signal-1':'signal-2' ">Go</a>
</template>
此外,它只能用于字符串插值和属性绑定;发出一个trigger
、call
或ref
表达式的信号是没有意义的。
计算属性
有效的数据绑定是一个复杂的问题。Aurelia 的数据绑定库是自适应的,使用各种技术尽可能高效地观察视图模型和 DOM 元素。如果可能的话,它会利用 DOM 事件和 Reflect API,然后在没有其他策略适用的情况下返回脏检查。
注
脏检查是一种观察机制,它使用超时循环来重复计算表达式,检查其值自上次计算以来是否已更改,如果已更改,则更新关联的绑定。
经常使用脏检查的场景之一是计算属性。举个例子:
export class ViewModel {
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
当对fullName
应用绑定时,Aurelia 无法知道其值是如何计算的,必须依靠脏检查来检测更改。在本例中,fullName
的 getter 很快就可以进行评估,所以脏检查是绝对可以的。
然而,某些计算属性可能会完成繁重的工作:例如,从大型数组中搜索或聚合数据。在这种情况下,依赖脏检查意味着每秒将对属性进行多次评估,这可能会使浏览器负担过重。
计算自
aurelia-binding
库导出一个computedFrom
修饰符,可用于解决此问题。装饰计算属性时,它会通知绑定系统属性计算其结果所依赖的依赖项。
import {computedFrom} from 'aurelia-binding';
const items = [/* a static, huge list of items */];
export class ViewModel {
@computedFrom('searchTerm')
get matchCount() {
return items.filter(i => i.value.includes(this.searchTerm)).size;
}
}
这里,为了观察matchCount
,绑定系统将观察searchTerm
。只有当它发生变化时才会重新评估matchCount
。这比每秒多次评估属性以检查其结果是否已更改要高效得多。
computedFrom
装饰器接受访问路径作为依赖项,这些依赖项与它所在的类的实例相关:
import {computedFrom} from 'aurelia-binding';
const items = [/* a static, huge list of items */];
export class ViewModel {
model = {
searchTerm: '...'
};
@computedFrom('model.searchTerm')
get matchCount() {
return items.filter(i => i.value.includes(this.searchTerm)).size;
}
}
在这里,我们可以看到,matchCount
依赖于作为视图模型的model
属性存储的对象的searchTerm
属性。
当然,它希望至少有一个依赖项作为参数传递。
computedFrom
装饰者观察属性或路径。它无法观察数组的内容。这意味着以下示例不起作用:
import {computedFrom} from 'aurelia-binding';
export class ViewModel {
items = [/* a huge list of items, that can change during the lifetime of the component */];
searchTerms = '...';
@computedFrom('items', 'searchTerms')
get matchCount() {
return this.items.filter(i => i.value.includes(this.searchTerm)).size;
}
}
在这里,如果items
添加或删除了一个项目,computedFrom
不会检测到它,也不会重新评估matchCount
。它唯一能检测到的是是否将一个全新的数组分配给了items
属性。
computedFrom
装饰器在非常特殊的情况下非常有用。它不应取代值转换器,因为它们是转换数据的首选方式。
从端点获取数据
获取 API
fetchapi 是为获取资源而设计的,包括通过网络获取资源。在撰写本文时,其规范虽然确实很有希望,但尚未获得批准。然而,许多现代浏览器,如 Chrome、Edge 和 Firefox 已经支持它。对于其他材料,需要使用 polyfill。
fetchapi 依赖于请求和响应的概念。这允许拦截管道在发送请求之前修改请求,在接收请求时修改响应。它使身份验证和 CORS 等工作变得更加容易。
在以下部分中,术语Request
和Response
指的是 FetchAPI 的类。Mozilla 开发者网络有大量关于此 API 的文档:https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API 。
使用取数客户端
Aurelia 的 Fetch client 是本机或多填充 Fetch API 的包装器。它支持默认的请求配置以及可插拔的拦截机制。它由一个名为HttpClient
的类组成。此类公开了通过 HTTP 获取资源的方法。
配置
HttpClient
类有一个configure
方法。它需要一个接收配置对象的回调函数作为参数,该对象公开可用于配置客户端的方法:
withBaseUrl(baseUrl: string)
:设置客户端的基本 URL。对相对 URL 的所有请求都将相对于此 URL 发出。withDefaults(defaults: RequestInit)
:设置传递给Request
构造函数的默认属性。withInterceptor(interceptor: Interceptor)
:在拦截管道中增加Interceptor
对象。rejectErrorReponses()
:fetch
方法返回Response
对象的Promise
。此Promise
仅在发生网络错误或类似情况阻止请求完成时被拒绝。否则,无论服务器以何种 HTTP 状态应答,Promise
都会通过Response
成功解析。此方法添加了一个拦截器,当响应的状态不是成功代码时,拦截器拒绝Promises
。HTTP 成功代码介于200
和299
之间。useStandardConfiguration()
:标准配置包括same-origin
凭证设置(有关此设置的更多信息,请参阅官方获取 API 文档)和拒绝错误响应(请参阅前面的rejectErrorResponses
方法)。
除了回调配置函数外,configure
方法还可以直接传递RequestInit
对象。在这种情况下,此RequestInit
对象将用作所有请求的默认属性。
这意味着,如果我们将一个RequestInit
对象存储在defaultProperties
变量中,那么下面两行将执行完全相同的操作:
client.configure(defaultProperties);
client.configure(config => { config.withDefaults(defaultProperties); });
RequestInit
对象对应于 Fetch API 的Request
构造函数所期望的第二个参数。用于指定Request
的各种属性。最常用的是:
method
:HTTP 方法,例如 GET、POSTheaders
:包含请求的 HTTP 头的对象-
body
: The body of the request, for example aBlob
,BufferSource
,FormData
,URLSearchParams
, orUSVString
instance注
我将让您查看官方文档,了解更多有关可用
Request
房产的详细信息。
如您所见,RequestInit
对象可用于指定 HTTP 方法和请求主体,因此我们将能够执行 POST 和 PUT 请求来创建和更新person
对象。我们将在下一章开始构建表单时看到这方面的示例。
常见的陷阱
正如我们在第 2 章、布局、菜单和熟悉中看到的,DI 容器默认自动将所有类注册为应用单例。这意味着,如果您的应用包含多个服务,这些服务依赖于HttpClient
的不同实例,并以不同的方式配置它们各自的HttpClient
,那么您将遇到奇怪的问题。
让我们设想以下两种服务:
import {inject} from 'aurelia-framework';
import {HttpClient} from 'aurelia-fetch-client';
@inject(HttpClient)
export class ContactService {
constructor(http) {
this.http = http.configure(c => c.withBaseUrl('api/contacts'));
}
}
@inject(HttpClient)
export class AddressService {
constructor(http) {
this.http = http.configure(c => c.withBaseUrl('api/addresses'));
}
}
在这里,我们有两个服务,分别命名为ContactService
和AddressService
。它们都作为一个HttpClient
实例注入到它们的构造函数中,并使用不同的基本 URL 配置它们自己的实例。
默认情况下,相同的HttpClient
实例将注入两个服务中,因为 DI 容器默认将其视为应用单例。你看到问题了吗?要创建的第二个服务将覆盖第一个服务的基本 URL,因此第一个服务最终将尝试对错误的 URL 执行 HTTP 调用。
这种情况有许多可能的解决方案。您可以使用NewInstance
解析器强制在每个服务中注入一个新实例:
import {inject, NewInstance} from 'aurelia-framework';
import {HttpClient} from 'aurelia-fetch-client';
@inject(NewInstance.of(HttpClient))
export class ContactService {
constructor(http) {
this.http = http.configure(c => c.withBaseUrl('api/contacts'));
}
}
@inject(NewInstance.of(HttpClient))
export class AddressService {
constructor(http) {
this.http = http.configure(c => c.withBaseUrl('api/addresses'));
}
}
另一个解决方案是在应用的主configure
方法中将HttpClient
类注册为瞬态:
import {HttpClient} from 'aurelia-fetch-client';
export function configure(config) {
config.container.registerTransient(HttpClient);
//Omitted snippet...
}
拦截器
拦截器是可以在 HTTP 调用期间的不同时间拦截请求和响应的对象。Interceptor
对象可以实现以下任何回调方法:
request(request: Request): Request|Response|Promise<Request|Response>
:在发送请求之前调用。它可以修改请求,或者返回一个新的请求。它还可以返回一个响应,以使流程的其余部分短路。在这种情况下,将跳过下一个拦截器的request
方法,并将使用响应,就好像请求已发送一样。Promise
支持。requestError(error: any): Request|Response|Promise<Request|Response>
:当前一个拦截器的request
方法抛出错误时调用。它可能会重新抛出错误以传播错误,或者返回新的请求或响应以从故障中恢复。Promise
支持。response(response: Response, request?: Request): Response|Promise<Response>
:收到响应后调用。它可以修改响应,或者返回一个新的响应。支持Promise
类。responseError(error: any, request?: Request): Response|Promise<Response>
:当前一个拦截器的response
方法抛出错误时调用。它可能会重新抛出错误以传播错误,或者返回新响应以从故障中恢复。Promise
支持。
例如,我们可以定义以下拦截器类:
export class BearerAuthorizationInterceptor {
constructor(token) {
this.token = token;
}
request(request) {
request.headers.set('Authorization', `Bearer ${this.token}`);
}
}
此拦截器期望将Bearer
身份验证令牌传递给其构造函数。当添加到 Fetch 客户端时,它会向每个请求添加一个Authorization
头,允许已经通过身份验证的用户访问安全端点。
我们的申请
至此,我们已经介绍了应用下一步所需的所有内容:查询 HTTP 端点、显示联系人列表以及允许导航到给定联系人的详细信息。
为了让我们的应用更性感,我们将利用 FontAwesome,一个提供可伸缩矢量图标的 CSS 库。让我们首先安装它:
> npm install font-awesome --save
接下来,我们需要将其包括在我们的应用中:
index.html
<head>
<!-- Omitted snippet -->
<link href="node_modules/font-awesome/css/font-awesome.min.css" rel="stylesheet">
</head>
我们的联系网关
我们可以直接在视图模型中进行 HTTP 调用。然而,这将模糊责任之间的界限。视图模型将负责进行调用、解析请求、处理错误并最终缓存响应,所有这些都是除了数据显示之外的,数据显示是其主要任务。
相反,我们将创建一个 contact gateway 类,该类将负责从端点获取数据,可重用,并能够自行发展:
src/contact-gateway.js
import {inject} from 'aurelia-framework';
import {HttpClient} from 'aurelia-fetch-client';
import {Contact} from './models';
import environment from './environment';
@inject(HttpClient)
export class ContactGateway {
constructor(httpClient) {
this.httpClient = httpClient.configure(config => {
config
.useStandardConfiguration()
.withBaseUrl(environment.contactsUrl);
});
}
getAll() {
return this.httpClient.fetch('contacts')
.then(response => response.json())
.then(dto => dto.map(Contact.fromObject));
}
getById(id) {
return this.httpClient.fetch(`contacts/${id}`)
.then(response => response.json())
.then(Contact.fromObject);
}
}
这里,我们首先声明一个类,其构造函数需要一个实例HttpClient
,它是 Aurelia 的 Fetch 客户端。在这个构造函数中,我们配置客户机,使其使用标准配置,我们在配置部分看到了这一点,并使用environment
对象的contactsUrl
属性作为其基本 URL。这意味着具有相对 URL 的所有请求都将相对于此 URL 进行。
我们的联系人网关公开了两种方法:一种是获取所有联系人,另一种是通过 ID 获取单个联系人。它们通过调用客户端的fetch
方法来工作,默认情况下,该方法会向提供的 URL 发送 get 请求。这里,由于 URL 是相对路径,因此它们将相对于构造函数中配置的基本 URL。
HTTP 请求完成后,解析fetch
返回的Promise
,并对解析的Response
对象调用json
方法,将响应体反序列化为 JSON。json
方法还返回一个Promise
,因此当第二个Promise
解析时,我们将转换Contact
类实例中的非类型化数据传输对象,我们将在后面编写。
这意味着,基于端点返回的内容,getAll
返回一个Contact
对象数组的Promise
和一个Contact
对象的getById
和Promise
。
先决条件
为了让所有这些都起作用,我们需要做两件事。首先,我们将通过在移动到应用目录后在控制台中运行以下命令来安装 Fetch 客户端:
npm install aurelia-fetch-client --save
注
为本书编写的所有代码都已在谷歌浏览器上运行。如果使用其他浏览器,则可能需要为各种 API(如 Fetch)安装 polyfills。
此外,您需要让 Aurelia bundler 了解此库。在aurelia_project/aurelia.json
中,在build
下,然后在bundles
下,在名为vendor-bundle.js
的捆绑包定义中,将aurelia-fetch-client
添加到dependencies
数组中:
aurelia_project/aurelia.json
{
//Omitted snippet...
"build": {
//Omitted snippet ...
"bundles": {
//Omitted snippet ...
{
"name": "vendor-bundle.js",
//Omitted snippet ...
"dependencies": [
"aurelia-fetch-client",
//Omitted snippet ...
]
}
}
}
}
这是将aurelia-fetch-client
库与其他供应商库捆绑在一起以便我们的应用可以使用它所必需的。
最后,environment
配置对象上默认不存在contactsUrl
属性。我们需要补充一点:
aurelia_project/environments/dev.js
export default {
debug: true,
testing: true,
contactsUrl: 'http://127.0.0.1:8000/',
};
这里,我们将端点默认运行的 URL 分配给contactsUrl
属性。在真实场景中,我们还将其设置为stage.js
和prod.js
,因此我们的端点配置适用于所有环境。我将把它作为练习留给读者。
显示联系人
现在让我们在空的contact-list
组件中添加一些代码。我们将利用新的ContactGateway
类获取联系人列表并显示它。
src/contact-list.js
import {inject} from 'aurelia-framework';
import {ContactGateway} from './contact-gateway';
@inject(ContactGateway)
export class ContactList {
contacts = [];
constructor(contactGateway) {
this.contactGateway = contactGateway;
}
activate() {
return this.contactGateway.getAll()
.then(contacts => {
this.contacts.splice(0);
this.contacts.push.apply(this.contacts, contacts);
});
}
}
这里,我们首先在contact-list
组件的视图模型中注入一个ContactGateway
实例。在activate
方法中,我们使用getAll
触点,一旦Promise
解析,我们确保清除触点阵列;然后我们将加载的联系人添加到其中,以便它们可用于模板。
在这种情况下,变异数组被认为是比覆盖整个contacts
属性更好的做法,因为视图中的repeat.for
绑定观察数组实例的变异,但不观察属性本身,因此如果在呈现视图后覆盖contacts
,视图将不会被刷新。
您可能已经注意到getAll
返回的Promise
是如何由activate
返回的。这使得对 HTTP 端点的调用作为屏幕激活生命周期的一部分运行。否则,导航可能会在联系人加载之前结束,屏幕显示为空。在这里,我们保证当路由器渲染组件时,联系人可用。
我们还需要定义Contact
类。它具有在列表和详细视图中有用的计算特性:
src/models.js
export class Contact {
static fromObject(src) {
return Object.assign(new Contact(), src);
}
get isPerson() {
return this.firstName || this.lastName;
}
get fullName() {
const fullName = this.isPerson
? `${this.firstName} ${this.lastName}`
: this.company;
return fullName || '';
}
}
此类有一个名为fromObject
的静态方法,它充当工厂方法。它需要一个源对象作为其参数,创建一个新的Contact
实例,并将源对象的所有属性分配给它。此外,它还定义了一个isPerson
属性,如果联系人至少有名字或姓氏,则返回true
,并将在模板中用于区分人员和公司。它还定义了一个fullName
属性,如果联系人代表个人,则返回名字和姓氏;如果联系人是公司,则返回公司名称。
现在,唯一缺少的是contact-list
模板:
src/contact-list.html
<template>
<section class="container">
<h1>Contacts</h1>
<ul>
<li repeat.for="contact of contacts">${contact.fullName}</li>
</ul>
</section>
</template>
在这里,我们只是将联系人呈现为无序列表。
您现在可以测试它:
> au run --watch
注
不要忘记在api
目录下运行npm start
来启动 HTTP 端点。当然,如果您以前没有运行过它,那么首先需要npm install
它的依赖项。
如果没有省略任何步骤,则在导航到时应该会看到联系人列表 http://localhost:9000/ .
联系人分组排序
现在,联系人名单很模糊。联系人显示在项目符号列表中,甚至没有排序。通过按联系人姓名的第一个字母对联系人进行分组并按字母顺序排列,我们可以极大地提高此屏幕的可用性。这将使浏览列表和查找联系人更加容易。
为了实现这一点,我们有两种可能:我们可以在视图模型中对触点进行分组然后排序,或者我们可以在值转换器中隔离此逻辑,以便以后可以重用它们。我们将使用后者,因为它尊重单一责任原则,并使我们的代码更加枯燥。
创建 orderBy 值转换器
我们的orderBy
值转换器将应用于一个数组,并期望将用于对项进行排序的属性的名称作为其第一个参数。
我们的值转换器还将接受可选的第二个参数,即排序方向,作为一个'asc'
或'desc'
字符串。省略时,排序顺序将升序。
src/resources/value-converters/order-by.js
export class OrderByValueConverter {
toView(array, property, direction = 'asc') {
array = array.slice(0);
const directionFactor = direction == 'desc' ? -1 : 1;
array.sort((item1, item2) => {
const value1 = item1[property];
const value2 = item2[property];
if (value1 > value2) {
return directionFactor;
} else if (value1 < value2) {
return -directionFactor;
} else {
return 0;
}
});
return array;
}
}
注
一个重要的部分是在调用sort
之前调用slice
。它确保获取数组的副本,因为sort
方法修改调用它的数组。如果没有slice
调用,原始数组将被修改。这将是糟糕的;值转换器绝对不应修改其源值。这不是预期的行为,因此这样一个转换器对于使用它的开发人员来说将是一个非常糟糕的惊喜。
在设计值转换器时,您确实应该密切注意,以避免此类副作用。
为了使这个新的转换器可用于模板,我们不必在每次需要时手动require
它,而是在resources
功能中加载它:
src/resources/index.js
export function configure(config) {
config.globalResources([
'./value-converters/order-by',
]);
}
您已经可以通过在contact-list
模板中更改contact of contacts | orderBy:'fullName'
的repeat.for
指令对其进行测试。
创建 groupBy 值转换器
接下来,我们的groupBy
值转换器将以几乎相同的方式工作;它将应用于一个数组,并且需要一个参数,该参数将是用于对项进行分组的属性的名称。它将返回一个对象数组,每个对象将包含两个属性:用于分组的值为key
,组中的项为items
数组:
src/resources/value-converters/group-by.js
export class GroupByValueConverter {
toView(array, property) {
const groups = new Map();
for (let item of array) {
let key = item[property];
let group = groups.get(key);
if (!group) {
group = { key, items: [] };
groups.set(key, group);
}
group.items.push(item);
}
return Array.from(groups.values());
}
}
此值转换器还需要加载到resources
功能的configure
功能中。我会让你自己做的。
更新联系人列表
要利用我们的值转换器,我们首先需要向Contact
类添加一个新属性:
src/models.js
//Omitted snippet...
export class Contact {
//Omitted snippet...
get firstLetter() {
const name = this.lastName || this.firstName || this.company;
return name ? name[0].toUpperCase() : '?';
}
}
此新的firstLetter
属性采用联系人的姓氏、名字或公司名称的第一个字母。它将用于将联系人分组在一起。
接下来,让我们扔掉之前的联系人列表模板,重新开始:
src/contact-list.html
<template>
<section class="container">
<h1>Contacts</h1>
<div repeat.for="group of contacts|groupBy:'firstLetter'|orderBy:'key'"
class="panel panel-default">
<div class="panel-heading">${group.key}</div>
<ul class="list-group">
<li repeat.for="contact of group.items|orderBy:'fullName'"
class="list-group-item">
<a route-href="route: contact-details;
params.bind: { id: contact.id }">
<span if.bind="contact.isPerson">
${contact.firstName} <strong>${contact.lastName}</strong>
</span>
<span if.bind="!contact.isPerson">
<strong>${contact.company}</strong>
</span>
</a>
</li>
</ul>
</div>
</section>
</template>
这里,我们首先根据联系人的firstLetter
属性值对其进行分组。groupBy
转换器返回一个组对象数组,然后按其key
属性排序并重复到面板中。对于每个组,在面板标题中呈现联系人分组所依据的字母,然后根据其fullName
属性对组中的联系人进行排序,并呈现到列表组中。对于每个联系人,都会显示一个指向其详细视图的链接,其中包含联系人的姓名或公司名称。
滤波触点
即使对联系人进行分组和排序,查找给定联系人也可能很麻烦,尤其是当用户不知道联系人的全名时。让我们添加一个搜索框,用于实时筛选联系人列表。
我们首先需要创建另一个值转换器来过滤联系人数组:
src/resources/value-converters/filter-by.js
export class FilterByValueConverter {
toView(array, value, ...properties) {
value = (value || '').trim().toLowerCase();
if (!value) {
return array;
}
return array.filter(item =>
properties.some(property =>
(item[property] || '').toLowerCase().includes(value)));
}
}
我们的filterBy
值转换器需要第一个参数,即要搜索的值。此外,它还将以下参数视为将在其上搜索值的属性。任何指定属性均不包含搜索值的联系人都将从结果中筛选出来。
注
不要忘记在resources
功能的configure
功能中加载filterBy
值转换器。
接下来,我们需要添加搜索框并在contact-list
模板中应用我们的值转换器:
src/contact-list.html
<template>
<section class="container">
<h1>Contacts</h1>
<div class="row">
<div class="col-sm-2">
<div class="input-group">
<input type="text" class="form-control" placeholder="Filter"
value.bind="filter & debounce">
<span class="input-group-btn" if.bind="filter">
<button class="btn btn-default" type="button"
click.delegate="filter = ''">
<i class="fa fa-times"></i>
<span class="sr-only">Clear</span>
</button>
</span>
</div>
</div>
</div>
<div repeat.for="group of contacts
| filterBy:filter:'firstName':'lastName':'company'
| groupBy:'firstLetter'
| orderBy:'key'"
class="panel panel-default">
<!-- Omitted snippet... -->
</div>
</section>
</template>
在这里,我们首先以input
元素的形式添加一个搜索框,该元素的value
绑定到filter
属性。此绑定已取消公告,因此只有在用户停止键入 200 毫秒后,属性才会更新。
此外,当filter
不为空时,input
旁边会显示一个按钮。单击此按钮时,会将空字符串分配给filter
。
最后,我们在repeat.for
绑定中将filterBy
应用于contacts
,传递filter
作为搜索值,然后是将要搜索的firstName
、lastName
和company
属性的名称。
注
这里要注意的一件有趣的事情是,我们甚至没有在视图模型上声明filter
属性。它仅在视图中使用。由于它绑定到输入元素的 value 属性,因此默认情况下绑定是双向的,并且绑定只会将其值分配给视图模型。视图模型不需要知道此属性。
联系人详细视图
如果单击联系人,您应该会在浏览器控制台中看到错误。原因很简单:应该显示联系人详细信息的路由引用了一个contact-details
组件,该组件还不存在。让我们纠正这个问题。
视图模型
视图模型将利用我们之前编写的一些类:
src/contact-details.js
import {inject} from 'aurelia-framework';
import {ContactGateway} from './contact-gateway';
@inject(ContactGateway)
export class ContactDetails {
constructor(contactGateway) {
this.contactGateway = contactGateway;
}
activate(params, config) {
return this.contactGateway.getById(params.id)
.then(contact => {
this.contact = contact;
config.navModel.setTitle(contact.fullName);
});
}
}
这段代码非常简单。视图模型期望在其构造函数中注入一个ContactGateway
实例,并实现activate
生命周期回调方法。此方法使用id
路由参数,并向网关请求正确的联系人对象。它返回网关的Promise
,因此只有在加载联系人时导航才会完成。当此Promise
解析时,联系人对象被指定给视图模型的contact
属性。此外,routeconfig
对象用于将文档标题动态分配给联系人的fullName
。
模板
联系人详细信息的模板很大,所以让我们将其分解为多个部分。您可以按照此部分逐步构建模板。
首先,我们添加一个标题,显示联系人的图片和姓名:
<template>
<section class="container">
<div class="row">
<div class="col-sm-2">
<img src.bind="contact.photoUrl" class="img-responsive" alt="Picture">
</div>
<template if.bind="contact.isPerson">
<h1 class="col-sm-10">${contact.fullName}</h1>
<h2 class="col-sm-10">${contact.company}</h2>
</template>
<template if.bind="!contact.isPerson">
<h1 class="col-sm-10">${contact.company}</h1>
</template>
</div>
</section>
</template>
模板的其余部分应放置在关闭的section
标记之前,并封装在一个div
元素中,该元素具有form-horizontal
类:
<div class="form-horizontal">
<!-- the rest of the template goes here. -->
</div>
在此元素中,我们将首先显示创建和上次修改联系人的日期和时间:
<div class="form-group">
<label class="col-sm-2 control-label">Created on</label>
<div class="col-sm-10">
<p class="form-control-static">${contact.createdAt}</p>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">Modified on</label>
<div class="col-sm-10">
<p class="form-control-static">${contact.modifiedAt}</p>
</div>
</div>
接下来,我们将显示联系人的生日,但仅当联系人有生日时:
<div class="form-group" if.bind="contact.birthday">
<label class="col-sm-2 control-label">Birthday</label>
<div class="col-sm-10">
<p class="form-control-static">${contact.birthday}</p>
</div>
</div>
之后,我们将显示联系人的电话号码:
<template if.bind="contact.phoneNumbers.length > 0">
<hr>
<div class="form-group">
<h4 class="col-sm-2 control-label">Phone numbers</h4>
</div>
<div class="form-group" repeat.for="phoneNumber of contact.phoneNumbers">
<label class="col-sm-2 control-label">${phoneNumber.type}</label>
<div class="col-sm-10">
<p class="form-control-static">
<a href="tel:${phoneNumber.number}">${phoneNumber.number}</a>
</p>
</div>
</div>
</template>
这里,块包含在一个模板中,该模板仅在联系人至少有一个电话号码时才呈现。每个电话号码都以其类型显示:例如,家庭、办公室或手机。
接下来的模块都遵循与电话号码相同的模式。他们将显示联系人的电子邮件地址、地理地址和社交档案:
<template if.bind="contact.emailAddresses.length > 0">
<hr>
<div class="form-group">
<h4 class="col-sm-2 control-label">Email addresses</h4>
</div>
<div class="form-group"
repeat.for="emailAddress of contact.emailAddresses">
<label class="col-sm-2 control-label">${emailAddress.type}</label>
<div class="col-sm-10">
<p class="form-control-static">
<a href="mailto:${emailAddress.address}"
target="_blank">${emailAddress.address}</a>
</p>
</div>
</div>
</template>
<template if.bind="contact.addresses.length > 0">
<hr>
<div class="form-group">
<h4 class="col-sm-2 control-label">Addresses</h4>
</div>
<div class="form-group" repeat.for="address of contact.addresses">
<label class="col-sm-2 control-label">${address.type}</label>
<div class="col-sm-10">
<p class="form-control-static">${address.number} ${address.street}</p>
<p class="form-control-static">${address.postalCode} ${address.city}</p>
<p class="form-control-static">${address.state} ${address.country}</p>
</div>
</div>
</template>
<template if.bind="contact.socialProfiles.length > 0">
<hr>
<div class="form-group">
<h4 class="col-sm-2 control-label">Social Profiles</h4>
</div>
<div class="form-group" repeat.for="profile of contact.socialProfiles">
<label class="col-sm-2 control-label">${profile.type}</label>
<div class="col-sm-10">
<p class="form-control-static">
<a if.bind="profile.type === 'GitHub'"
href="https://github.com/${profile.username}"
target="_blank">${profile.username}</a>
<a if.bind="profile.type === 'Twitter'"
href="https://twitter.com/${profile.username}"
target="_blank">${profile.username}</a>
</p>
</div>
</div>
</template>
最后,我们将显示联系人的备注(如果有):
<template if.bind="contact.note">
<hr>
<div class="form-group">
<label class="col-sm-2 control-label">Note</label>
<div class="col-sm-10">
<p class="form-control-static">${contact.note}</p>
</div>
</div>
</template>
由于加载的联系人在组件的生命周期内从未更改,因此通过一次性绑定,可以大大改进此模板。这意味着用one-time
命令替换所有bind
命令,并用oneTime
绑定行为装饰所有字符串插值。我将把这个作为练习留给读者。
总结
如您所见,Aurelia 的数据绑定语言清晰简洁。这是非常不言自明的,使模板易于理解,即使对于不熟悉 Aurelia 的开发人员也是如此。此外,它是自适应的,使得编写性能良好的应用尽可能容易。
除了 Fetch 客户端的便利性之外,这些特性加上值转换器和绑定行为系统的灵活性和可重用性,使得编写数据显示组件变得轻而易举。
构建表单以创建和编辑数据并不复杂。我们将在下一章中看到这一点以及表单验证。
版权属于:月萌API www.moonapi.com,转载请注明出处