二、布局、菜单和熟悉
此时,您应该很好地理解如何创建 Aurelia 应用。大局也许还不明朗,但在我们阅读本章的过程中,细节将不断浮现。我们将首先看到依赖注入和 Aurelia 的插件系统是如何工作的,然后我们将看到如何使用、配置和定制 Aurelia logger,以便跟踪和监控代码中发生的事情。最后,我们将探讨 Aurelia 路由器和导航模型。顺便说一句,我们将继续抓取模板,同时通过创建全局布局模板及其导航菜单开始构建实际应用。
在本书中,我们将逐步构建一个应用。在每一章中,我们都将添加功能和技术特性。从这一章开始。因此,在了解技术之前,让我首先描述一下我们的应用将做什么。
我们将构建一个联系人管理应用。此应用将允许用户浏览联系人、执行搜索、创建和编辑条目。当然,它将依赖 HTTP API 来管理数据。此后端可用https://github.com/PacktPublishing/Learning-Aurelia ;它是一个基于 Node.js 的简单服务。只需下载它,将其解压缩到一个目录中,在该目录中启动一个控制台,然后运行npm install
恢复所需的包,然后npm start
启动 web 服务器。
接下来,您应该使用 Aurelia CLI 创建一个空项目,最好使用默认选项。本书中的所有示例和代码示例都是在考虑默认 CLI 设置的情况下构建的;如果自定义项目创建或改用骨架,某些代码段将无法工作。因此,为了保持学习过程尽可能顺利,我强烈建议您从默认设置开始。
依赖注入
坚实的原则最初是由罗伯特·C·马丁(又称鲍勃叔叔)在 21 世纪初提出的。后来由迈克尔·费瑟(MichaelFeathers)发明的助记首字母缩写词有助于《原则》的普及。它们描述了良好的面向对象设计的五个核心关注点。虽然坚实的原则本身不在本书的范围之内,但我们将详细讨论其中的一个:依赖倒置。
依赖倒置原则指出类和模块应该依赖于抽象。当一个类依赖于抽象时,它不能负责创建这些依赖项,必须将它们注入到对象中。这就是我们所说的依赖注入(DI。它极大地提高了解耦性和可组合性,并实施了一种编码风格,其中对象的图形在层次结构的顶部、应用的入口点或入口点附近组成。然后,应用的行为可以在不修改大量代码的情况下进行更改,只需更改应用根目录下对象的组成方式即可。
然而,手动创建整个对象图,或者马克·希曼(MarkSeemann)称之为穷人的 DI,很快就会变得单调乏味。这就是依赖注入容器发挥作用的地方。使用约定和配置的 DI 容器能够理解如何创建对象图。
在 Aurelia 中,几乎所有对象都由 DI 容器提供。这个容器有两个职责:创建和组装对象,然后管理它们的生命周期。它可以做到这一点的一种方法是使用附加到它必须实例化的类的元数据。
注入式装饰器
让我们想象一个PersonListView
组件显示一个人员列表。视图模型需要一个PersonService
实例,用于检索Person
对象列表:
src/person-list-view.js
import {PersonService} from 'app-services';
import {inject} from 'aurelia-framework';
@inject(PersonService)
export class PersonListView {
constructor(personService) {
this.personService = personService;
}
getPeople() {
return this.personService.getAll();
}
}
这里,我们有一个简单的视图模型,它的构造函数需要一个personService
参数。然后将此参数存储在实例变量中,以便以后使用。视图模型还有一个getPeople
方法,调用personService
上的getAll
方法来检索人物列表。如果您熟悉面向对象的设计和依赖项反转,这里没有什么新东西。
这段代码中有趣的是PersonListView
类上的inject
装饰器。此装饰器从 Aurelia 导入,指示 DI 容器解析PersonService
的实例,并在创建PersonListView
的新实例时将其作为构造函数的第一个参数注入。这里重要的是,传递给inject
装饰器的依赖项列表与构造函数期望的参数列表相匹配。如果类有多个依赖项,则必须按正确顺序将它们全部传递给inject
:
src/person-list-view.js
import {PersonService, AnotherService} from 'app-services';
import {inject} from 'aurelia-framework';
@inject(PersonService, AnotherService)
export class PersonListView {
constructor(personService, anotherService) {
this.personService = personService;
this.anotherService = anotherService;
}
getPeople() {
return this.personService.getAll();
}
}
注
装饰器是下一个特性;目前,任何浏览器都不支持它们。此外,Babel 默认不支持它们,因此如果您想在代码中使用它们,您需要添加babel-plugin-transform-decorators-legacy
插件。使用 CLI 创建的项目已启用此设置。
打字脚本和自动注入
如果使用 TypeScript,那么在构造函数声明中指定每个依赖项的类型时,使用inject
装饰器是非常多余的。为了让事情变得更简单,Aurelia 提供了一个autoinject
修饰符,它利用 TypeScript transpiler 添加到传输的 JS 类中的类型元数据。
为了使用autoinject
,您首先需要在您的tsconfig.json
文件中将experimentalDecorators
设置为true
,然后在同一文件的compilerOptions
部分将emitDecoratorMetadata
设置为true
,从而在 TypeScript 中启用装饰器和元数据发射。CLI 创建的 TypeScript 项目已启用这些设置。
下面是一个使用 TypeScript 的相同PersonListView
的示例:
src/person-list-view.js
import {PersonService} from 'app-services';
import {Person} from 'models';
import {autoinject} from 'aurelia-framework';
@autoinject
export class PersonListView {
constructor(private personService: PersonService) {
}
getPeople(){
return this.personService.getAll();
}
}
在这里,DI 容器将知道,为了创建PersonListView
的实例,首先需要解析PersonService
的实例并将其注入PersonListView
的构造函数中,这要归功于autoinject
修饰符。
静态注入方法或性质
如果您不使用 ESNext decorators 或 TypeScript,或者不希望在给定类中具有对 Aurelia 的依赖关系,则可以使用返回这些依赖关系的静态inject
方法声明类的依赖关系:
src/person-list-view.js
import {PersonService} from 'app-services';
export class PersonListView {
static inject() { return [PersonService]; }
constructor(personService) {
this.personService = personService;
}
getPeople() {
return this.personService.getAll();
}
}
静态inject
方法应该返回一个包含类依赖项的数组。
或者,还支持包含依赖项数组的静态inject
属性。这实际上是当你使用inject
或autoinject
装饰器时,在幕后发生的事情,它们只是将依赖项分配给类上的静态inject
属性。它们只是语法上的糖。
根容器和子容器
在 Aurelia 中,容器可以创建子容器,子容器可以自己创建自己的子容器,从应用的根容器开始形成容器树。每个子容器继承其父容器的服务,但可以注册自己的服务以覆盖其父容器的服务。
正如我们在第 1 章、入门中看到的,应用从根组件开始。它也从根容器开始。评估视图时,模板引擎将在每次遇到视图中的子组件时创建子容器,可以是自定义元素、具有自定义属性的元素或通过路由或合成创建的视图模型。子组件的视图模型类将在子容器中注册为单例,然后将用于解析子组件实例。当模板引擎加载和分析该组件的视图时,该过程将递归进行。当组件组成一棵树时,容器也是如此。
由于大多数情况下子容器都是由模板引擎创建的,因此您可能永远不必手动创建子容器。但是,下面是一个如何实现的示例:
let childContainer = container.createChild();
解析实例
实例的解析涉及解析程序。稍后我们将回到这些方面,更详细地解释它们是如何工作以及如何使用的,但同时,将它们视为负责解析 DI 容器请求的类实例的策略。
解析实例时,根容器首先检查它是否已经有类的Resolver
。如果有,则此Resolver
用于获取实例。如果没有找到Resolver
,根容器会自动为类注册一个单例Resolver
,并使用它获取实例。
当使用子容器解析实例时,情况略有不同。子容器仍然会检查类是否有Resolver
,如果有,则仍然使用它获取实例。但是,如果没有找到Resolver
,子容器将把解析委托给其父容器。父级将重复此过程,直到解析实例或解析请求到达根容器。执行此操作时,根容器将按照前面所述解析实例。
这意味着,在第一次解析时动态注册的类的实例是应用单例,因为它们是在根容器中注册的,所以每个子容器最终都会解析到此单例。
视图模型由模板引擎使用容器进行解析,因此您几乎不必手动解析实例。但是,在某些情况下,您需要在对象中插入容器并手动解析服务。以下是如何做到这一点:
let personService = container.get(PersonService);
这里用PersonService
类调用get
方法,并返回该类的一个实例。
寿命
容器创建的任何对象都有生存期。有三种典型的生命周期:
- 容器单例:容器在第一次请求类时实例化该类,然后保留对该实例的引用。每隔一次从容器中请求类的实例时,都会返回相同的实例。这意味着实例的生命周期与容器的生命周期相关联。在容器被丢弃并且没有其他对象持有对实例的引用之前,它不会被垃圾收集。
- 应用单例:注册为应用单例的类只是注册在应用根容器中的容器单例,因此在整个应用中重复使用同一实例。
- Transient:当类注册为 Transient 时,容器会在每次请求实例时创建一个新实例。它不会引用这些实例中的任何一个。容器只是一个工厂。
注册
为了解析类的实例,容器必须首先了解它。这个学习过程称为注册。大多数情况下,当容器收到解析请求时,它会自动并动态地执行。也可以使用容器的注册 API 手动执行。
集装箱登记 API
Container
类提供多种方法手动注册一个类。
container.registerSingleton(key: any, fn?: Function): void
此方法将类注册为容器单例。key
将用于查找,fn
预计是将被实例化的类。如果只提供了key
,那么它应该是一个类,因为它将用于查找和实例化。
例如,container.registerSingleton(HttpClient)
将HttpClient
类注册为单例。第一次解析HttpClient
时,会创建并返回一个实例。对于HttpClient
的每个后续解决请求,将返回此单个实例。
或者,container.registerSingleton(PersonService, CachingPersonService)
使用PersonService
作为键注册CachingPersonService
类。这意味着在解析PersonService
类时,将返回CachingPersonService
的单个实例。在处理抽象时,这种映射是至关重要的。
当然,类是容器单例还是应用单例这一事实仅仅取决于调用它的容器是否是应用的根容器。
container.registerTransient(key: any, fn?: Function): void
此方法将类注册为 transient,这意味着每次请求key
时,都会创建一个新的fn
实例。与registerSingleton
一样,fn
可以省略,在这种情况下key
将用于查找和实例创建。
container.registerInstance(key: any, instance?: any): void
此方法将现有实例注册为单例。如果您已经有一个实例,并且希望在容器中注册它,那么这将非常有用。与registerSingleton
的唯一区别在于,不是传递类,而是传递实际使用的单个实例。如果只提供了key
,它将用于查找和作为实例,但我看不出在哪些情况下这会有用,因为您需要已经具有该值才能进行查找。
例如,container.registerInstance(HttpClient, myClient)
为HttpClient
类注册myClient
实例。每次从容器请求一个HttpClient
实例时,myClient
实例将被返回:
container.registerHandler(key: any,
(container?: Container, key?: any, resolver?: Resolver) => any): void
此方法注册一个自定义处理程序,该函数将在每次为key
请求容器时调用。此处理程序函数将传递给容器、key
和存储处理程序的内部Resolver
。这允许支持标准单例和瞬态生命周期之外的多个场景。
例如,container.registerHandler(PersonService, () => new PersonService(myConfig))
注册一个工厂函数。每次从容器请求一个PersonService
实例时,将调用 handler 函数,并使用捕获的myConfig
值创建一个新的PersonService
实例:
container.registerResolver(key: any, resolver: Resolver): void
此方法注册一个自定义Resolver
实例。在幕后,我们之前看到的所有容器方法都将此方法与内置解析器一起使用。但是,我们可以创建自己的Resolver
实现。
注
虽然键在大多数情况下是类,但它们可以是任何东西,包括字符串、数字、符号或对象。
自动注册
类的自动注册由以下类方法处理:
container.autoRegister(key: any, fn?: Function): Resolver
可以使用单个参数(要注册的类)调用此方法,也可以使用两个参数调用此方法,第一个参数是必须注册类的密钥,第二个参数是要注册的类。当只传递一个参数时,类本身被用作键。
当容器尝试解析一个类的实例时,它会自动调用autoRegister
,而该类的实例找不到任何解析程序。应用很少直接使用它。
注册策略
可以通过将Registration
策略附加到类的元数据来定制给定类的自动注册过程。这可以使用其中一个注册装饰器完成:
import {transient} from 'aurelia-framework';
@transient()
export class MyModel {}
在本例中,transient
装饰器将告诉autoRegister
方法MyModel
类必须注册为 transient,因此每次容器必须解析MyModel
实例时,它都会创建一个新实例。
或者,您可以使用singleton(registerInChild: boolean = false)
装饰器。当registerInChild
参数为false
时(默认情况下是如此),此装饰器会告诉autoRegister
方法该类应在根容器上注册为单例。这使得该类成为应用单例,这是容器的默认行为,因此使用singleton
并将registerInChild
设置为false
或保留其默认值是毫无用处的。
但是,registerInChild
设置为true
的singleton
表示类应该注册为 singleton,而不是在根容器上,而是在调用autoRegister
方法的实际容器上。这允许我们装饰一个类,使每个容器都有自己的实例:
import {singleton} from 'aurelia-framework';
@singleton(true)
export class MyModel {}
在本例中,MyModel
将注册为容器单例。每个容器都有自己的实例。
那两个装饰师在幕后依靠registration(registration: Registration)
。第三个装饰器用于将Registration
策略与类关联。如果您创建自己的自定义Registration
策略,则可以使用它。transient
和singleton
在幕后使用它将内置Registration
策略之一附加到他们装饰的班级。
创建自定义注册策略
注册策略必须实现以下方法:
registerResolver(container: Container, key: any, fn: Function): Resolver
默认情况下,autoRegister
方法将传递给它的类注册为应用单例。但是,当被调用时,一个类的元数据中附加了一个Registration
策略autoRegister
将把类的注册委托给Registration
的registerResolver
方法,该方法将为该类创建一个Resolver
,在容器中注册并返回该类。
通常,registerResolver
方法实现将使用作为参数传递的Container
实例的注册 API 来注册类。例如,transient
装饰师在幕后使用的内置TransientRegistration
类的registerResolver
方法如下:
registerResolver(container, key, fn) {
return container.registerTransient(key, fn);
}
在这里,该方法调用容器的registerTransient
方法,该方法创建一个瞬态Resolver
,并返回它。
分解器
我们之前将Resolver
定义为负责解决实例的策略。当一个容器被剥离到最低限度时,它只需管理一个与它们各自的Resolver
相关的Map
,该Resolver
由Registration
策略或容器注册方法创建。
除了在注册服务时使用解析程序外,在声明依赖项时还可以使用解析程序:inject
装饰程序(顺便提一下,inject
静态方法或属性可以作为Resolver
而不是key
传递。正如我们前面所看到的,在解析key
依赖项的过程中,容器或其祖先之一将找到密钥的Resolver
,或者根容器将自动注册一个单例Resolver
,该Resolver
将用于解析实例。但在解析Resolver
依赖项时,容器将直接使用此Resolver
解析实例。这允许我们在特定注入的上下文中重写给定类的注册解析策略。
通常有六个解析器在注入期间非常有用。
懒惰
Lazy
解析器注入一个函数,该函数在求值时会延迟解析依赖项:
import {Lazy, inject} from 'aurelia-dependency-injection';
import {PersonService} from 'person-service';
@inject(Lazy.of(PersonService))
Export class PersonListView {
constructor(personServiceAccessor) {
this.personServiceAccessor = personServiceAccessor;
}
getPeople() {
return this.personServiceAccessor().getAll();
}
}
这意味着PersonService
的解析不会在创建实例时执行,而是在调用personServiceAccessor
函数时执行。如果需要将解析委派给以后的时间,而不是创建对象时,或者在对象的生命周期内必须多次重新计算解析,则此选项非常有用。
全部
默认情况下,Container
解析为与请求的密钥匹配的第一个实例。All
解析器允许我们注入一个数组,其中包含为给定密钥注册的所有服务:
import {All, inject} from 'aurelia-dependency-injection';
import {PersonValidator} from 'person-validator';
@inject(All.of(PersonValidator))
Export class PersonForm {
constructor(validators) {
this.validators = validators;
}
validate() {
for (let i = 0; i < this.validators.length; ++i) {
this.validators[i].validate();
}
}
}
在这里,我们可以想象多个对象或类已经使用PersonValidator
键注册,并且它们都作为一个数组注入PersonForm
视图模型。
可选
Optional
解析器仅在给定密钥已注册的情况下注入实例。如果没有,则不会自动注册,而是注入null
。第二个参数被省略或设置为true
时,将使解析器的查找上升到容器层次结构。如果设置为false
,则只检查当前容器。
import {Optional, inject} from 'aurelia-dependency-injection';
import {PersonService} from 'person-service';
@inject(Optional.of(PersonService, false))
Export class PersonListView {
constructor(personService) {
this.personService = personService;
}
getPeople() {
return this.personService ? this.personService.getAll() : [];
}
}
这里,只有当PersonService
的实例已经在当前容器中注册时,才会将其注入PersonListView
构造函数中。如果不是,则注射null
。
父母
Parent
解析程序跳过当前容器并在父容器开始解析。如果当前容器是根容器,则注入null
:
import {Parent, inject} from 'aurelia-dependency-injection';
import {PersonService} from 'person-service';
@inject(Parent.of(PersonService))
Export class PersonListView {
constructor(personService) {
this.personService = personService;
}
}
工厂
Factory
解析器注入工厂函数。每次执行工厂函数时,它都会从容器中请求一个新实例。此外,传递到此工厂函数的任何参数都将由容器传递给类构造函数。如果类具有使用任何inject
策略声明的依赖项,则在传递给构造函数时,附加参数将附加到已解析的依赖项:
import {Factory, inject} from 'aurelia-dependency-injection';
import {AddressService} from 'address-service';
@inject(AddressService)
class Person {
constructor(addressService, address) {
this.addressService = addressService;
this.address = address;
}
}
@inject(Factory.of(Person))
export class PersonListView {
constructor(personFactory) {
this.personFactory = personFactory;
}
createPerson(address) {
return this.personFactory(address);
}
}
在本例中,我们首先看到一个用inject
修饰的Person
类,它向容器建议其构造函数需要一个AddressService
实例作为其第一个参数。我们还可以看到,构造函数实际上需要第二个参数address
,容器对此一无所知。接下来,我们有一个PersonListView
类,以这样一种方式装饰,Person
工厂被注入其构造函数中。它的createPerson
方法通过address
调用具有此地址的Person
工厂函数。
调用时,为了创建Person
实例,容器首先解析AddressService
实例以满足Person
依赖关系,然后调用Person
构造函数,解析后的AddressService
实例和传递给工厂函数的address
。
新实例
NewInstance
解析器使容器每次注入类的新实例,完全忽略类的任何现有注册。
import {NewInstance, inject} from 'aurelia-dependency-injection';
import {PersonService} from 'person-service';
@inject(NewInstance.of(PersonService))
Export class PersonListView {
constructor(personService) {
this.personService = personService;
}
}
插件系统
现在我们已经很好地理解了依赖注入在 Aurelia 中的工作原理,我们可以开始使用它了。除了使用inject
和Resolver
创建和合成组件外,依赖注入也是 Aurelia 插件系统的核心。
插件
Aurelia 的几乎每个部分都是作为插件提供的。事实上,aurelia-framework
库只是一个插件系统和配置机制,其他所有 Aurelia 核心库都加入了这个机制。
Aurelia 插件以一个index.js
文件开始,该文件必须导出一个configure
函数。该函数将在启动时由 Aurelia 调用,并将接收一个 Aurelia 配置对象作为其第一个参数和一个可选的配置回调函数。
一个例子
让我们想象一个名为our-plugin
的插件。首先需要在我们的main.js
文件的configure
功能中启用此插件:
src/main.js
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging()
.plugin('our-plugin', config => { config.debug = true; });
aurelia.start().then(() => aurelia.setRoot());
}
在这里,除了标准应用配置之外,我们还告诉 Aurelia 加载our-plugin
。我们还告诉 Aurelia 使用提供的回调作为plugin
函数的第二个参数来配置our-plugin
。此回调接收由our-plugin
定义的配置对象,我们将其debug
属性设置为true
。
现在让我们想象一下我们插件的index.js
文件:
export function configure(aurelia, callback) {
let config = { debug: false };
if (typeof callback === 'function') {
callback(config);
}
aurelia.container.registerInstance(OurPluginConfig, config);
}
在这里,我们可以首先为我们的插件创建一个默认配置对象,如果提供了配置回调,我们将使用我们的配置调用它,给插件的用户更改它的机会。然后我们可以将配置对象注册为OurPluginConfig
类的单个实例。然后我们可以想象,our-plugin
公开的服务将依赖于这个OurPluginConfig
,因此当它们被容器实例化时,它们将注入配置对象。
注册全球资源
使用这个configure
功能,任何插件都可以注册自己的服务,甚至可以更改或覆盖其他插件声明的服务。它还可以为模板引擎注册资源:
export function configure(aurelia) {
aurelia.globalResources('./my-component');
}
在这里,插件注册了一个名为my-component
的资源。这种资源可能是不同的东西;我们将在接下来的章节中介绍模板资源。
特征
插件是构造和解耦代码的好方法。但是插件作为项目依赖项与外部库一起存在。例如,当使用 CLI 时,插件位于node_modules
目录中。在典型的项目中,代码不受版本控制。此代码不得作为项目的一部分进行修改。它实际上不属于这个项目;它由其他人管理,或者至少在不同的项目工作流中进行管理。
但是如果我们想这样构造自己的应用呢?使用插件机制使这相当复杂,因为我们需要考虑不同的插件作为单独的项目,并将它们单独打包,然后将它们安装到我们的应用上。每次需要对其中一个插件进行更改时,都需要对其进行单独更改,然后在应用中发布并更新其依赖项。尽管共享多个项目中使用的公共组件或行为有时很有用,但此工作流更为复杂,并在不必要时增加了开发过程的负担。
幸运的是,Aurelia 有一个解决方案,即功能。功能的工作原理与插件完全相同,但它位于应用内部。让我们看一个例子:
src/my-feature/index.js
export function configure(aurelia) {
// register some services or resources used by this feature
}
src/main.js
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging()
.feature('my-feature');
aurelia.start().then(() => aurelia.setRoot());
}
特性的工作方式与插件完全相同,只是我们使用feature
方法而不是plugin
方法来加载它们,并且它们位于src
目录中。与插件一样,功能的根目录也应该有一个index.js
文件,该文件应该导出一个configure
函数。与插件一样,它可以作为feature
方法的第二个参数传递一个配置回调,该回调将传递给feature
的configure
函数。
feature
方法需要到包含特性index.js
文件的目录的相对路径。例如,如果我的功能位于src/some/path/index.js
,则加载它的调用将是feature('some/path')
。
特性是组织代码的好方法。它们使得将一个巨大的单片应用分解为一组设计良好的模块变得更加容易。当然,这完全取决于开发团队的设计技能。在第 6 章设计关注点-组织和解耦中,我们将介绍一些模式、策略和方法来组织代码,以构建更好的 Aurelia 应用。
测井
Aurelia 配备了一个简单但功能强大的日志系统。它支持日志级别和可插入的附加器。
配置
要配置日志记录,必须至少添加一个日志追加器:
src/main.js
import * as LogManager from 'aurelia-logging';
import {ConsoleAppender} from 'aurelia-logging-console';
export function configure(aurelia) {
aurelia.use.standardConfiguration();
LogManager.addAppender(new ConsoleAppender());
LogManager.setLevel(LogManager.logLevel.info);
aurelia.start().then(() => aurelia.setRoot());
};
这里,首先将从aurelia-logging-console
库导入的ConsoleAppender
实例添加到日志模块中。这个附加器只是将日志输出到浏览器的控制台。
必须至少添加一个 appender 才能使日志记录正常工作。如果没有添加 appender,日志将被丢弃。
接下来,将日志级别设置为info
。这意味着所有具有较低级别的日志都不会被分派到 appender。Aurelia 支持从最低到最高的四个日志级别:debug
、info
、warn
和error
。例如,将最小日志级别设置为warn
意味着将忽略debug
和info
日志。此外,还提供了none
日志级别。设置后,它根本不执行过滤,而是将所有日志分派给 appender。
默认配置
上一个示例旨在显示完全定制的设置。相反,您可以在配置应用时使用developmentLogging
方法:
**src/main.js**
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging();
aurelia.start().then(() => aurelia.setRoot());
};
此默认配置安装ConsoleAppender
并将日志级别设置为none
。
追加者
Appender 必须实现一个简单的接口,每个日志级别有一个方法。例如,下面是 Aurelia 的ConsoleAppender
实现:
export class ConsoleAppender {
debug(logger, ...rest) {
console.debug(`DEBUG [${logger.id}]`, ...rest);
}
info(logger, ...rest) {
console.info(`INFO [${logger.id}]`, ...rest);
}
warn(logger, ...rest) {
console.warn(`WARN [${logger.id}]`, ...rest);
}
error(logger, ...rest) {
console.error(`ERROR [${logger.id}]`, ...rest);
}
}
如您所见,每个方法首先接收启动日志的记录器,然后是传递给记录器的日志方法的参数。
写日志
要写入日志,首先需要获取日志记录器:
import {LogManager} from 'aurelia-framework';
const logger = LogManager.getLogger('my-logger');
getLogger
方法需要记录器的名称,并返回记录器实例。如果提供的名称不存在记录器,则会创建一个新的记录器。记录器是单例的,因此对于给定的名称总是返回相同的实例。
一旦有了 logger 实例,就可以调用它的四种日志记录方法之一:debug()
、info()
、warn()
或error()
。假设方法的日志级别等于或大于配置的最小日志级别,这些方法中的每一个都将在所有 appender 上向相应级别的方法发送一个调用。否则,将不调用 appender,并丢弃日志。
记录器方法可以传递任意数量的参数,并且这些参数将被分派到附加器。例如,在记录器上调用error('A message', 12)
时,调用将作为appender.error
(logger, 'A message', 12)
委托给附件方。
默认情况下,所有记录器都配置了全局日志级别。但是,记录器也有一个setLevel
方法,允许为单个记录器设置不同的日志级别:
logger.setLevel(LogManager.logLevel.warn);
路由
除了非常简单的情况外,典型的单页应用由多个视图组成。大多数情况下,此类应用具有固定的全局布局,包括显示当前视图的可变区域和允许用户从一个视图导航到另一个视图的菜单。在 Aurelia 中,路由器插件支持这些功能。
配置路由器
要启用路由,请确保您的应用在默认情况下与基于 CLI 的项目一样依赖于aurelia-router
和aurelia-templating-router
库。然后在main.js
文件的configure
功能中加载路由器插件,加载整个standardConfiguration()
,包括路由器,或者单独加载router()
。有关如何在应用configure
功能中加载插件的更多信息,请参见第 1 章、入门。
申报航线
我们将首先向根组件添加一个configureRouter
方法。当 Aurelia 在组件上检测到这个回调方法时,它会在组件初始化周期中调用它。此方法接收两个参数:路由器配置对象和路由器本身:
src/app.js
export class App {
configureRouter(config, router) {
this.router = router;
config.title = 'Learning Aurelia';
config.map([
{ route: ['', 'contacts'], name: 'contacts', moduleId: 'contact-list', nav: true, title: 'Contacts' },
{ route: 'contacts/:id', name: 'contact-details', moduleId: 'contact-details' },
]);
}
}
在configureRouter
方法中,我们首先将路由器分配给一个实例变量。这很重要,因为根组件的视图需要访问路由器才能呈现菜单和活动路由组件。
完成后,我们将设置全局标题。此值将显示在浏览器的标题栏中。
接下来,我们使用map
方法配置两条路由。路由配置基本上是 URL 路径模式和组件之间的映射,URL 路径模式匹配后会激活路由,组件在激活路由时会显示。它还包含其他属性。让我们分解一个路由配置:
-
route
属性是 URL 路径模式。需要注意的是,模式忽略了路径的前导斜杠。有三种类型的模式:- 静态路由:模式与路径完全匹配。我们的第一个路由的第一个模式就是这样的一个例子:它匹配根路径(
/
),因为省略了前导斜杠,所以根路径匹配一个空字符串。这使其成为默认路线。 -
Parameterized routes: The pattern matches the path exactly, and the parts of the path matching the placeholders, prefixed by a colon (
:
), are parsed as route parameters. The value of those parameters are made available to the route component as part of the screen activation life cycle. The pattern of our second route is an example of this: it matches paths starting with/contacts/
, followed by a second part interpreted as the contact'sid
.注
此外,还可以通过在 route 参数后面添加问号使其成为可选参数。例如,
contacts/:id?/details
模式将由/contacts/12/details
和/contacts/details
匹配。当路径中省略该参数时,传递给路由组件的相应参数为undefined
。 -
通配符路由:模式匹配路径的开头,路径的其余部分被视为单个参数,其值作为屏幕激活生命周期的一部分提供给路由组件。例如,
my-route*param
模式将匹配以/my-route
开始的任何路径,param
将是一个参数,其值为匹配路径的其余部分。 name
属性唯一标识路由。稍后我们将看到如何使用它生成路由的 URL。moduleId
属性是路由组件的路径。- 当设置为
true
值时,nav
属性告诉路由器将此路由包括在其导航模型中,该模型用于自动构建应用的导航菜单。此外,如果nav
是一个数字,路由器将使用它对导航菜单中的项目进行排序。 - 当此管线处于活动状态时,
title
属性将显示在浏览器的标题栏中,除非组件覆盖它。如果nav
为true
,则也用作路线菜单项的文本。 settings
属性是可选的,可以包含可由激活组件或管道步骤使用的任意数据,我们将在本章后面看到。
- 静态路由:模式与路径完全匹配。我们的第一个路由的第一个模式就是这样的一个例子:它匹配根路径(
重定向路由
路由可以声明redirect
属性,而不是moduleId
。当这样的路由被激活时,路由器将执行到该属性值表示的路径的内部重定向。这允许多模式技术的替代方法声明默认路由,如我们的第一条路由所示。相反,我们可以宣布以下路线:
config.map([
{ route: '', redirect: 'contacts' },
{ route: 'contacts', name: 'contacts', moduleId: 'contact-list', nav: true, title: 'Contacts' },
{ route: 'contacts/:id', name: 'contact-details', moduleId: 'contact-details' },
]);
此配置的主要区别在于,当访问/
时,浏览器地址栏中的 URL 将更改为/contacts
,因为路由器将执行重定向。
使用此模式时,nav
属性应仅在目标路由上设置true
。如果它设置在重定向路由而不是目标路由上,路由器将无法突出显示相应的菜单项,因为在目标路由依次被激活之前,路由在技术上仅被激活了一小会儿。最后,在重定向路由和目标路由上都设置true
将导致在菜单中呈现这两个路由,这有点无意义,因为它们都指向同一个位置。
如果nav
属性是false
,那么设置title
也是毫无意义的,因为路线永远不会保持激活状态足够长的时间以使标题可见。
然而,在重定向路由上设置name
可能有用。当预期将来会更改重定向时,可以使用重定向路由的name
而不是目标路由生成链接。这样,路由的redirect
属性是唯一需要更改的内容,依赖于此路由的每个链接都将随之更改。
导航策略
除了moduleId
和redirect
属性之外,路由还可以具有navigationStrategy
属性。它的值必须是一个由路由器调用并传递一个NavigationInstruction
实例的函数。然后可以动态配置此对象。例如,我们的最后一条路线可以如下配置:
{
route: 'contacts/:id', name: 'contact-details',
navigationStrategy: instruction => {
instruction.config.moduleId = 'contact-details';
}
}
最后,这条路线做了和以前一样的事情。但对于需要比moduleId
和redirect
更大灵活性的场景,此替代方案会变得很方便,因为NavigationInstruction
实例包含以下属性:
config
:被导航到的路由的配置对象fragment
:触发导航的 URL 路径params
:包含从路由模式提取的每个参数的属性的对象parentInstruction
:父路由器的指令,如果该路由器是子路由器plan
:路由器内部构建并使用的导航计划,用于执行导航previousInstruction
:当前指令将在路由器中替换的导航指令queryParams
:包含从查询字符串解析的值的对象queryString
:原始查询字符串viewPortInstructions
:路由器内部构建和使用的用于执行导航的视口指令
展示我们的应用
路由器根据其路由配置生成导航模型,可用于自动生成导航菜单。因此,在添加新路由时,我们不必同时更改路由的配置和菜单视图。
因为我们根组件的视图模型是负责声明路由的,所以它的视图是全局布局并呈现导航菜单才有意义。让我们使用此导航模型并创建根组件的视图:
src/app.html
<template>
<require from="app.css"></require>
<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse"
data-target="#skeleton-navigation-navbar-collapse">
<span class="sr-only">Toggle Navigation</span>
</button>
<a class="navbar-brand" href="#">
<i class="fa fa-home"></i>
<span>${router.title}</span>
</a>
</div>
<div class="collapse navbar-collapse" id="skeleton-navigation-navbar-collapse">
<ul class="nav navbar-nav">
<li repeat.for="row of router.navigation" class="${row.isActive ? 'active' : ''}">
<a data-toggle="collapse" data-target="#skeleton-navigation-navbar-collapse.in" href.bind="row.href">
${row.title}
</a>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="loader" if.bind="router.isNavigating">
<i class="fa fa-spinner fa-spin fa-2x"></i>
</li>
</ul>
</div>
</nav>
<div class="page-host">
<router-view></router-view>
</div>
</template>
此模板中的有趣部分将高亮显示。让我们检查一下。
首先要注意的是,我们需要一个名为app.css
的文件,稍后我们将编写该文件。此文件将设置我们的应用组件的样式。
接下来,视图使用根组件视图模型的configureRouter
方法中定义的router
属性。我们首先在带有nav-brand
类的a
标记中看到它,其中字符串插值指令呈现文档标题。
然后,我们在li
标记上找到一个repeat.for="row of router.navigation"
属性。此绑定指令为router.navigation
数组中的每个项重复li
标记。此navigation
属性包含路由器的导航模型,使用路由的 truthynav
属性构建。呈现每个li
标记时,包含当前导航模型项的row
变量在模板引擎的绑定上下文中可用。
li
标记还具有class="${row.isActive ? 'active' : ''}"
属性。此字符串插值指令使用当前导航模型项的isActive
属性。如果isActive
的计算结果为true
值,则为li
标记分配active
CSS 类。此属性由路由器管理,仅当导航模型项属于活动路由时才为true
。在此模板中,它用于高亮显示活动菜单项。
li
标记内的锚具有href.bind="row.href"
属性。此指令将标记的href
属性绑定到当前导航模型项的href
属性。此href
属性由路由器使用路由的路径模式构建。此外,在锚内,路线的title
被渲染。
在菜单的末尾,我们可以看到一个带有loader
CSS 类的li
标记。此元素包含一个微调器图标。它有一个if.bind="router.isNavigating"
属性,该属性将 DOM 中该元素的存在与路由器的isNavigating
属性的值绑定。这意味着,当路由器执行导航时,会在应用的右上角看到一个微调器图标。当没有导航发生时,图标不仅不可见,而且实际上由于if
属性,它甚至不存在于 DOM 中。
最后,router-view
元素充当路由器视口并显示活动路由组件。这是整个模板中唯一需要的部分。组件配置路由器时,其视图必须包含router-view
元素,否则将抛出错误。利用导航模型是可选的,菜单可以是静态的,也可以通过您可以想象的任何其他方式构建。显示标题也是可选的。利用isNavigating
指标绝对不是强制性的。但是,如果某个组件的视图不能显示活动路由组件,那么让该组件配置路由器是完全没有意义的。
此视图使用了一种您可能熟悉的结构,如果您曾经使用过引导。Bootstrap 是 Twitter 开发的 CSS 框架,我们将在应用中使用它。让我们安装它:
> npm install bootstrap --save
我们还需要将其加载到我们的应用中:
index.html
<!DOCTYPE html>
<html>
<head>
<title>Learning Aurelia</title>
<link href="node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<!-- Omitted snippet... -->
</html>
我们的app
组件在开始工作之前仍然缺少最后一块,app.css
文件。这是:
src/app.css
.page-host {
position: absolute;
left: 0;
right: 0;
top: 50px;
bottom: 0;
overflow-x: hidden;
overflow-y: auto;
}
试试看
此时,如果您运行我们的应用,您应该会在浏览器控制台中看到路由器错误。这是因为默认路由尝试加载contact-list
组件,但该组件尚不存在。
让我们创建它并将其保留为空:
src/contact-list.html
<template>
<h1>Contacts</h1>
</template>
src/contact-list.js
export class ContactList {}
现在,如果您再次尝试运行应用,您应该可以正确地看到应用加载,显示顶部菜单和空的contact-list
组件。
屏幕激活生命周期
当路由器检测到 URL 路径更改时,它将经历以下生命周期:
- 确定了目标路线。如果没有与新路径匹配的路由,将抛出错误,进程将在此停止。
- 主动路由组件有机会拒绝停用,在这种情况下,路由器恢复以前的 URL 并在此停止进程。
- 目标路由组件有机会拒绝激活,在这种情况下,路由器将恢复以前的 URL 并在此处停止该过程。
- 激活的路由组件已停用。
- 目标路由组件已激活。
- 视图被交换。
组件为了选择此生命周期,可以实现以下任何回调方法:
canActivate(params, routeConfig, navigationInstruction)
:在步骤#2 调用,以了解组件是否可以激活。可以返回boolean
值、boolean
值的Promise
值、导航命令或导航命令的Promise
。activate(params, routeConfig, navigationInstruction)
:当组件被激活时,在步骤 5 调用。可以选择返回一个Promise
。canDeactivate()
:在步骤#3 调用,以了解组件是否可以停用。可以返回boolean
值、boolean
值的Promise
值、导航命令或导航命令的Promise
。deactivate()
:当组件停用时,在步骤 4 调用。可以选择返回一个Promise
。
Promise
在整个生命周期内都得到支持。这意味着,当任何回调方法返回Promise
时,路由器将等待其解析,然后继续该过程。
此外,canActivate
和activate
都接收与导航上下文相关的参数:
params
对象将具有路由模式中每个已解析参数的属性,以及每个查询字符串值的属性。例如,我们的contact-details
组件将接收一个具有id
属性的params
对象。匹配路径中没有值的可选参数为undefined
。routeConfig
将是原始路由配置对象,具有额外的navModel
属性。此navModel
对象具有setTitle(title: string)
方法,组件可以使用该方法将文档标题更改为动态值,例如激活期间加载的数据。当我们在第 3 章显示数据中开始构建数据显示组件时,我们将看到更多这方面的内容。navigationInstruction
是路由器用来执行导航的NavigationInstruction
实例。
最后,canDeactivate
和canActivate
如果返回false
、Promise
解析为false
、导航命令或Promise
解析为导航命令,都可以取消导航。
导航命令
导航命令是具有navigate(router: Router)
方法的对象。当canDeactivate
或canActivate
返回导航命令时,路由器取消当前导航并将控制权委托给该命令。Aurelia 附带一个现成的导航命令:Redirect
。下面是一个如何使用它的示例:
src/contact-details.js
import {inject} from 'aurelia-framework';
import {Redirect} from 'aurelia-router';
import {ContactService} from 'app-services';
@inject(ContactService)
export class ContactDetails {
constructor(contactService) {
this.contactService = contactService;
}
canActivate(params) {
return this.contactService.getById(params.id)
.then(contact => { this.contact = contact; })
.catch(e => new Redirect('error'));
}
}
这里,在canActivate
回调方法中,ContactDetails
视图模型试图通过其id
加载联系人。如果getById
返回的Promise
被拒绝,则用户被重定向到error
路由。
处理未知路径
当路由器无法将 URL 路径与任何路由匹配时,它会抛出一个错误。但在引发此错误之前,它首先将导航指令委托给未知的路由处理程序(如果有)。可以使用mapUnknownRoutes
方法配置此处理程序,该方法可以将以下值之一作为参数:
- 要显示而不是引发错误的组件的路径。
- 路由配置对象,包含
moduleId
、redirect
或navigationStrategy
属性。路由器将把导航委托给该路由,而不是抛出错误。 - 函数接收
NavigationInstruction
实例并返回要显示的组件路径,而不是抛出错误。
让我们实现一个not-found
组件,当链接断开时,我们的应用将显示该组件:
src/not-found.html
<template>
<h1>Something is broken...</h1>
<p>The page cannot be found.</p>
</template>
src/not-found.js
export class NotFound {}
在根组件中,我们只需添加高亮显示的行:
src/app.js
export class App {
configureRouter(config, router) {
this.router = router;
config.title = 'Learning Aurelia';
config.map([ /* omitted for brevity */ ]);
config.mapUnknownRoutes('not-found');
}
}
当路由器无法将 URL 路径与现有路由匹配时,将显示我们的not-found
组件。
常规路由
mapUnknownRoutes
提供的另一个选项是使用路由约定,而不是一组静态定义的路由。如果您的所有路由在路径和moduleId
之间遵循相同的命名模式,我们可以想象这样的情况:
src/app.js
export class App {
configureRouter(config, router) {
this.router = router;
config.title = 'Learning Aurelia';
config.mapUnknownRoutes(instruction => getComponentForRoute(instruction.fragment));
}
}
这里,路由依赖于将由getComponentForRoute
函数实现的约定,该函数接收触发导航的 URL 路径,并返回必须显示的组件路径。
激活策略
当多个静态路由指向同一组件,并且在其中两个路由之间进行导航时,路由器只保留相同的组件实例。因此,不执行激活生命周期。此行为由激活策略决定。activationStrategy
枚举有两个值:
replace
:将当前路由替换为新路由,保持组件实例不变,不经过激活生命周期。这是默认行为。invokeLifecycle
:即使活性成分没有变化,也要经历激活生命周期。
有两种方法可以更改此行为:
- 在路由的配置对象中,您可以添加一个
activationStrategy
属性,指定激活此路由时应使用的策略。 - 在 route 组件的视图模型中,您可以添加一个
determineActivationStrategy
方法,该方法必须返回用于显示该组件的所有路由的策略。
子路由器
就像 DI 一样,容器可以有子容器并形成容器树;就像组件可以包含子组件并形成组件树一样,路由器也可以有子组件。这意味着路由组件的视图模型可以有自己的configureRouter
方法,其视图可以是router-view
元素。当遇到这样的组件时,路由器将为此子组件创建一个子路由器。此子路由器的路由模式将与父路由的模式相关。
这允许应用具有具有多个级别的导航树。在第 6 章、设计关注点-组织和解耦中讨论如何组织大型应用时,我们将看到如何利用此功能
管线
将路由器与每次发出导航请求时调用的某些逻辑连接起来可能很有用。例如,具有身份验证机制的应用可能需要将某些路由仅限于经过身份验证的用户。奥雷利亚路由器的管道正是为这种情况而设计的。
路由器支持四条管线:authorize
、preActivate
、preRender
和postRender
。这些管道在航行过程中的不同阶段调用。让我们看看它们发生在哪里:
- 调用当前路由组件的
canDeactivate
方法(如果存在)。 - 执行
authorize
管道。 - 如果存在,则调用目标路由组件的
canActivate
方法。 - 执行
preActivate
管道。 - 调用当前路由组件的
deactivate
方法(如果存在)。 - 如果存在,则调用目标路由组件的
activate
方法。 - 执行
preRender
管道。 - 视图在“路由器”视口中交换。
- 执行
postRender
管道。
管道由按顺序调用的步骤组成。管道步骤是具有run(instruction, next)
方法的类,其中instruction
是NavigationInstruction
实例,next
是Next
对象。
Next
对象是具有方法的函数。
当next()
被调用时,它告诉路由器管道继续进行下一步。next.cancel()
方法取消导航过程,并期望将导航命令或Error
对象作为参数传递。
两者都返回Promise
s。
让我们看一个例子:
src/app.js
import {AuthenticatedStep} from 'authenticated-step';
export class App {
configureRouter(config, router) {
config.title = 'Aurelia';
config.addPipelineStep('authorize', AuthenticatedStep);
config.map([
{ route: 'login', name: 'login', moduleId: 'login', title: 'Login' },
{ route: 'management', name: 'management', moduleId: 'management',
settings: { secured: true } },
]);
this.router = router;
}
}
这里需要注意的重要一点是,AuthenticatedStep
类被添加到authorize
管道中。管道步骤作为类而不是实例添加。这是因为路由器使用其 DI 容器来解析这些步骤的实例。这允许步骤具有依赖项,这些依赖项在执行之前被解析和注入。
第二件需要注意的事情是,management
路由有一个settings
对象,其secured
属性设置为true
。下面代码段中描述的管道步骤将使用它来标识需要限制为经过身份验证的用户的路由。
src/authenticated-step.js
import {inject} from 'aurelia-framework';
import {Redirect} from 'aurelia-router';
import {User} from 'user';
@inject(User)
export class AuthenticatedStep {
constructor(user) {
this.user = user;
}
run(instruction, next) {
let isRouteSecured = instruction.getAllInstructons().some(i => i.config.settings.secured);
if (isRouteSecured && !this.user.isAuthenticated) {
return next.cancel(new Redirect('login'));
}
return next();
}
}
这是实际的管道步骤。在这个例子中,我们可以想象我们的应用包含一个User
类,它公开了当前用户的信息。我们的管道依赖于此类的实例来了解当前用户是否经过身份验证。
run
方法首先检查指令中的任何路由是否配置为安全路由。这是通过检查所有导航指令来实现的,包括潜在父路由器的指令,并检查它们的配置settings
的 truthysecured
属性。
例如,当导航到前面代码段中定义的management
路由时,分配给isRouteSecured
的值将是true
。如果management
组件声明了子路由,并且其中一个路由发生了导航,则情况也是如此。在这种情况下,即使子路由未配置为secured
,由于父路由之一将是secured
,因此isRouteSecured
仍将是true
。
当目标路由或其父路由之一处于安全状态时,如果用户未通过身份验证,则取消导航,并将用户重定向到login
路由。否则,next
被调用,让路由器知道它可以继续进行导航过程。
事件
Aurelia 路由器提供了另一个扩展点。除了屏幕激活生命周期和管道之外,路由器还通过事件聚合器发布事件,事件聚合器是 Aurelia 的另一个核心库。
路由器事件的演示可在samples/chapter-2/router-events
中找到。让我们看看这些事件:
router:navigation:processing
:每次路由器开始处理导航指令时都会触发此事件。router:navigation:error
:导航指令触发错误时触发此事件。router:navigation:canceled
:当通过当前或目标路由组件的屏幕激活生命周期回调方法之一或管道步骤取消导航指令时,会触发此事件。router:navigation:success
:导航指令成功时触发此事件。router:navigation:complete
:导航指令处理完成后,无论失败、取消或成功,都会触发此事件。
所有这些事件的有效负载都包含作为instruction
属性存储的NavigationInstruction
实例。此外,除router:navigation:processing
之外,所有其他事件的有效负载都将PipelineResult
作为result
属性。例如,在处理error
事件时,可以使用result
的output
属性访问抛出的Error
对象。
我们将在第 6 章、设计关注点-组织和解耦中了解事件聚合器的工作原理
多个视口
在前面的所有示例中,router-view
元素从来没有任何属性。它实际上可以有一个name
属性。省略此属性时,元素在路由器上声明的视口命名为default
。你能理解这里的含意吗?
如果您的答案是路由器支持多个视口,那么您猜对了。当然,这也意味着必须为视图中声明的每个视口配置每个管线。让我们看看它是如何工作的:
注
以下代码片段摘自samples/chapter-2/router-multiple-viewports
。
src/app.html
<template>
<require from="nav-bar.html"></require>
<require from="bootstrap/css/bootstrap.css"></require>
<nav-bar router.bind="router"></nav-bar>
<div class="page-host">
<router-view name="header"></router-view>
<router-view name="content"></router-view>
</div>
</template>
在这里,组件视图中需要注意的有趣的事情是有两个router-view
元素,具有不同的name
属性。该组件的路由器将以两个视口结束:一个名为header
,另一个名为content
:
src/app.js
export class App {
configureRouter(config, router) {
config.title = 'Learning Aurelia';
config.map([
{
route: ['', 'page-1'], name: 'page-1', nav: true, title: 'Page 1',
viewPorts: {
header: { moduleId: 'header' },
content: { moduleId: 'page-1' }
}
},
{
route: 'page-2', name: 'page-2', nav: true, title: 'Page 2',
viewPorts: {
header: { moduleId: 'header' },
content: { moduleId: 'page-2' }
}
},
]);
this.router = router;
}
}
在视图模型的configureRouter
回调方法中,两条路由都为header
和content``viewPorts
配置了特定的moduleId
。
如果不是每个路由器的视口在激活时都被路由配置占用,路由器将抛出一个错误。无论路由是静态配置的viewPorts
属性为每个视口定义了moduleId
,还是viewPorts
属性是由navigationStrategy
动态配置的。在前面的示例中,page-2
路线可以替换为:
{
route: 'page-2', name: 'page-2', nav: true, title: 'Page 2',
navigationStrategy: instruction => {
instruction.config.viewPorts = {
header: { moduleId: 'header' },
content: { moduleId: 'page-2' }
};
}
}
此路径的效果与上一个示例中的效果相同。这里唯一的区别是每次激活管线时都会动态配置视口。
当然,重定向路由不受视口的影响,因为它们不渲染任何内容。
推送状态与散列更改
路由器通过响应 URL 更改来工作。在较旧的浏览器中,只有散列符号(#)后面的 URL 部分(称为散列)可以更改,而不会触发页面重新加载。因此,在这些浏览器上运行的路由器只能更改哈希部分,并侦听哈希部分中的更改。
HTML5 引入了一个新的历史 API,可以操纵浏览器历史。这允许运行在现代浏览器上的 JavaScript 路由器直接操作其当前 URL 和浏览历史,并监视对其当前 URL 的更改。此 API 使路由器能够使用完整的 URL,并允许使用服务器呈现和渐进增强的同构应用。这些技术可以让更广泛的客户端访问应用的内容,还可以提高应用的 SEO,因为 Google 不推荐使用 AJAX 内容加载的基于哈希的应用(请参见https://googlewebmastercentral.blogspot.com/2015/10/deprecating-our-ajax-crawling-scheme.html )。
注
当应用可以在客户端和服务器上执行时,它是同构的。通常,在服务器端执行同构应用,以呈现基于文本的 HTML 表示,然后将其返回给客户端;例如,搜索引擎爬虫。在客户端执行时,通常会使用运行时事件处理程序、数据绑定和实际行为对其进行增强,以便用户可以与应用交互。
Aurelia 的路由器插件可以使用这两种策略中的任何一种。默认情况下,它被配置为使用基于散列的策略,因为推送状态要求相应地配置服务器。此外,基于散列的策略支持与 HTML5 不完全兼容的旧浏览器。
但是,如果不需要支持较旧的浏览器,或者需要服务器端渲染,并且应用可能会朝着同构的方向发展,那么可以将路由器配置为使用历史 API。
注
以下代码片段摘自samples/chapter-2/router-push-state
。
首先,在index.html
文件的头部部分,必须添加<base href="/">
标记。此元素指示浏览器,/
是页面中所有相关 URL 的基础。
接下来,在根组件的视图模型中,路由器的配置必须不同:
src/app.js
export class App {
configureRouter(config, router) {
this.router = router;
config.title = 'Aurelia';
config.options.pushState = true;
config.options.hashChange = false;
config.map([ /* omitted for brevity */ ]);
}
}
此外,为了在用户使用根 URL 以外的 URL 访问应用时显示正确的路由,服务器需要输出index.html
页面,而不是 404 响应未知路径的请求。这样,当用户访问应用路由时,服务器将使用索引页进行响应,然后应用将启动,路由器将处理该路由并显示正确的视图。隐式地说,这意味着应用中的路由与服务器端资源(如 CSS、图像、字体、JS、HTML 或必须由索引页或应用从服务器加载的任何文件)之间不得存在命名冲突。
生成 URL
路由器能够对 URL 更改做出反应并相应地更新其视口是一回事。但是允许它导航的链接呢?如果我们硬编码 URL,那么路由路径模式中的任何更改都不仅需要更改路由配置,还需要检查此 URL 用于导航的每个位置,可能在 JS 代码或视图中,并对其进行更改。
幸运的是,路由器还能够生成 URL。生成路由路径有两个要求:
- 路由配置必须具有唯一的
name
属性 - 如果路由具有参数化或通配符模式,则在生成 URL 时必须提供包含每个参数值的参数对象。
代码中的
要在 JS 代码中生成 URL 路径,首先必须拥有路由器的实例,通常是将其注入到需要的类中。然后,可以调用以下方法:
router.generate(name: string, params?: any, options?: any): string
必须使用路由名称、参数对象(如果路由有)和可选选项对象调用此方法,并将返回结果 URL。目前唯一支持的选项是absolute
,当设置为true
时,强制路由器返回绝对 URL 而不是相对 URL。
例如,对于路径模式为contacts/:id
的名为contact-details
的路由,为id
为 12 的联系人生成 URL 的调用将是:
let url = router.generate('contact-details', { id: 12 });
对于绝对 URL:
let url = router.generate('contact-details', { id: 12 }, { absolute: true });
在视图中
如果我们需要在视图中呈现到某个路由的链接,该怎么办?我想您可以看到如何将路由器注入视图模型,调用generate
方法,并将锚的href
属性绑定到结果。这充其量也会很快变得乏味。
aurelia-templating-router
库附带了一个route-href
属性,这使得这更容易。例如,为我们名为contact-details
的路线提供指向id 12
联系人的链接的模板片段是:
<a route-href="route: contact-details; params.bind: { id: 12 }">
Contact #12</a>
ID 可能不会硬编码,而是存储在对象中:
<a route-href="route: contact-details; params.bind: { id: contact.id }">
${contact.name}</a>
默认情况下,route-href
属性将结果 URL 分配给它所在元素的href
属性,但它支持attribute
属性,该属性可用于指定必须设置 URL 的属性的名称:
<q route-href="route: quote; attribute: cite">...</q>
这里,quote
路由的 URL 将分配给q
元素的cite
属性。
导航
路由器提供了从 JS 代码执行导航的便捷方法:
navigate(fragment: string, options?: any): boolean
:导航到新位置,路径为fragment
。如果导航成功返回true
,否则返回false
。支持两种options
:replace: boolean
:如果设置为true
,则新 URL 将替换历史中的当前位置,而不是附加到历史中。trigger: boolean
:如果设置为false
,则不会触发 Aurelia 的路由器。这意味着,如果 URL 是相对的,它将在浏览器的地址栏中更改,但不会出现实际的导航。
navigateToRoute(name: string, params?: any, options?: any): boolean
:方便地包装对generate
和navigate
的调用。navigateBack(): void
:导航回历史上的上一个位置。
总结
依赖注入是 Aurelia 的核心,了解它的工作原理很重要。如果您不熟悉本章之前的概念,那么可能需要立即处理很多内容;但请放心,在本书的其余部分中,我们将继续大量使用这些功能,这将帮助您更加熟悉它。
插件、特性和路由也是如此。我们将在本书后面继续挖掘这些主题,特别是在第 6 章、设计关注点——组织和解耦中,我们将讨论构建应用的各种方法。
在到达那里之前,我们还有很多地方要覆盖。在下一章中,我们将讨论数据绑定和模板的基础知识,并将添加组件以在联系人管理应用中获取和显示数据。
版权属于:月萌API www.moonapi.com,转载请注明出处