路由与导航

The Angular Router enables navigation from one view to the next as users perform application tasks.

在用户使用应用程序时,Angular的路由器能让用户从一个视图导航到另一个视图。

We cover the router's primary features in this chapter, illustrating them through the evolution of a small application that we can run live.

本章覆盖了该路由器的主要特性。我们通过一个小型应用的成长演进来讲解它。参见在线例子

pop out the window

To see the URL changes in the browser address bar, pop out the preview window by clicking the blue 'X' button in the upper right corner.

请点击右上角的蓝色'X'按钮以弹出预览窗口,这样可以看到浏览器地址栏的变化情况。

Overview

概览

The browser is a familiar model of application navigation. We enter a URL in the address bar and the browser navigates to a corresponding page. We click links on the page and the browser navigates to a new page. We click the browser's back and forward buttons and the browser navigates backward and forward through the history of pages we've seen.

浏览器是一个熟悉的应用导航操作模型。 如果在地址栏中输入一个URL,浏览器就会导航到相应的页面。 如果你在页面中点击链接,浏览器就会导航到一个新的页面。 如果你点击浏览器上的前进和后退按钮,浏览器就会根据你看过的页面历史向前或向后进行导航。

The Angular Router ("the router") borrows from this model. It can interpret a browser URL as an instruction to navigate to a client-generated view and pass optional parameters along to the supporting view component to help it decide what specific content to present. We can bind the router to links on a page and it will navigate to the appropriate application view when the user clicks a link. We can navigate imperatively when the user clicks a button, selects from a drop box, or in response to some other stimulus from any source. And the router logs activity in the browser's history journal so the back and forward buttons work as well.

Angular的路由器(以下简称“路由器”)借鉴了这个模型。它把浏览器中的URL看做一个操作指南, 据此导航到一个由客户端生成的视图,并可以把参数传给支撑视图的相应组件,帮它决定具体该展现哪些内容。 我们可以为页面中的链接绑定一个路由,这样,当用户点击链接时,就会导航到应用中相应的视图。 当用户点击按钮、从下拉框中选取,或响应来自任何地方的事件时,我们也可以在代码控制下进行导航。 路由器还在浏览器的历史日志中记录下这些活动,这样浏览器的前进和后退按钮也能照常工作。

We'll learn many router details in this chapter which covers

在本章中,我们将会学到关于路由器的更多细节知识:

We proceed in phases marked by milestones building from a simple two-pager with placeholder views up to a modular, multi-view design with child routes.

接下来,我们分为几个里程碑阶段,把一个简单的、只有占位(placeholder)视图的双页范例,升级成一个模块化的、带有子路由的多视图设计。

But first, an overview of router basics.

不过,还是先来做一个路由器基础知识的概览。

The Basics

基础知识

Let's begin with a few core concepts of the Router. Then we can explore the details through a sequence of examples.

我们从路由器的少量核心概念开始,然后在通过一系列的例子来了解它们的各种细节。

<base href>

<base href>

Most routing applications should add a <base> element to the index.html as the first child in the <head> tag to tell the router how to compose navigation URLs.

大多数带路由的应用都要在index.html<head>标签下先添加一个<base>元素,来告诉路由器该如何合成导航用的URL。

If the app folder is the application root, as it is for our sample application, set the href value exactly as shown here.

如果app文件夹是该应用的根目录(就像我们的范例应用一样),那就把href的值设置为下面这样:

index.html (base-href)

<base href="/">

Router imports

从路由库中导入

The Angular Router is an optional service that presents a particular component view for a given URL. It is not part of the Angular core. It is in its own library package, @angular/router. We import what we need from it as we would from any other Angular package.

Angular的路由器是一个可选的服务,它用来呈现指定的URL所对应的视图。 它并不是Angular 2核心库的一部分,而是在它自己的@angular/router包中。 像其它Angular包一样,我们可以从它导入所需的一切。

app/app.module.ts (import)

import { RouterModule, Routes } from '@angular/router';

We cover other options in the details below.

我们将会在后面详细讲解其它选项。

Configuration

配置

The application will have one router. When the browser's URL changes, the router looks for a corresponding Route from which it can determine the component to display.

该应用将有一个router(路由器)。当浏览器的地址变化时,该路由器会查找相应的Route(路由定义,简称路由),并据此确定所要显示的组件。

A router has no routes until we configure it. We bootstrap our application with an array of routes that we'll provide to our RouterModule.forRoot function.

需要先配置路由器,才会有路由信息。 首选方案是用带有“路由数组”的provideRouter工厂函数([provideRouter(routes)])来启动此应用。

In the following example, we configure our application with four route definitions. The configured RouterModule is add to the AppModule's imports array.

在下面的例子中,我们用四个路由定义配置了本应用的路由器。这个配置的RouterModule被添加到AppModuleimports数组中。

app/app.module.ts (excerpt)

const appRoutes: Routes = [ { path: 'hero/:id', component: HeroDetailComponent }, { path: 'crisis-center', component: CrisisListComponent }, { path: 'heroes', component: HeroListComponent, data: { title: 'Heroes List' } }, { path: '', component: HomeComponent }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ BrowserModule, FormsModule, RouterModule.forRoot(appRoutes) ], declarations: [ AppComponent, HeroListComponent, HeroDetailComponent, CrisisListComponent, PageNotFoundComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }

The RouterModule is provided an array of routes that describe how to navigate. Each Route maps a URL path to a component.

RouterConfig是一个路由数组,它会决定如何导航。 每个Route会把一个URL的path映射到一个组件。

There are no leading slashes in our path. The router parses and builds the URL for us, allowing us to use relative and absolute paths when navigating between application views.

path不能用斜线/开头。路由器会为我们解析和生成URL,以便在多个视图间导航时,可以自由使用相对路径和绝对路径。

The :id in the first route is a token for a route parameter. In a URL such as /hero/42, "42" is the value of the id parameter. The corresponding HeroDetailComponent will use that value to find and present the hero whose id is 42. We'll learn more about route parameters later in this chapter.

第一个路由中的:id是一个路由参数的令牌(Token)。比如/hero/42这个URL中,“42”就是id参数的值。 此URL对应的HeroDetailComponent组件将据此查找和展现id为42的英雄。 在本章中稍后的部分,我们将会学习关于路由参数的更多知识。

The data property in the third route is a place to store arbitrary data associated with each specific route. This data is accessible within each activated route and can be used to store items such as page titles, breadcrumb text and other read-only data. We'll use the resolve guard to retrieve additional data later in the chapter.

第三个路由中的data属性用来存放于每个具体路由有关的任意信息。该数据可以被任何一个激活路由访问,并能用来保存诸如 页标题、面包屑以及其它只读数据。本章稍后的部分,我们将使用resolve守卫来获取更多数据。

The empty path in the fourth route matches as the default path for each level of routing. It also allows for adding routes without extending the URL path.

第四个路由中的empty path匹配各级路由的默认路径。 它还支持在不扩展URL路径的前提下添加路由。

The ** in the last route denotes a wildcard path for our route. The router will match this route if the URL requested doesn't match any paths for routes defined in our configuration. This is useful for displaying a 404 page or redirecting to another route.

最后一个路由中的**代表该路由是一个通配符路径。如果当前URL无法匹配上我们配置过的任何一个路由中的路径,路由器就会匹配上这一个。当需要显示404页面或者重定向到其它路由时,该特性非常有用。

The order of the routes in the configuration matters and this is by design. The router uses a first-match wins strategy when matching routes, so more specific routes should be placed above less specific routes. In our configuration above, the routes with a static path are listed first, followed by an empty path route, that matches as the default route. The wildcard route is listed last as it's the most generic route and should be matched only if no other routes are matched first.

这些路由的定义顺序是故意如此设计的。路由器使用先匹配者优先的策略来匹配路由,所以,具体路由应该放在通用路由的前面。在上面的配置中,带静态路径的路由被放在了前面,后面是空路径路由,因此它会作为默认路由。而通配符路由被放在最后面,这是因为它是最通用的路由,应该只在前面找不到其它能匹配的路由时才匹配它。

Router Outlet

路由插座

Given this configuration, when the browser URL for this application becomes /heroes, the router matches that URL to the Route path /heroes and displays the HeroListComponent after a RouterOutlet that we've placed in the host view's HTML.

有了这份配置,当本应用在浏览器中的URL变为/heroes时,路由器就会匹配到pathheroesRoute,并在宿主视图中的RouterOutlet之后显示HeroListComponent组件。

<router-outlet></router-outlet> <!-- Routed views go here -->

路由器链接

Now we have routes configured and a place to render them, but how do we navigate? The URL could arrive directly from the browser address bar. But most of the time we navigate as a result of some user action such as the click of an anchor tag.

现在,我们已经有了配置好的一些路由,还找到了渲染它们的地方,但又该如何导航到它呢?固然,从浏览器的地址栏直接输入URL也能做到,但是大多数情况下,导航是某些用户操作的结果,比如点击一个A标签。

We add a RouterLink directive to the anchor tag. Since we know our link doesn't contain any dynamic information, we can use a one-time binding to our route path.

我们往A标签上添加了RouterLink指令。由于我们知道链接中不包含任何动态信息,因此我们使用一次性绑定的方式把它绑定到我们路由中的path值。

If our RouterLink needed to be more dynamic we could bind to a template expression that returns an array of route link parameters (the link parameters array). The router ultimately resolves that array into a URL and a component view.

如果RouterLink需要动态信息,我们就可以把它绑定到一个能返回路由链接数组(链接参数数组)的模板表达式上。 路由器最终会把此数组解析成一个URL和一个组件视图。

We also add a RouterLinkActive directive to each anchor tag to add or remove CSS classes to the element when the associated RouterLink becomes active. The directive can be added directly on the element or on its parent element.

我们还往每个A标签上添加了一个RouterLinkActive指令,用于在相关的RouterLink被激活时为所在元素添加或移除CSS类。 该指令可以直接添加到该元素上,也可以添加到其父元素上。

We see such bindings in the following AppComponent template:

我们会在下面的AppComponent模板中看到类似这样的绑定:

template: ` <h1>Angular Router</h1> <nav> <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> </nav> <router-outlet></router-outlet> `

We're adding two anchor tags with RouterLink and RouterLinkActive directives. We bind each RouterLink to a string containing the path of a route. '/crisis-center' and '/heroes' are the paths of the Routes we configured above.

我们用RouterLink指令添加了两个带RouterLinkRouterLinkActive指令的A标签。每个RouterLink都绑定到了一个包含路由路径的字符串上。 '/crisis-center'和'/heroes'都是我们前面配置过的Routes中的路径。

We'll learn to write link expressions — and why they are arrays — later in the chapter.

在本章的后面,我们还将学到如何写链接表达式,以及了解它们为什么是数组。

We define active as the CSS class we want toggled to each RouterLink when they become the current route using the RouterLinkActive directive. We could add multiple classes to the RouterLink if we so desired.

利用RouterLinkActive指令,我们把active作为当路由被激活时为RouterLink切换的CSS类。 必要时,还可以为RouterLink添加多个类。

Router State

路由器状态

After the end of each successful navigation lifecycle, the router builds a tree of ActivatedRoute objects that make up the current state of the router. We can access the current RouterState from anywhere in our application using the Router service and the routerState property.

在导航时的每个生命周期成功完成时,路由器会构建出一个ActivatedRoute组成的树,它表示路由器的当前状态。 我们可以在应用中的任何地方用Router服务及其routerState属性来访问当前的RouterState值。

Each ActivatedRoute in the RouterState provides methods to traverse up and down the route tree to get information we may need from parent, child and sibling routes.

路由器状态为我们提供了从任意激活路由开始向上或向下遍历路由树的一种方式,以获得关于父、子、兄弟路由的信息。

Let's summarize

总结一下

The application is provided with a configured router. The component has a RouterOutlet where it can display views produced by the router. It has RouterLinks that users can click to navigate via the router.

为应用提供了一个配置过的路由器。 组件中有一个RouterOutlet,它能显示路由器所生成的视图。 它还有一些RouterLink,用户可以点击它们,来通过路由器进行导航。

Here are the key Router terms and their meanings:

下面是一些路由器中的关键词汇及其含义:

Router Part

路由器部件

Meaning

含义

Router

Router(路由器)

Displays the application component for the active URL. Manages navigation from one component to the next.

为激活的URL显示应用组件。管理从一个组件到另一个组件的导航

RouterModule

RouterModule(路由器模块)

A separate Angular module that provides the necessary service providers and directives for navigating through application views.

一个独立的Angular模块,用于提供所需的服务提供商,以及用来在应用视图之间进行导航的指令。

Routes

Routes(路由数组)

Defines an array of Routes, each mapping a URL path to a component.

定义了一个路由数组,每一个都会把一个URL路径映射到一个组件。

Route

Route(路由)

Defines how the router should navigate to a component based on a URL pattern. Most routes consist of a path and a component type.

定义路由器该如何根据URL模式(pattern)来导航到组件。大多数路由都由路径和组件类构成。

RouterOutlet

RouterOutlet(路由插座)

The directive (<router-outlet>) that marks where the router should display a view.

该指令(<router-outlet>)用来标记出路由器该在哪里显示视图。

RouterLink

RouterLink(路由链接)

The directive for binding a clickable HTML element to a route. Clicking an anchor tag with a routerLink directive that is bound to a Link Parameters Array triggers a navigation.

该指令用来把一个可点击的HTML元素绑定到路由。 点击带有绑定到字符串链接参数数组routerLink指令的A标签就会触发一次导航。

RouterLinkActive

RouterLinkActive(活动路由链接)

The directive for adding/removing classes from an HTML element when an associated routerLink contained on or inside the element becomes active/inactive.

当HTML元素上或元素内的routerLink变为激活或非激活状态时,该指令为这个HTML元素添加或移除CSS类。

ActivatedRoute

ActivatedRoute(激活的路由)

A service that is provided to each route component that contains route specific information such as route parameters, static data, resolve data, global query params and the global fragment.

为每个路由组件提供提供的一个服务,它包含特定于路由的信息,比如路由参数、静态数据、解析数据、全局查询参数和全局碎片(fragment)。

RouterState

RouterState(路由器状态)

The current state of the router including a tree of the currently activated routes in our application along convenience methods for traversing the route tree.

路由器的当前状态包含了一棵由程序中激活的路由构成的树。它包含一些用于遍历路由树的快捷方法。

Link Parameters Array

链接参数数组

An array that the router interprets into a routing instruction. We can bind a RouterLink to that array or pass the array as an argument to the Router.navigate method.

这个数组会被路由器解释成一个路由操作指南。我们可以把一个RouterLink绑定到该数组,或者把它作为参数传给Router.navigate方法。

Routing Component

路由组件

An Angular component with a RouterOutlet that displays views based on router navigations.

一个带有RouterOutlet的Angular组件,它根据路由器的导航来显示相应的视图。

We've barely touched the surface of the router and its capabilities.

我们已经对路由器及其能力有了肤浅的了解。

The following detail sections describe a sample routing application as it evolves over a sequence of milestones. We strongly recommend taking the time to read and understand this story.

下面的详情区描述了一个带路由的范例应用,它经过一系列里程碑一步步演进。我们强烈建议你花点时间阅读并理解这个过程。

The Sample Application

范例应用

We have an application in mind as we move from milestone to milestone.

从一个里程碑前进到另一个里程碑,我们总是有一个应用程序在心中。

While we make incremental progress toward the ultimate sample application, this chapter is not a tutorial. We discuss code and design decisions pertinent to routing and application design. We gloss over everything in between.

虽然我们会渐进式的前进到最终的范例应用,但本章并不是一个教程。 我们讨论路由和应用设计有关的代码和设计决策,并在这期间,处理遇到的所有问题。

The full source is available in the .

完整代码可以在在线例子中找到。

Our client is the Hero Employment Agency. Heroes need work and The Agency finds Crises for them to solve.

我们的客户是“英雄管理局”。 英雄们需要找工作,而“英雄管理局”为它们寻找待解决的危机。

The application has three main feature areas:

本应用分为三个主要的特性区:

  1. A Crisis Center where we maintain the list of crises for assignment to heroes.

  2. 一个危机中心区,它维护一个危机列表,用来分派给英雄。

  3. A Heroes area where we maintain the list of heroes employed by The Agency.

  4. 一个英雄区,它维护由英雄管理局雇佣的英雄列表。

  5. An Admin area where we manage the list of crises and heroes displayed.

  6. 一个管理区,用来维护该中心危机列表和雇佣英雄的列表。

Run the . It opens in the Crisis Center. We'll come back to that.

运行在线例子。它打开了危机中心,一会儿我们就会回到那里。

Click the Heroes link. We're presented with a list of Heroes.

点击英雄链接,我们就会展现出一个英雄列表。

Hero List

We select one and the application takes us to a hero editing screen.

选择其中之一,该应用就会把我们带到此英雄的编辑页面。

Crisis Center Detail

Our changes take effect immediately. We click the "Back" button and the app returns us to the Heroes list.

修改会立即见效。我们再点击“后退”按钮,该应用又把我们带回了英雄列表页。

We could have clicked the browser's back button instead. That would have returned us to the Heroes List as well. Angular app navigation updates the browser history as normal web navigation does.

另外我们也可以点击浏览器本身的后退按钮,这样也同样会回到英雄列表页。 在Angular应用中导航也会和标准的Web导航一样更新浏览器中的历史。

Now click the Crisis Center link. We go to the Crisis Center and its list of ongoing crises.

现在,点击危机中心链接,我们就会前往危机中心页,那里列出了待处理的危机。

Crisis Center List

We select one and the application takes us to a crisis editing screen.

选择其中之一,该应用就会把我们带到此危机的编辑页面。

Crisis Center Detail

This is a bit different from the Hero Detail. Hero Detail saves the changes as we type. In Crisis Detail our changes are temporary until we either save or discard them by pressing the "Save" or "Cancel" buttons. Both buttons navigate back to the Crisis Center and its list of crises.

这和英雄详情页略有不同。英雄详情会立即保存我们所做的更改。 而危机详情页中,我们的更改都是临时的 —— 除非按“保存”按钮保存它们,或者按“取消”按钮放弃它们。 这两个按钮都会导航回危机中心,显示危机列表。

Suppose we click a crisis, make a change, but do not click either button. Maybe we click the browser back button instead. Maybe we click the "Heroes" link.

假如我们点击一个危机、做了修改,但是没有点击任何按钮,可能点击了浏览器的后退按钮,也可能点击了“英雄”链接。

Do either. Up pops a dialog box.

无论哪种情况,我们都应该弹出一个对话框。

Confirm Dialog

We can say "OK" and lose our changes or click "Cancel" and continue editing.

我们可以回答“确定”以放弃这些更改,或者回答“取消”来继续编辑。

The router supports a CanDeactivate guard that gives us a chance to clean-up or ask the user's permission before navigating away from the current view.

路由器支持CanDeactivate守卫函数,它让我们有机会进行清理工作,或在离开当前视图之前征求用户的许可。

Here we see an entire user session that touches all of these features.

这里,我们看到的是一次完整的用户会话,它涉及到了所有这些我们要讲的特性。

App in action

Here's a diagram of all application routing options:

下面是该应用中所有可选路由导航图:

Navigation diagram

This app illustrates the router features we'll cover in this chapter

该应用展示了本章中涉及到的所有路由器特性:

Milestone #1: Getting Started with the Router

里程碑#1:从路由器开始

Let's begin with a simple version of the app that navigates between two empty views.

我们从该应用的一个简化版开始,它在两个空视图之间导航。

App in action

Set the <base href>

设置<base href>

The Router uses the browser's history.pushState for navigation. Thanks to pushState, we can make our in-app URL paths look the way we want them to look, e.g. localhost:3000/crisis-center. Our in-app URLs can be indistinguishable from server URLs.

路由器使用浏览器的history.pushState进行导航。 感谢pushState!有了它,我们就能按所期望的样子来显示应用内部的URL路径,比如:localhost:3000/crisis-center。虽然我们使用的全部是客户端合成的视图,但应用内部的这些URL看起来和来自服务器的没有什么不同。

Modern HTML 5 browsers were the first to support pushState which is why many people refer to these URLs as "HTML 5 style" URLs.

现代HTML 5浏览器是最早支持pushState的,这也就是很多人喜欢把这种URL称作“HTML 5风格的”URL的原因。

We must add a <base href> element tag to the index.html to make pushState routing work. The browser also needs the base href value to prefix relative URLs when downloading and linking to css files, scripts, and images.

我们必须往index.html添加一个<base href>元素标签来让pushState路由正常工作。 浏览器也需要这个basehref值,以便在下载和链接css文件、脚本和图片时,为那些相对URL加前缀。

Add the base element just after the <head> tag. If the app folder is the application root, as it is for our application, set the href value in index.html exactly as shown here.

<head>标签中的最前面添加base元素。如果app文件夹是应用的根目录(就像我们这个一样),那么就像下面展示的这样在index.html中设置href的值:

index.html (base-href)

<base href="/">

HTML 5 style navigation is the Router default. Learn why "HTML 5" style is preferred, how to adjust its behavior, and how to switch to the older hash (#) style if necessary in the Browser URL Styles appendix below.

HTML 5风格的导航是路由器的默认值。请到下面的附录浏览器URL风格中学习为什么首选“HTML 5”风格、如何调整它的行为,以及如何在必要时切换回老式的hash(#)风格。

Live example note

在线例子说明

We have to get tricky when we run the live example because the host service sets the application base address dynamically. That's why we replace the <base href...> with a script that writes a <base> tag on the fly to match.

当我们运行在线例子时,我们不得不耍一点小花招,因为在线例子的宿主服务会动态设置应用的基地址。这就是为什么我们要把<base href...>替换成了一段脚本,用来动态写入<base>标签来适应这种情况。

<script>document.write('<base href="' + document.location + '" />');</script>

We should only need this trick for the live example, not production code.

我们只应该在在线例子这种情况下使用这种小花招,不要把它用到产品的正式代码中。

Configure the routes for the Router

为路由器配置一些路由

We begin by importing some symbols from the router library.

我们先从路由库中导入一些符号。

The Router is in its own @angular/router package. It's not part of the Angular core. The router is an optional service because not all applications need routing and, depending on your requirements, you may need a different routing library.

路由器在它自己的@angular/router包中。 它不是Angular 2内核的一部分。该路由器是可选的服务,这是因为并不是所有应用都需要路由,并且,如果需要,你还可能需要另外的路由库。

We teach our router how to navigate by configuring it with routes.

通过一些路由来配置路由器,我们可以教它如何进行导航。

Define routes

定义一些路由

A router must be configured with a list of route definitions.

路由器必须用“路由定义”的列表进行配置。

Our first configuration defines an array of two routes with simple paths leading to the CrisisListComponent and HeroListComponent components.

我们的第一个配置中定义了由两个路由构成的数组,它们分别通过路径(path)导航到了CrisisListComponentHeroListComponent组件。

Each definition translates to a Route object which has a path, the URL path segment for this route, and a component, the component associated with this route.

每个定义都被翻译成了一个Route对象。该对象有一个path字段,表示该路由中的URL路径部分,和一个component字段,表示与该路由相关联的组件。

The router draws upon its registry of such route definitions when the browser URL changes or when our code tells the router to navigate along a route path.

当浏览器的URL变化时或在代码中告诉路由器导航到一个路径时,路由器就会翻出它用来保存这些路由定义的注册表。

In plain English, we might say of the first route:

直白的说,我们可以这样解释第一个路由:

Here is our first configuration. We pass the array of routes to the RouterModule.forRoot method which returns a module containing the configured Router service provider ... and some other, unseen providers that the routing library requires. Once our application is bootstrapped, the Router will perform the initial navigation based on the current browser URL.

下面是第一个配置。我们将路由数组传递到RouterModule.forRoot方法,该方法返回一个包含已配置的Router服务提供商模块和一些其它路由包需要的服务提供商。应用启动时,Router将在当前浏览器URL的基础上进行初始导航。

app/app.module.ts (excerpt)

import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { RouterModule, Routes } from '@angular/router'; import { AppComponent } from './app.component'; import { CrisisListComponent } from './crisis-list.component'; import { HeroListComponent } from './hero-list.component'; const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent }, { path: 'heroes', component: HeroListComponent } ]; @NgModule({ imports: [ BrowserModule, FormsModule, RouterModule.forRoot(appRoutes) ], declarations: [ AppComponent, HeroListComponent, CrisisListComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }

Adding the configured RouterModule to the AppModule is sufficient for simple route configurations. As our application grows, we'll want to refactor our routing configuration into a separate file and create a Routing Module, a special type of Service Module dedicated for the purpose of routing in feature modules.

作为简单的路由配置,将添加配置好的RouterModuleAppModule中就足够了。 随着应用的成长,我们将需要将路由配置重构到单独的文件,并创建路由模块 - 一种特别的、专门为特征模块的路由器服务的服务模块

Providing the RouterModule in our AppModule makes the Router available everywhere in our application.

AppModule中提供RouterModule,让该路由器在应用的任何地方都能被使用。

The AppComponent shell

壳组件AppComponent

The root AppComponent is the application shell. It has a title at the top, a navigation bar with two links, and a Router Outlet at the bottom where the router swaps views on and off the page. Here's what we mean:

根组件AppComponent是本应用的壳。它在顶部有一个标题、一个带两个链接的导航条,在底部有一个路由器插座,路由器会在它所指定的位置上把视图切入或调出页面。就像下图中所标出的:

Shell

The corresponding component template looks like this:

该组件所对应的模板是这样的:

template: ` <h1>Angular Router</h1> <nav> <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> </nav> <router-outlet></router-outlet> `

RouterOutlet

RouterOutlet is a component from the router library. The router displays views within the bounds of the <router-outlet> tags.

RouterOutlet是一个来自路由库的组件。 路由器会在<router-outlet>标签中显示视图。

A template may hold exactly one unnamed <router-outlet>. The router supports multiple named outlets, a feature we'll cover in future.

一个模板中只能有一个未命名的<router-outlet>。 但路由器可以支持多个命名的插座(outlet),将来我们会涉及到这部分特性。

Above the outlet, within the anchor tags, we see Property Bindings to the RouterLink directive that look like routerLink="...". We use the RouterLink from the router library.

在插座上方的A标签中,有一个绑定RouterLink指令的属性绑定,就像这样:routerLink="..."。我们从路由库中导入了RouterLink

The links in this example each have a string path, the path of a route that we configured earlier. We don't have route parameters yet.

例子中的每个链接都有一个字符串型的路径,也就是我们以前配置过的路由路径,但还没有指定路由参数。

We can also add more contextual information to our RouterLink by providing query string parameters or a URL fragment for jumping to different areas on our page. Query string parameters are provided through the [queryParams] binding which takes an object (e.g. { name: 'value' }), while the URL fragment takes a single value bound to the [fragment] input binding.

我们还可以通过提供查询字符串参数为RouterLink提供更多情境信息,或提供一个URL片段(Fragment或hash)来跳转到本页面中的其它区域。 查询字符串可以由[queryParams]绑定来提供,它需要一个对象型参数(如{ name: 'value' }),而URL片段需要一个绑定到[fragment]的单一值。

Learn about the how we can also use the link parameters array in the appendix below.

还可以到后面的附录中学习如何使用链接参数数组

On each anchor tag, we also see Property Bindings to the RouterLinkActive directive that look like routerLinkActive="...".

每个A标签还有一个到RouterLinkActive指令的属性绑定,就像routerLinkActive="..."

The template expression to the right of the equals (=) contains our space-delimited string of CSS classes. We can also bind to the RouterLinkActive directive using an array of classes such as [routerLinkActive]="['...']".

等号(=)右侧的模板表达式包含用空格分隔的一些CSS类。我们还可以把RouterLinkActive指令绑定到一个CSS类组成的数组,如[routerLinkActive]="['...']"

The RouterLinkActive directive toggles css classes for active RouterLinks based on the current RouterState. This cascades down through each level in our route tree, so parent and child router links can be active at the same time. To override this behavior, we can bind to the [routerLinkActiveOptions] input binding with the { exact: true } expression. By using { exact: true }, a given RouterLink will only be active if its URL is an exact match to the current URL.

RouterLinkActive指令会基于当前的RouterState对象来为激活的RouterLink切换CSS类。 这会一直沿着路由树往下进行级联处理,所以父路由链接和子路由链接可能会同时激活。 要改变这种行为,可以把[routerLinkActiveOptions]绑定到{exact: true}表达式。 如果使用了{ exact: true },那么只有在其URL与当前URL精确匹配时才会激活指定的RouterLink

Router Directives

路由器指令集

RouterLink, RouterLinkActive and RouterOutlet are directives provided by the Angular RouterModule package. They are readily available for us to use in our template.

RouterLinkRouterLinkActiveRouterOutlet是由RouterModule包提供的指令。 现在它已经可用于我们自己的模板中。

The current state of app.component.ts looks like this:

app.component.ts目前看起来是这样的:

app/app.component.ts (excerpt)

import { Component } from '@angular/core'; @Component({ selector: 'my-app', template: ` <h1>Angular Router</h1> <nav> <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> </nav> <router-outlet></router-outlet> ` }) export class AppComponent { }

"Getting Started" wrap-up

“起步阶段”总结

We've got a very basic, navigating app, one that can switch between two views when the user clicks a link.

我们得到了一个非常基本的、带导航的应用,当用户点击链接时,它能在两个视图之间切换。

We've learned how to

我们已经学会了如何:

The rest of the starter app is mundane, with little interest from a router perspective. Here are the details for readers inclined to build the sample through to this milestone.

这个初学者应用的其它部分有点平淡无奇,从路由器的角度来看也很平淡。 如果你还是倾向于在这个里程碑里构建它们,参见下面的构建详情。

Our starter app's structure looks like this:

这个初学者应用的结构看起来是这样的:

router-sample
app
app.component.ts
app.module.ts
crisis-list.component.ts
hero-list.component.ts
main.ts
node_modules ...
index.html
package.json
styles.css
tsconfig.json

Here are the files discussed in this milestone

下面是当前里程碑中讨论过的文件列表:

import { Component } from '@angular/core'; @Component({ selector: 'my-app', template: ` <h1>Angular Router</h1> <nav> <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> </nav> <router-outlet></router-outlet> ` }) export class AppComponent { } import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { RouterModule, Routes } from '@angular/router'; import { AppComponent } from './app.component'; import { CrisisListComponent } from './crisis-list.component'; import { HeroListComponent } from './hero-list.component'; const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent }, { path: 'heroes', component: HeroListComponent } ]; @NgModule({ imports: [ BrowserModule, FormsModule, RouterModule.forRoot(appRoutes) ], declarations: [ AppComponent, HeroListComponent, CrisisListComponent ], bootstrap: [ AppComponent ] }) export class AppModule { } import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app.module'; platformBrowserDynamic().bootstrapModule(AppModule); import { Component } from '@angular/core'; @Component({ template: ` <h2>HEROES</h2> <p>Get your heroes here</p>` }) export class HeroListComponent { } import { Component } from '@angular/core'; @Component({ template: ` <h2>CRISIS CENTER</h2> <p>Get your crisis here</p>` }) export class CrisisListComponent { } <html> <head> <!-- Set the base href --> <base href="/"> <title>Router Sample</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="styles.css"> <!-- Polyfills for older browsers --> <script src="node_modules/core-js/client/shim.min.js"></script> <script src="node_modules/zone.js/dist/zone.js"></script> <script src="node_modules/reflect-metadata/Reflect.js"></script> <script src="node_modules/systemjs/dist/system.src.js"></script> <script src="systemjs.config.js"></script> <script> System.import('app') .catch(function(err){ console.error(err); }); </script> </head> <body> <my-app>loading...</my-app> </body> </html>

Milestone #2: The Routing Module

里程碑 #2:路由模块

In our initial route configuration, we provided a simple setup with two routes used to configure our application for routing. This is perfectly fine for simple routing. As our application grows and we make use of more Router features, such as guards, resolvers, and child routing, we'll naturally want to refactor our routing. We recommend moving the routing into a separate file using a special-purpose service called a Routing Module.

在原始的路由配置中,我们提供了仅有两个路由的简单配置来设置应用的路由。对于简单的路由,这没有问题。 随着应用的成长,我们使用更多路由器特征,比如守卫、解析器和子路由等,我们很自然想要重构路由。 建议将路由移到单独的文件,使用专门目的的服务,叫做路由模块

The Routing Module

路由模块

Refactor routing into a module

将路由重构为模块

We'll create a file named app-routing.module.ts in our /app folder to contain our Routing Module. The routing module will import our RouterModule tokens and configure our routes. We'll follow the convention of our filename and name the Angular module AppRoutingModule.

/app目录新建一个文件,名叫app-routing.module.ts。路由模块将导入RouterModule令牌,并配置路由。 我们遵循文件名约定,并命名Angular模块为AppRoutingModule

We import the CrisisListComponent and the HeroListComponent components just like we did in the app.module.ts. Then we'll move the Router imports and routing configuration including RouterModule.forRoot into our routing module.

app.module.ts中一样,导入CrisisListComponentHeroListComponent组件。 然后在路由模块中导入Router和路由配置(RouterModule.forRoot)。

We'll also export the AppRoutingModule so we can add it to our AppModule imports.

同时导出AppRoutingModule,这样我们可以将它添加到AppModuleimports中。

Our last step is to re-export the RouterModule. By re-exporting the RouterModule, our feature module will be provided with the Router Directives when using our Routing Module.

最后,重新导出RouterModule,这样,特征模块在使用路由模块时,将获得路由指令

Here is our first Routing Module:

下面是首个路由模块

app/app-routing.module.ts (excerpt)

import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisListComponent } from './crisis-list.component'; import { HeroListComponent } from './hero-list.component'; const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent }, { path: 'heroes', component: HeroListComponent } ]; @NgModule({ imports: [ RouterModule.forRoot(appRoutes) ], exports: [ RouterModule ] }) export class AppRoutingModule {}

Next, we'll update our app.module.ts file by importing our AppRoutingModule token from the app-routing.module.ts and replace our RouterModule.forRoot with our newly created AppRoutingModule.

接下来,我们将更新app.module.ts文件,从app-routing.module.ts中导入AppRoutingModule令牌, 并用它替换RouterModule.forRoot

app/app.module.ts (excerpt)

import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { CrisisListComponent } from './crisis-list.component'; import { HeroListComponent } from './hero-list.component'; @NgModule({ imports: [ BrowserModule, FormsModule, AppRoutingModule ], declarations: [ AppComponent, HeroListComponent, CrisisListComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }

Our application continues to work just the same, and we can use our routing module as the central place to maintain our routing configuration for each feature module.

应用继续正常运行,我们可以把路由模块作为为每个特征模块维护路由配置的中心地方。

Do you need a Routing Module?

你需要路由模块吗?

The Routing Module replaces the routing configuration in the root or feature module. Either configure routes in the Routing Module or within the module itself but not in both.

路由模块在根模块或者特征模块替换了路由配置。在路由模块或者在模块内部配置路由,但不要同时在两处都配置。

The Routing Module is a design choice whose value is most obvious when the configuration is complex and includes specialized guard and resolver services. It can seem like overkill when the actual configuration is dead simple.

路由模块是设计选择,它的价值在配置很复杂,并包含专门守卫和解析器服务时尤其明显。 在配置很简单时,它可能看起来很多余。

Some developers skip the Routing Module (e.g., AppRoutingModule) when the configuration is simple and merge the routing configuration directly into the companion module (e.g., AppModule).

在配置很简单时,一些开发者跳过路由模块(例如AppRoutingModule),并将路由配置直接混合在关联模块中(比如AppModule )。

We recommend that you choose one pattern or the other and follow that pattern consistently.

我们建议你选择其中一种模式,并坚持模式的一致性。

Most developers should always implement a Routing Module for the sake of consistency.

大多数开发者应该采用路由模块,以保持一致性。

It keeps the code clean when configuration becomes complex. It makes testing the feature module easier. Its existence calls attention to the fact that a module is routed. It is where developers expect to find and expand routing configuration.

它在配置复杂时,能确保代码干净。 它让测试特征模块更加容易。 它的存在突出了模块时被路由的事实。 开发者可以很自然的从路由模块中查找和扩展路由配置。

Milestone #3: The Heroes Feature

里程碑 #2 英雄特征区

We've seen how to navigate using the RouterLink directive.

我们刚刚学习了如何用RouterLink指令进行导航。

Now we'll learn some new tricks such as how to

现在,我们将学习一些新的技巧,比如该如何:

To demonstrate, we'll build out the Heroes feature.

作为演示,我们将构建出英雄特性区。

The Heroes "feature area"

英雄“特性区”

A typical application has multiple feature areas, each an island of functionality with its own workflow(s), dedicated to a particular business purpose.

典型的应用中有多个特性区,每个区都是一个“功能岛”,它们有自己的工作流、实现一个特定的业务目标。

We could continue to add files to the app/ folder. That's unrealistic and ultimately not maintainable. We think it's better to put each feature area in its own folder.

我们可以继续把文件全添加到app/目录中。 但那么做不太现实,并且最终将无法维护。 因此,把每个特性区都放进自己的目录中会更好一些。

Our first step is to create a separate app/heroes/ folder and add Hero Management feature files there.

第一步是创建一个独立的app/heroes/文件夹,并在其中添加属于英雄管理特性区的文件。

We won't be creative about it. Our example is pretty much a copy of the code and capabilities in the "Tutorial: Tour of Heroes".

我们没有在这里引进新东西。这个例子从代码上和功能上来看,几乎就是“教程: 英雄指南”的一份拷贝。

Here's how the user will experience this version of the app

下面是该版本应用的用户体验演示:

App in action

Add Heroes functionality

添加“英雄”功能

We want to break our app out into different feature modules that we then import into our main module so it can make use of them. First, we'll create a heroes.module.ts in our heroes folder.

我们要把应用拆成不同的特性模块,然后把它们导入我们的主模块中,以便使用它们。首先,我们要在heroes目录下创建一个heroes.module.ts文件。

We delete the placeholder hero-list.component.ts that's in the app/ folder.

我们删除了位于app/目录下的占位文件hero-list.component.ts

We create a new hero-list.component.ts in the app/heroes/ folder and copy over the contents of the final heroes.component.ts from the tutorial. We copy the hero-detail.component.ts and the hero.service.ts files into the heroes/ folder.

然后在app/heroes/目录下创建了一个hero-list.component.ts文件,并从上面的教程中把heroes.component.ts最终版的内容拷贝进来。 再把hero-detail.component.tshero.service.ts文件拷贝到heroes/目录下。

We provide the HeroService in the providers array of our Heroes module so its available to all components within our module.

我们在Heroes模块的providers数组中提供了HeroService,以便它可用于模块中的所有组件。

Our Heroes module is ready for routing.

我们的Heroes模块准备好路由了。

app/heroes/heroes.module.ts (excerpt)

import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HeroListComponent } from './hero-list.component'; import { HeroDetailComponent } from './hero-detail.component'; import { HeroService } from './hero.service'; @NgModule({ imports: [ CommonModule, FormsModule ], declarations: [ HeroListComponent, HeroDetailComponent ], providers: [ HeroService ] }) export class HeroesModule {}

When we're done organizing, we have four Hero Management files:

安排完这些,我们就有了四个英雄管理特性区的文件:

app/heroes
hero-detail.component.ts
hero-list.component.ts
hero.service.ts
heroes.module.ts

Now it's time for some surgery to bring these files and the rest of the app into alignment with our application router.

现在到时间做一些“外科手术”了。我们利用应用程序的路由器,把这些文件和应用的其它部分联合起来。

Hero feature routing requirements

英雄特性区的路由需求

The new Heroes feature has two interacting components, the list and the detail. The list view is self-sufficient; we navigate to it, it gets a list of heroes and displays them. It doesn't need any outside information.

新的“英雄”特性有两个相互协作的组件,列表和详情。 列表视图是自给自足的,我们导航到它,它会自行获取英雄列表并显示它们,该组件不需要任何外部信息。

The detail view is different. It displays a particular hero. It can't know which hero on its own. That information must come from outside.

详情视图就不同了。它要显示一个特定的英雄,但是它本身却无法知道显示哪一个,此信息必须来自外部。

In our example, when the user selects a hero from the list, we navigate to the detail view to show that hero. We'll tell the detail view which hero to display by including the selected hero's id in the route URL.

在这个例子中,当用户从列表中选择了一个英雄时,我们就导航到详情页以显示那个英雄。 通过把所选英雄的id编码进路由的URL中,就能告诉详情视图该显示哪个英雄。

Hero feature route configuration

英雄特性区的路由配置

We recommend giving each feature area its own route configuration file.

我们推荐的方式是为每个特性区创建它自己的路由配置文件。

Create a new heroes-routing.module.ts in the heroes folder like this:

heroes目录下创建一个新的heroes-routing.module.ts文件,就像这样:

app/heroes/heroes-routing.module.ts (excerpt)

import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { HeroListComponent } from './hero-list.component'; import { HeroDetailComponent } from './hero-detail.component'; const heroesRoutes: Routes = [ { path: 'heroes', component: HeroListComponent }, { path: 'hero/:id', component: HeroDetailComponent } ]; @NgModule({ imports: [ RouterModule.forChild(heroesRoutes) ], exports: [ RouterModule ] }) export class HeroRoutingModule { }

Keep the Routing Module file in the same folder as its companion module file. Here both heroes-routing.module.ts and heroes.module.ts are in the same app/heroes folder.

将路由模块文件放到它相关的模块文件所在目录里。 这里,heroes-routing.module.tsheroes.module.ts都在app/heroes目录中。

We use the same techniques we learned in creating the app-routing.module.ts.

我们使用与app.routes.ts中一样的技巧。

We import the two components from their new locations in the app/heroes/ folder, define the two hero routes. and add export our HeroRoutingModule that returns our RoutingModule for the hero feature module.

从它们所在的新app/heroes/目录导入列表和详情组件,定义两个英雄路由并导出到HeroesRoutes数组。

Now that we have routes for our Heroes module, we'll need to register them with the Router. We'll import the RouterModule like we did in the app-routing.module.ts, but there is a slight difference here. In our app-routing.module.ts, we used the static forRoot method to register our routes and application level service providers. In a feature module we use static forChild method.

现在,我们的Heroes模块有路由了,我们得用路由器注册它们。 我们像在app.routing.ts中那样导入RouterModule,但这里稍微有一点不同。 在设置app.routing.ts时,我们使用了静态的forRoot方法来注册我们的路由和全应用级服务提供商。 在特性模块中,我们要改用Router.forChild静态方法。

The RouterModule.forRoot should only be provided for the AppModule. Since we are in a feature module, we'll use RouterModule.forChild method to only register additional routes.

RouterModule.forRoot只能由AppModule提供。但我们位于特性模块中,所以使用RouterModule.forChild来单独注册更多路由。

We import our HeroRoutingModule token from heroes-routing.module.ts into our Heroes module and register the routing.

我们在Heroes模块中从heroes-routing.module.ts中导入HeroRoutingModule,并注册其路由。

app/heroes/heroes.module.ts (heroes routing)

import { HeroRoutingModule } from './heroes-routing.module'; @NgModule({ imports: [ CommonModule, FormsModule, HeroRoutingModule ], declarations: [ HeroListComponent, HeroDetailComponent ], providers: [ HeroService ] })

Route definition with a parameter

带参数的路由定义

The route to HeroDetailComponent has a twist.

HeroDetailComponent的路由有点特殊。

app/heroes/heroes-routing.module.ts (excerpt)

{ path: 'hero/:id', component: HeroDetailComponent }

Notice the :id token in the path. That creates a slot in the path for a Route Parameter. In this case, we're expecting the router to insert the id of a hero into that slot.

注意路径中的:id令牌。它为路由参数在路径中创建一个“空位”。在这里,我们期待路由器把英雄的id插入到那个“空位”中。

If we tell the router to navigate to the detail component and display "Magneta", we expect hero id (15) to appear in the browser URL like this:

如果要告诉路由器导航到详情组件,并让它显示“Magneta”,我们会期望这个英雄的id(15)像这样显示在浏览器的URL中:

localhost:3000/hero/15

If a user enters that URL into the browser address bar, the router should recognize the pattern and go to the same "Magneta" detail view.

如果用户把此URL输入到浏览器的地址栏中,路由器就会识别出这种模式,同样进入“Magneta”的详情视图。

Route parameter: Required or optional?

路由参数:必选还是可选?

Embedding the route parameter token, :id, in the route definition path is a good choice for our scenario because the id is required by the HeroDetailComponent and because the value 15 in the path clearly distinguishes the route to "Magneta" from a route for some other hero.

在这个场景下,把路由参数的令牌:id嵌入到路由定义的path中是一个好主意,因为对于HeroDetailComponent来说id必须的, 而且路径中的值15已经足够把到“Magneta”的路由和到其它英雄的路由明确区分开。

An optional-route-parameter might be a better choice if we were passing an optional value to HeroDetailComponent.

当我们把一个可选值传给HeroDetailComponent时,可选路由参数可能是一个更好的选择。

命令式地导航到英雄详情

We won't navigate to the detail component by clicking a link so we won't be adding a new RouterLink anchor tag to the shell.

这次我们不打算通过点击链接来导航到详情组件,因此也不用再把带RouterLink的新的A标签加到壳组件中。

Instead, when the user clicks a hero in the list, we'll command the router to navigate to the hero detail view for the selected hero.

取而代之,当用户在列表中点击一个英雄时,我们将命令路由器导航到所选英雄的详情视图。

We'll adjust the HeroListComponent to implement these tasks, beginning with its constructor which acquires the router service and the HeroService by dependency injection:

我们将调整HeroListComponent来实现这些任务。先从构造函数开始改:它通过依赖注入系统获得路由服务和HeroService服务。

app/heroes/hero-list.component.ts (constructor)

constructor( private router: Router, private service: HeroService ) {}

We make a few changes to the template:

还要对模板进行一点修改:

template: ` <h2>HEROES</h2> <ul class="items"> <li *ngFor="let hero of heroes | async" (click)="onSelect(hero)"> <span class="badge">{{ hero.id }}</span> {{ hero.name }} </li> </ul> `

The template defines an *ngFor repeater such as we've seen before. There's a (click) EventBinding to the component's onSelect method which we implement as follows:

模板像以前一样定义了一个*ngFor重复器。 还有一个(click)事件绑定,绑定到了组件的onSelect方法,就像这样:

app/heroes/hero-list.component.ts (select)

onSelect(hero: Hero) { this.router.navigate(['/hero', hero.id]); }

It calls the router's navigate method with a Link Parameters Array. We can use this same syntax with a RouterLink if we want to use it in HTML rather than code.

它用一个链接参数数组调用路由器的navigate方法。 如果我们想把它用在HTML中,那么也可以把相同的语法用在RouterLink中。

Setting the route parameters in the list view

在列表视图中设置路由参数

We're navigating to the HeroDetailComponent where we expect to see the details of the selected hero. We'll need two pieces of information: the destination and the hero's id.

我们将导航到HeroDetailComponent组件。在那里,我们期望看到所选英雄的详情,这需要两部分信息:导航目标和该英雄的id

Accordingly, the link parameters array has two items: the path of the destination route and a route parameter that specifies the id of the selected hero.

因此,这个链接参数数组中有两个条目:目标路由的path(路径),和一个用来指定所选英雄id路由参数

app/heroes/hero-list.component.ts (link-parameters-array)

['/hero', hero.id] // { 15 }

The router composes the appropriate two-part destination URL from this array:

路由器从该数组中组合出一个合适的两段式目标URL:

localhost:3000/hero/15

Getting the route parameter in the details view

在详情视图中获得路由参数

How does the target HeroDetailComponent learn about that id? Certainly not by analyzing the URL! That's the router's job.

目标组件HeroDetailComponent该怎么知道这个id参数呢? 当然不会是自己去分析URL了!那是路由器的工作。

The router extracts the route parameter (id:15) from the URL and supplies it to the HeroDetailComponent via the ActivatedRoute service.

路由器从URL中解析出路由参数(id:15),并通过ActivatedRoute服务来把它提供给HeroDetailComponent组件。

ActivatedRoute: the one-stop-shop for route information

ActivatedRoute:一站式获得路由信息

Each route contains information about its path, data parameters, URL segment and much more. All of this information is available in an injected service provided by the router called the ActivatedRoute.

每个路由都包含路径、数据参数、URL片段等很多信息。 所有这些信息都可以通过有路由器提供的一个叫ActivatedRoute的服务提供商来获取。

The ActivatedRoute contains all the information you need from the current route component as well as ways to get information about other activated routes in the RouterState.

ActivatedRoute包含你需要从当前路由组件中获得的全部信息,正如你可以从RouterState中获得关于其它激活路由的信息。

url: An Observable of the route path(s). The value is provided as an array of strings for each part of the route path.

url: 该路由路径的Observable对象。它的值是一个由路径中各个部件组成的字符串数组。

data: An Observable that contains the data object provided for the route. Also contains any resolved values from the resolve guard.

data: 该路由提供的data对象的一个Observable对象。还包含从resolve守卫中解析出来的值。

params: An Observable that contains the required and optional parameters specific to the route.

params: 包含该路由的必选参数和可选参数Observable对象。

queryParams: An Observable that contains the query parameters available to all routes.

queryParams: 一个包含对所有路由都有效的查询参数Observable对象。

fragment: An Observable of the URL fragment available to all routes.

fragment: 一个包含对所有路由都有效的片段值的Observable对象。

outlet: The name of the RouterOutlet used to render the route. For an unnamed outlet, the outlet name is primary.

outlet: RouterOutlet的名字,用于指示渲染该路由的位置。对于未命名的RouterOutlet,这个名字是primary

routeConfig: The route configuration used for the route that contains the origin path.

routeConfig: 与该路由的原始路径对应的配置信息。

parent: an ActivatedRoute that contains the information from the parent route when using child routes.

parent: 当使用子路由时,它是一个包含父路由信息的ActivatedRoute对象。

firstChild: contains the first ActivatedRoute in the list of child routes.

firstChild: 包含子路由列表中的第一个ActivatedRoute对象。

children: contains all the child routes activated under the current route.

children: 包含当前路由下激活的全部子路由

We import the Router, ActivatedRoute, and Params tokens from the router package.

我们要从路由器(router)包中导入RouterActivatedRouteParams类。

app/heroes/hero-detail.component.ts (activated route)

import { Router, ActivatedRoute, Params } from '@angular/router';

We import the switchMap operator because we need it later to process the Observable route parameters.

app/heroes/hero-detail.component.ts (switchMap operator import)

import 'rxjs/add/operator/switchMap';

As usual, we write a constructor that asks Angular to inject services that the component requires and reference them as private variables.

通常,我们会直接写一个构造函数,让Angular把组件所需的服务注入进来,自动定义同名的私有变量,并把它们存进去。

app/heroes/hero-detail.component.ts (constructor)

constructor( private route: ActivatedRoute, private router: Router, private service: HeroService ) {}

Later, in the ngOnInit method, we use the ActivatedRoute service to retrieve the parameters for our route. Since our parameters are provided as an Observable, we use the switchMap operator to provide them for the id parameter by name and tell the HeroService to fetch the hero with that id.

然后,在ngOnInit方法中,我们用ActivatedRoute服务来接收本路由的参数。 由于这些参数是作为Observable(可观察对象)提供的,所以我们用_switchMap来通过名字引用id参数,并告诉HeroService获取指定id的英雄。

app/heroes/hero-detail.component.ts (ngOnInit)

ngOnInit() { this.route.params // (+) converts string 'id' to a number .switchMap((params: Params) => this.service.getHero(+params['id'])) .subscribe((hero: Hero) => this.hero = hero); }

The switchMap operator allows you to perform an action with the current value of the Observable, and map it to a new Observable. As with many rxjs operators, switchMap handles an Observable as well as a Promise to retrieve the value they emit.

switchMap允许你在Observable的当前值上执行一个动作,并将它映射一个新的Observable。像许多其它rxjs操作符一样, switchMap既可以处理Observable也可以处理Promise发射的值。

The switchMap operator will also cancel any in-flight requests if our user re-navigates to the route while still retrieving a hero. Our Observable is cold until subscribed to, so we use the subscribe method to get and set our retrieved Hero.

如果用户重新导航到该路由,并且它正在获取一个英雄时,switchMap操作符还将取消任何正在执行的请求。Observable是一个 可观察对象,直接订阅它,所以我们使用subscribe方法来获得并设置取到的英雄

Angular calls the ngOnInit method shortly after creating an instance of the HeroDetailComponent.

在创建了HeroDetailComponent之后,Angular很快就会调用ngOnInit方法。

We put the data access logic in the ngOnInit method rather than inside the constructor to improve the component's testability. We explore this point in greater detail in the OnInit appendix below.

我们要把数据访问逻辑放进ngOnInit方法中而不是构造函数中,以提高该组件的可测试性。 在后面的OnInit附录中,我们会再详细讲解这一点。

Learn about the ngOnInit method in the Lifecycle Hooks chapter.

要学习关于ngOnInitngOnDestroy方法的更多知识,参见生命周期钩子一章。

Observable params and component re-use

Observable参数与组件复用

In this example, we retrieve the route params from an Observable. That implies that the route params can change during the lifetime of this component.

在这个例子中,我们订阅了路由参数的Observable对象。 这种写法暗示着这些路由参数在该组件的生存期内可能会变化。

They might. By default, the router reuses a component instance when it re-navigates to the same component type without visiting a different component first. The parameters can change between each re-use.

确实如此!默认情况下,如果它没有访问过其它组件就导航到了同一个组件实例,那么路由器倾向于复用组件实例。如果复用,这些参数可以变化。

Suppose a parent component navigation bar had "forward" and "back" buttons that scrolled through the list of heroes. Each click navigated imperatively to the HeroDetailComponent with the next or previous id.

假设父组件的导航栏有“前进”和“后退”按钮,用来轮流显示英雄列表中中英雄的详情。 每次点击都会强制导航到带前一个或后一个idHeroDetailComponent组件。

We don't want the router to remove the current HeroDetailComponent instance from the DOM only to re-create it for the next id. That could be visibly jarring. Better to simply re-use the same component instance and update the parameter.

我们不希望路由器仅仅从DOM中移除当前的HeroDetailComponent实例,并且用下一个id重新创建它。 那可能导致界面抖动。 更好的方式是复用同一个组件实例,并更新这些参数。

But ngOnInit is only called once per instantiation. We need a way to detect when the route parameters change from within the same instance. The observable params property handles that beautifully.

但是ngOnInit对每个实例只调用一次。 我们需要一种方式来检测在同一个实例中路由参数什么时候发生了变化。 而params属性这个可观察对象(Observable)干净漂亮的处理了这种情况。

Snapshot: the no-observable alternative

快照:不需要可观察(no-observable)时的替代方案

This application won't reuse the HeroDetailComponent. We always return to the hero list to select another hero to view. There's no way to navigate from hero detail to hero detail without visiting the list component in between. That means we get a new HeroDetailComponent instance every time.

本应用不需要复用HeroDetailComponent。 我们总会先返回英雄列表,再选择另一位英雄。 所以,不存在从一个英雄详情导航到另一个而不用经过英雄列表的情况。 这意味着我们每次都会得到一个全新的HeroDetailComponent实例。

Suppose we know for certain that HeroDetailComponent will never, never, ever be re-used. We'll always re-create the component each time we navigate to it.

假如我们很确定HeroDetailComponent组件永远、永远不会被复用,每次导航到英雄详情时都会重新创建该组件。

The router offers a Snapshot alternative that gives us the initial value of the route parameters. We don't need to subscribe or unsubscribe. It's much simpler to write and read:

路由器提供了一个备选方案:快照(snapshot),它会给我们路由参数的初始值。这样我们就不用订阅,也不用被迫在ngDestroy中反订阅了。 这样会更容易书写和阅读:

app/heroes/hero-detail.component.ts (ngOnInit snapshot)

ngOnInit() { // (+) converts string 'id' to a number let id = +this.route.snapshot.params['id']; this.service.getHero(id) .then((hero: Hero) => this.hero = hero); }

Remember: we only get the initial value of the parameters with this technique. Stick with the observable params approach if there's even a chance that we might navigate to this component multiple times in a row. We are leaving the observable params strategy in place just in case.

记住:,用这种技巧,我们只得到了这些参数的初始值。 如果有可能连续多次导航到此组件,那么就该用params可观察对象的方式。 我们在这里选择使用params可观察对象策略,以防万一。

导航回列表组件

The HeroDetailComponent has a "Back" button wired to its gotoHeroes method that navigates imperatively back to the HeroListComponent.

HeroDetailComponent组件有一个“Back”按钮,关联到它的gotoHeroes方法,该方法会导航回HeroListComponent组件。

The router navigate method takes the same one-item link parameters array that we can bind to a [routerLink] directive. It holds the path to the HeroListComponent:

路由的navigate方法同样接受一个单条目的链接参数数组,我们也可以把它绑定到[routerLink]指令上。 它保存着HeroListComponent组件的路径

app/heroes/hero-detail.component.ts (excerpt)

gotoHeroes() { this.router.navigate(['/heroes']); }

Route Parameters

路由参数

We use route parameters to specify a required parameter value within the route URL as we do when navigating to the HeroDetailComponent in order to view-and-edit the hero with id:15.

如果想导航到HeroDetailComponent以对id为15的英雄进行查看并编辑,就要在路由的URL中使用路由参数来指定必要参数值。

localhost:3000/hero/15

Sometimes we wish to add optional information to a route request. For example, the HeroListComponent doesn't need help to display a list of heroes. But it might be nice if the previously-viewed hero were pre-selected when returning from the HeroDetailComponent.

有时我们希望往路由请求中添加可选的信息。 例如,HeroListComponent虽然不需要借助此信息显示英雄列表,但是如果从HeroDetailComponent返回时,它能自动选中刚刚查看过的英雄就好了。

Selected hero

That becomes possible if we can include hero Magneta's id in the URL when we return from the HeroDetailComponent, a scenario we'll pursue in a moment.

如果我们能在从HeroDetailComponent返回时在URL中带上英雄Magneta的id,不就可以了吗?接下来我们就尝试实现这个场景。

Optional information takes other forms. Search criteria are often loosely structured, e.g., name='wind*'. Multiple values are common — after='12/31/2015' & before='1/1/2017' — in no particular order — before='1/1/2017' & after='12/31/2015' — in a variety of formats — during='currentYear' .

可选信息有很多种形式。搜索条件通常就不是严格结构化的,比如name='wind*';有多个值也很常见,如after='12/31/2015'&before='1/1/2017'; 而且顺序无关,如before='1/1/2017'&after='12/31/2015',还可能有很多种变体格式,如during='currentYear'

These kinds of parameters don't fit easily in a URL path. Even if we could define a suitable URL token scheme, doing so greatly complicates the pattern matching required to translate an incoming URL to a named route.

这么多种参数要放在URL的路径中可不容易。即使我们能制定出一个合适的URL方案,实现起来也太复杂了,得通过模式匹配才能把URL翻译成命名路由。

Optional parameters are the ideal vehicle for conveying arbitrarily complex information during navigation. Optional parameters aren't involved in pattern matching and afford enormous flexibility of expression.

可选参数是在导航期间传送任意复杂信息的理想载体。 可选参数不涉及到模式匹配并在表达上提供了巨大的灵活性。

The Router supports navigation with optional parameters as well as required route parameters. We define optional parameters in an object after we define our required route parameters.

和必要参数一样,路由器也支持通过可选参数导航。 我们在定义完必要参数之后,通过一个对象来定义可选参数

Route Parameters: Required or Optional?

路由参数:用必要的还是可选的?

There is no hard-and-fast rule. In general,

并没有一劳永逸的规则,通常:

prefer a required route parameter when

下列情况下优先使用必要参数

prefer an optional parameter when

下列情况下优先使用可选参数

Route parameter

路由参数

When navigating to the HeroDetailComponent we specified the required id of the hero-to-edit in the route parameter and made it the second item of the link parameters array.

要导航到HeroDetailComponent,我们需要在路由参数中指定要编辑英雄的必要参数id,把这个id作为链接参数数组的第二个条目。

app/heroes/hero-list.component.ts (link-parameters-array)

['/hero', hero.id] // { 15 }

The router embedded the id value in the navigation URL because we had defined it as a route parameter with an :id placeholder token in the route path:

路由器在导航URL中内嵌了id的值,这是因为我们把它用一个:id占位符当做路由参数定义在了路由的path中:

app/heroes/heroes-routing.module.ts (hero-detail-route)

{ path: 'hero/:id', component: HeroDetailComponent }

When the user clicks the back button, the HeroDetailComponent constructs another link parameters array which it uses to navigate back to the HeroListComponent.

当用户点击后退按钮时,HeroDetailComponent构造了另一个链接参数数组,可以用它导航回HeroListComponent

app/heroes/hero-detail.component.ts (gotoHeroes)

gotoHeroes() { this.router.navigate(['/heroes']); }

This array lacks a route parameter because we had no reason to send information to the HeroListComponent.

该数组缺少一个路由参数,这是因为我们那时没有理由往HeroListComponent发送信息。

Now we have a reason. We'd like to send the id of the current hero with the navigation request so that the HeroListComponent can highlight that hero in its list. This is a nice-to-have feature; the list will display perfectly well without it.

但现在有了。我们要在导航请求中同时发送当前英雄的id,以便HeroListComponent可以在列表中高亮这个英雄。 这是一个有更好,没有也无所谓的特性,就算没有它,列表照样能显示得很完美。

We do that with an object that contains an optional id parameter. For demonstration purposes, we also defined a junk parameter (foo) that the HeroListComponent should ignore. Here's the revised navigation statement:

我们通过一个包含可选id参数的对象来做到这一点。 为了演示,我们还定义了一个没用的参数(foo),HeroListComponent应该忽略它。 下面是修改过的导航语句:

app/heroes/hero-detail.component.ts (go to heroes)

gotoHeroes() { let heroId = this.hero ? this.hero.id : null; // Pass along the hero id if available // so that the HeroList component can select that hero. this.router.navigate(['/heroes', { id: heroId, foo: 'foo' }]); }

The application still works. Clicking "back" returns to the hero list view.

该应用仍然能工作。点击“back”按钮返回英雄列表视图。

Look at the browser address bar.

注意浏览器的地址栏。

pop out the window

When running in plunker, pop out the preview window by clicking the blue 'X' button in the upper right corner.

当在plunker中运行时,请点击右上角的蓝色'X'按钮来弹出预览窗口,否则你看不到地址栏的变化。

It should look something like this, depending on where you run it:

看起来应该是这样,不过也取决于你在哪里运行它:

localhost:3000/heroes;id=15;foo=foo

The id value appears in the URL as (;id=15;foo=foo), not in the URL path. The path for the "Heroes" route doesn't have an :id token.

id的值像这样出现在URL中(;id=15;foo=foo),但不在URL的路径部分。 “Heroes”路由的路径部分并没有定义:id

The optional route parameters are not separated by "?" and "&" as they would be in the URL query string. They are separated by semicolons ";" This is matrix URL notation — something we may not have seen before.

可选的路由参数没有使用“?”和“&”符号分隔,因为它们将用在URL查询字符串中。 它们是用“;”分隔的。 这是矩阵URL标记法 —— 我们以前可能从未见过。

Matrix URL notation is an idea first floated in a 1996 proposal by the founder of the web, Tim Berners-Lee.

Matrix URL写法首次提出是在1996提案中,提出者是Web的奠基人:Tim Berners-Lee。

Although matrix notation never made it into the HTML standard, it is legal and it became popular among browser routing systems as a way to isolate parameters belonging to parent and child routes. The Router is such a system and provides support for the matrix notation across browsers.

虽然Matrix写法未曾进入过HTML标准,但它是合法的。而且在浏览器的路由系统中,它作为从父路由和子路由中单独隔离出参数的方式而广受欢迎。Angular的路由器正是这样一个路由系统,并支持跨浏览器的Matrix写法。

The syntax may seem strange to us but users are unlikely to notice or care as long as the URL can be emailed and pasted into a browser address bar as this one can.

这种语法对我们来说可能有点奇怪,不过用户不会在意这一点,因为该URL可以正常的通过邮件发出去或粘贴到浏览器的地址栏中。

Route parameters in the ActivatedRoute service

ActivatedRoute服务中的路由参数

The list of heroes is unchanged. No hero row is highlighted.

英雄列表仍没有改变,没有哪个英雄列被加亮显示。

The does highlight the selected row because it demonstrates the final state of the application which includes the steps we're about to cover. At the moment we're describing the state of affairs prior to those steps.

在线例子高亮了选中的行,因为它演示的是应用的最终状态,因此包含了我们即将示范的步骤。 此刻,我们描述的仍是那些步骤之前的状态。

The HeroListComponent isn't expecting any parameters at all and wouldn't know what to do with them. Let's change that.

HeroListComponent还完全不需要任何参数,也不知道该怎么处理它们。我们这就改变这一点。

Previously, when navigating from the HeroListComponent to the HeroDetailComponent, we subscribed to the route params Observable and made it available to the HeroDetailComponent in the ActivatedRoute service. We injected that service in the constructor of the HeroDetailComponent.

以前,当从HeroListComponent导航到HeroDetailComponent时,我们通过ActivatedRoute服务订阅了路由参数这个Observable,并让它能用在HeroDetailComponent中。我们把该服务注入到了HeroDetailComponent的构造函数中。

This time we'll be navigating in the opposite direction, from the HeroDetailComponent to the HeroListComponent.

这次,我们要进行反向导航,从HeroDetailComponentHeroListComponent

First we extend the router import statement to include the ActivatedRoute service symbol;

首先,我们扩展该路由的导入语句,以包含进ActivatedRoute服务的类;

app/heroes/hero-list.component.ts (import)

import { Router, ActivatedRoute, Params } from '@angular/router';

We'll import the switchMap operator to perform an operation on our Observable of route parameters.

我们将导入switchMap操作符,在路由参数的Observable对象上执行操作。

app/heroes/hero-list.component.ts (rxjs imports)

import 'rxjs/add/operator/switchMap'; import { Observable } from 'rxjs/Observable';

Then we inject the ActivatedRoute in the HeroListComponent constructor.

接着,我们注入ActivatedRouteHeroListComponent的构造函数中。

app/heroes/hero-list.component.ts (constructor and ngOnInit)

export class HeroListComponent implements OnInit { heroes: Observable<Hero[]>; private selectedId: number; constructor( private service: HeroService, private route: ActivatedRoute, private router: Router ) {} ngOnInit() { this.heroes = this.route.params .switchMap((params: Params) => { this.selectedId = +params['id']; return this.service.getHeroes(); }); } }

The ActivatedRoute.params property is an Observable of route parameters. The params emits new id values when the user navigates to the component. In ngOnInit we subscribe to those values, set the selectedId, and get the heroes.

ActivatedRoute.params属性是一个路由参数的可观察对象。当用户导航到这个组件时,params会发射一个新的id值。 在ngOnInit中,我们订阅了这些值,设置到selectedId,并获取英雄数据。

All route/query parameters are strings. The (+) in front of the params['id'] expression is a JavaScript trick to convert the string to an integer.

所有的路由参数或查询参数都是字符串。 params['id']表达式前面的加号(+)是一个JavaScript的小技巧,用来把字符串转换成整数。

We add an isSelected method that returns true when a hero's id matches the selected id.

我们添加了一个isSelected方法,当英雄的id和选中的id匹配时,它返回真值。

app/heroes/hero-list.component.ts (isSelected)

isSelected(hero: Hero) { return hero.id === this.selectedId; }

Finally, we update our template with a Class Binding to that isSelected method. The binding adds the selected CSS class when the method returns true and removes it when false. Look for it within the repeated <li> tag as shown here:

最后,我们用CSS类绑定更新模板,把它绑定到isSelected方法上。 如果该方法返回true,此绑定就会添加CSS类selected,否则就移除它。 在<li>标记中找到它,就像这样:

app/heroes/hero-list.component.ts (template)

template: ` <h2>HEROES</h2> <ul class="items"> <li *ngFor="let hero of heroes | async" [class.selected]="isSelected(hero)" (click)="onSelect(hero)"> <span class="badge">{{ hero.id }}</span> {{ hero.name }} </li> </ul> `

When the user navigates from the heroes list to the "Magneta" hero and back, "Magneta" appears selected:

当用户从英雄列表导航到英雄“Magneta”并返回时,“Magneta”看起来是选中的:

Selected List

The optional foo route parameter is harmless and continues to be ignored.

这儿可选的foo路由参数人畜无害,并继续被忽略。

Adding animations to the route component

为路由组件添加动画

Our heroes feature module is almost complete, but what is a feature without some smooth transitions? We already know that Angular supports animations and we want to take advantage of them by adding some animation to our Hero Detail component.

我们的“英雄”这个特性模块就要完成了,但这个特性还没有平滑的专场效果。 我们知道Angular支持动画,想要得到这项优点,就要为英雄详情组件添加一些动画。

First, we'll start by importing our animation functions that build our animation triggers, control state and manage transitions between states. We'll use these functions to add transitions to our route component as it moves between states our application view. We'll also import the HostBinding decorator for binding to our route component.

首先,我们从导入动画函数开始,它们用于构建动画触发器,以控制状态和管理状态之间的转场。 我们使用这些函数来把转场效果添加到路由组件中,这样当应用视图的多个状态之间发生转移时,就会触发动画。 我们还要导入HostBinding装饰器来绑定到路由组件。

app/heroes/hero-detail.component.ts (animation imports)

import { Component, OnInit, HostBinding, trigger, transition, animate, style, state } from '@angular/core';

Next, we'll use a host binding for route animations named @routeAnimation. There is nothing special about the choice of the binding name, but since we are controlling route animation, we'll go with routeAnimation. The binding value is set to true because we only care about the :enter and :leave states which are entering and leaving transition aliases.

接下来,我们将对名叫@routeAnimation的路由动画使用宿主绑定(HostBinding)。选择绑定名时没什么特别的要求,但是由于我们是在控制路由的动画,所以把它叫做routeAnimation。 该绑定值被设置为true,因为我们只关心:enter:leave状态,这些动画状态代表进场和离开

We'll also add some display and positioning bindings for styling.

我们还将为样式添加一些显示和位置绑定。

app/heroes/hero-detail.component.ts (route animation binding)

export class HeroDetailComponent implements OnInit { @HostBinding('@routeAnimation') get routeAnimation() { return true; } @HostBinding('style.display') get display() { return 'block'; } @HostBinding('style.position') get position() { return 'absolute'; } hero: Hero; constructor( private route: ActivatedRoute, private router: Router, private service: HeroService ) {} ngOnInit() { this.route.params // (+) converts string 'id' to a number .switchMap((params: Params) => this.service.getHero(+params['id'])) .subscribe((hero: Hero) => this.hero = hero); } gotoHeroes() { let heroId = this.hero ? this.hero.id : null; // Pass along the hero id if available // so that the HeroList component can select that hero. this.router.navigate(['/heroes', { id: heroId, foo: 'foo' }]); } }

Now we can build our animation trigger, which we'll call routeAnimation to match the binding we previously setup. We'll use the wildcard state that matches any animation state our route component is in, along with two transitions. One transition animates the component as it enters the application view (:enter), while the other animates the component as it leaves the application view (:leave).

现在,我们可以构建动画触发器了,我们称之为routeAnimation来匹配我们以前定义的绑定。 我们的使用通配符状态,它们匹配我们这个路由组件的任意动画状态, 后面是两个转场动画。一个转场动画(:enter)在组件进入应用视图时触发, 另一个(:leave)在离开时触发。

We could add different transitions to different route components depending on our needs. We'll just animate our HeroDetailComponent for this milestone.

如果需要,我们还可以为其它路由组件添加不同的转场动画。在这个里程碑中我们只为HeroDetailComponent添加动画。

Using route animations on individual components is something we don't want to do throughout our entire application. It would be better to animate routes based on route paths, a topic to cover in a future update to this chapter.

在整个应用程序中,我们并不想在每个组件中都使用路由动画。 我们认为基于路由路径进行路由动画会更好一些,本章将来的更新中会涉及到这个主题。

Our route component animation looks as such:

我们的路由动画看起来像这样:

app/heroes/hero-detail.component.ts (route animation)

@Component({ template: ` <h2>HEROES</h2> <div *ngIf="hero"> <h3>"{{ hero.name }}"</h3> <div> <label>Id: </label>{{ hero.id }}</div> <div> <label>Name: </label> <input [(ngModel)]="hero.name" placeholder="name"/> </div> <p> <button (click)="gotoHeroes()">Back</button> </p> </div> `, animations: [ trigger('routeAnimation', [ state('*', style({ opacity: 1, transform: 'translateX(0)' }) ), transition(':enter', [ style({ opacity: 0, transform: 'translateX(-100%)' }), animate('0.2s ease-in') ]), transition(':leave', [ animate('0.5s ease-out', style({ opacity: 0, transform: 'translateY(100%)' })) ]) ]) ] }) export class HeroDetailComponent implements OnInit {

Simply stated, our HeroDetailComponent will ease in from the left when routed to and will slide down when navigating away. We could add more complex animations here, but we'll leave our HeroDetailComponent as is for now.

简单的说,当进入路由时,HerodetailComponent组件会从左侧弹入,离开时会向下方滑出。 这里我们还可以添加更复杂的动画,不过我们现在就先不这么做了。

Import hero module into AppModule

把hero模块导入到AppModule中

Our heroes feature module is ready, but application doesn't know about our heroes module yet. We'll need to import it into the AppModule we defined in app.module.ts.

英雄特性已经完工,但是应用还不知道我们的英雄模块。 我们得把它导入我们在app.module.ts中导入的AppModule中。

Update app.module.ts as follows:

像这样修改app.module.ts

app/app.module.ts (heroes module import)

import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisListComponent } from './crisis-list.component'; @NgModule({ imports: [ BrowserModule, FormsModule, HeroesModule, AppRoutingModule ], declarations: [ AppComponent, CrisisListComponent ], bootstrap: [ AppComponent ] })

We imported the HeroesModule and added it to our AppModule's imports.

我们导入了HeroesModule,并把它加入了AppModuleimports中。

We removed the HeroListComponent from the AppModule's declarations because its being provided by the HeroesModule now. This is important because there can be only one owner for a declared component. In our case, the Heroes module is the owner of the Heroes components and is making them available to the AppModule.

我们从AppModuledeclarations中移除了HeroListComponent,因为它现在改由HeroesModule提供了。 这一步很重要,因为一个组件只能有一个所有者。现在这种情况下,Heroes模块应该是Heroes组件的所有者,并让它可用于AppModule中。

Routes provided by feature modules will be combined together into their imported module's routes by the router. This allows us to continue defining our feature module routes without modifying our main route configuration.

由特性模块提供的路由将会被路由器和它们导入的模块提供的路由组合在一起。这让我们可以继续定义特性路由,而不用修改主路由配置。

As a result, the AppModule no longer has specific knowledge of the hero feature, its components, or its route details. We can evolve the hero feature with more components and different routes. That's a key benefit of creating a separate module for each feature area.

这种调整的结果是:app.module.ts不再具有任何有关英雄特性的特殊知识,关于它的组件或路由的细节。 这个“英雄特性区”可以演化出更多的组件和不同的路由。 这是为每个特性区创建一个独立模块带来的核心优势。

Since our Heroes routes are defined within our feature module, we can also remove our initial heroes route from the app-routing.module.ts.

由于Heroes路由被定义在了我们的特性模块中,我们也可以从app-routing.module.ts中移除当初的heroes路由了。

app/app-routing.module.ts (v2)

import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisListComponent } from './crisis-list.component'; const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent } ]; @NgModule({ imports: [ RouterModule.forRoot(appRoutes) ], exports: [ RouterModule ] }) export class AppRoutingModule {}

Heroes App Wrap-up

“英雄”应用总结

We've reached the second milestone in our router education.

我们已经抵达了路由教程的第二个里程碑。

We've learned how to

我们学到了如何:

After these changes, the folder structure looks like this:

做完这些修改之后,目录结构看起来就像这样:

router-sample
app
heroes
hero-detail.component.ts
hero-list.component.ts
hero.service.ts
heroes.module.ts
heroes-routing.module.ts
app.component.ts
app.module.ts
app-routing.module.ts
crisis-list.component.ts
main.ts
node_modules ...
index.html
package.json
styles.css
tsconfig.json

The Heroes App code

英雄应用的源码

Here are the relevant files for this version of the sample application.

这里是当前版本的范例程序相关文件。

import { Component } from '@angular/core'; @Component({ selector: 'my-app', template: ` <h1>Angular Router</h1> <nav> <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> </nav> <router-outlet></router-outlet> ` }) export class AppComponent { } import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisListComponent } from './crisis-list.component'; @NgModule({ imports: [ BrowserModule, FormsModule, HeroesModule, AppRoutingModule ], declarations: [ AppComponent, CrisisListComponent ], bootstrap: [ AppComponent ] }) export class AppModule { } import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; const appRoutes: Routes = [ ]; @NgModule({ imports: [ RouterModule.forRoot(appRoutes) ], exports: [ RouterModule ] }) export class AppRoutingModule {} // TODO SOMEDAY: Feature Componetized like CrisisCenter import 'rxjs/add/operator/switchMap'; import { Observable } from 'rxjs/Observable'; import { Component, OnInit } from '@angular/core'; import { Router, ActivatedRoute, Params } from '@angular/router'; import { Hero, HeroService } from './hero.service'; @Component({ template: ` <h2>HEROES</h2> <ul class="items"> <li *ngFor="let hero of heroes | async" [class.selected]="isSelected(hero)" (click)="onSelect(hero)"> <span class="badge">{{ hero.id }}</span> {{ hero.name }} </li> </ul> ` }) export class HeroListComponent implements OnInit { heroes: Observable<Hero[]>; private selectedId: number; constructor( private service: HeroService, private route: ActivatedRoute, private router: Router ) {} ngOnInit() { this.heroes = this.route.params .switchMap((params: Params) => { this.selectedId = +params['id']; return this.service.getHeroes(); }); } isSelected(hero: Hero) { return hero.id === this.selectedId; } onSelect(hero: Hero) { this.router.navigate(['/hero', hero.id]); } } import 'rxjs/add/operator/switchMap'; import { Component, OnInit, HostBinding, trigger, transition, animate, style, state } from '@angular/core'; import { Router, ActivatedRoute, Params } from '@angular/router'; import { Hero, HeroService } from './hero.service'; @Component({ template: ` <h2>HEROES</h2> <div *ngIf="hero"> <h3>"{{ hero.name }}"</h3> <div> <label>Id: </label>{{ hero.id }}</div> <div> <label>Name: </label> <input [(ngModel)]="hero.name" placeholder="name"/> </div> <p> <button (click)="gotoHeroes()">Back</button> </p> </div> `, animations: [ trigger('routeAnimation', [ state('*', style({ opacity: 1, transform: 'translateX(0)' }) ), transition(':enter', [ style({ opacity: 0, transform: 'translateX(-100%)' }), animate('0.2s ease-in') ]), transition(':leave', [ animate('0.5s ease-out', style({ opacity: 0, transform: 'translateY(100%)' })) ]) ]) ] }) export class HeroDetailComponent implements OnInit { @HostBinding('@routeAnimation') get routeAnimation() { return true; } @HostBinding('style.display') get display() { return 'block'; } @HostBinding('style.position') get position() { return 'absolute'; } hero: Hero; constructor( private route: ActivatedRoute, private router: Router, private service: HeroService ) {} ngOnInit() { this.route.params // (+) converts string 'id' to a number .switchMap((params: Params) => this.service.getHero(+params['id'])) .subscribe((hero: Hero) => this.hero = hero); } gotoHeroes() { let heroId = this.hero ? this.hero.id : null; // Pass along the hero id if available // so that the HeroList component can select that hero. this.router.navigate(['/heroes', { id: heroId, foo: 'foo' }]); } } import { Injectable } from '@angular/core'; export class Hero { constructor(public id: number, public name: string) { } } let HEROES = [ new Hero(11, 'Mr. Nice'), new Hero(12, 'Narco'), new Hero(13, 'Bombasto'), new Hero(14, 'Celeritas'), new Hero(15, 'Magneta'), new Hero(16, 'RubberMan') ]; let heroesPromise = Promise.resolve(HEROES); @Injectable() export class HeroService { getHeroes() { return heroesPromise; } getHero(id: number | string) { return heroesPromise .then(heroes => heroes.find(hero => hero.id === +id)); } } import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HeroListComponent } from './hero-list.component'; import { HeroDetailComponent } from './hero-detail.component'; import { HeroService } from './hero.service'; import { HeroRoutingModule } from './heroes-routing.module'; @NgModule({ imports: [ CommonModule, FormsModule, HeroRoutingModule ], declarations: [ HeroListComponent, HeroDetailComponent ], providers: [ HeroService ] }) export class HeroesModule {} import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { HeroListComponent } from './hero-list.component'; import { HeroDetailComponent } from './hero-detail.component'; const heroesRoutes: Routes = [ { path: 'heroes', component: HeroListComponent }, { path: 'hero/:id', component: HeroDetailComponent } ]; @NgModule({ imports: [ RouterModule.forChild(heroesRoutes) ], exports: [ RouterModule ] }) export class HeroRoutingModule { }

Milestone #4: The Crisis Center

里程碑#4:危机中心

The Crisis Center is a fake view at the moment. Time to make it useful.

此刻,危机中心还只是一个假视图,该让它有用点了!

The new Crisis Center begins as a virtual copy of the Heroes module. We create a new app/crisis-center folder, copy the Hero files, and change every mention of "hero" to "crisis".

新的危机中心英雄模块的一个拷贝开始。我们创建新的app/crisis-center目录,把英雄区的文件拷贝过去,并把所有的“hero”修改“crisis”。

A Crisis has an id and name, just like a Hero The new CrisisListComponent displays lists of crises. When the user selects a crisis, the app navigates to the CrisisDetailComponent for display and editing of the crisis name.

Crisisidname,和Hero一样。 新的CrisisListComponent显示了危机列表。 当用户选择一个危机时,应用导航到CrisisDetailComponent,显示和编辑危机名字。

Voila, another feature module!

真棒!另一个特性模块诞生了

There's no point to this exercise unless we can learn something. We do have new ideas and techniques in mind:

除非我们能学到点新东西,否则这种练习就没啥亮点。 不过,我们已经有了一些新主意和新技巧:

We'll address all of these issues in the Crisis Center starting with the introduction of child routes

我们要在危机中心中处理好所有这些问题。 那就先从对子路由的介绍开始吧。

We'll leave Heroes in its less-than-perfect state to serve as a contrast with what we believe to be a superior Crisis Center design.

我们将把英雄特性区保留在不够完美的状态,以便与危机中心进行对比,我们相信后者是一个更好的设计。

A Crisis Center with child routes

带子路由的“危机中心”

We'll organize the Crisis Center to conform to the following recommended pattern for Angular applications.

我们将按照下列模式组织危机中心的目录结构。

If we had many feature areas, their component trees might look like this:

如果我们有更多特性区,它们的组件树看起来是这样的:

Component Tree

Child Routing Component

子路由组件

Add the following crisis-center.component.ts to the crisis-center folder:

crisis-center目录下添加下列crisis-center.component.ts文件:

app/crisis-center/crisis-center.component.ts (minus imports)

@Component({ template: ` <h2>CRISIS CENTER</h2> <router-outlet></router-outlet> ` }) export class CrisisCenterComponent { }

The CrisisCenterComponent is much like the AppComponent shell.

CrisisCenterComponent和壳组件AppComponent很像。

Unlike AppComponent (and most other components), it lacks a selector. It doesn't need one. We don't embed this component in a parent template. We navigate to it from the outside, via the router.

但与AppComponent(以及大多数其它组件)不同的是,它缺少一个选择器(selector。 它不需要。我们不会把该组件嵌入到父模板中,只会通过路由器从外部导航到它。

We can give it a selector. There's no harm in it. Our point is that we don't need one because we only navigate to it.

我们可以为它指定一个选择器。这么做没有什么坏处。 这里的观点是,我们不需要选择器,因为我们只能*导航到它。

Child Route Configuration

子路由配置

The CrisisCenterComponent is a Routing Component like the AppComponent. It has its own RouterOutlet and its own child routes.

CrisisCenterComponent是一个像AppComponent一样的路由组件。 它有自己的RouterOutlet和自己的子路由。

Add the following crisis-center-home.component.ts to the crisis-center folder.

把下面的crisis-center-home.component.ts文件添加到crisis-center目录中。

app/crisis-center/crisis-center-home.component.ts (minus imports)

@Component({ template: ` <p>Welcome to the Crisis Center</p> ` }) export class CrisisCenterHomeComponent { }

We create a crisis-center-routing.module.ts file as we did the heroes-routing.module.ts file. But this time we define child routes within the parent crisis-center route.

heroes-routing.module.ts文件一样,我们也创建一个crisis-center-routing.module.ts。 但这次,我们要把子路由定义在父路由crisis-center中。

app/crisis-center/crisis-center-routing.module.ts (Routes)

const crisisCenterRoutes: Routes = [ { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ] }) export class CrisisCenterRoutingModule { }

Notice that the parent crisis-center route has a children property with a single route containing our CrisisListComponent. The CrisisListComponent route also has a children array with two routes.

注意,父路由crisis-center有一个children属性,它有一个包含CrisisListComponent的路由。 CrisisListModule路由还有一个带两个路由的children数组。

These two routes navigate to the two Crisis Center child components, CrisisCenterHomeComponent and CrisisDetailComponent.

这两个路由导航到了危机中心的两个子组件:CrisisCenterHomeComponentCrisisDetailComponent

There are some important differences in the treatment of these routes.

对这些路由的处理中有一些重要的不同

The router displays the components of these routes in the RouterOutlet of the CrisisCenterComponent, not in the RouterOutlet of the AppComponent shell.

路由器会把这些路由对应的组件放在CrisisCenterComponentRouterOutlet中,而不是AppComponent壳组件中的。

The CrisisListComponent contains the crisis list and a RouterOutlet to display the Crisis Center Home and Crisis Detail route components.

CrisisListComponent包含危机列表和一个RouterOutlet,用以显示Crisis Center HomeCrisis Detail这两个路由组件。

The Crisis Detail route is a child of the Crisis List. Since the router reuses components by default, the Crisis Detail component will be re-used as we select different crises.

Crisis Detail路由是Crisis List的子路由。由于路由器默认会复用组件,因此当我们选择了另一个危机时,CrisisDetailComponent会被复用。

In contrast, back in the Hero Detail route, the component was recreated each time we selected a different hero.

作为对比,回到Hero Detail路由时,每当我们选择了不同的英雄时,该组件都会被重新创建。

At the top level, paths that begin with / refer to the root of the application. But these are child routes. They extend the path of the parent route. With each step down the route tree, we add a slash followed by the route path (unless the route path is empty).

在顶级,以/开头的路径指向的总是应用的根。 但这里是子路由。 它们是在父路由路径的基础上做出的扩展。 在路由树中每深入一步,我们就会在该路由的路径上添加一个斜线/(除非该路由的路径是空的)。

For example, the parent path to the CrisisCenterComponent is /crisis-center The router appends these child paths to the parent path to the CrisisCenterComponent (/crisis-center).

例如,CrisisCenterComponent的路径是/crisis-center。 路由器就会把这些子路由的路径中添加上到父路由CrisisCenterComponent的路径(/crisis-center)。

The absolute URL for the latter example, including the origin, is

本例子中的绝对URL,包含源站部分,就是:

localhost:3000/crisis-center/2

Here's the complete crisis-center-routing.module.ts file with its imports.

这里是完整的crisis-center.routing.ts及其导入语句。

app/crisis-center/crisis-center-routing.module.ts (excerpt)

import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisCenterHomeComponent } from './crisis-center-home.component'; import { CrisisListComponent } from './crisis-list.component'; import { CrisisCenterComponent } from './crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail.component'; const crisisCenterRoutes: Routes = [ { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ] }) export class CrisisCenterRoutingModule { }

Import crisis center module into the AppModule routes

把危机中心模块导入`AppModule`的路由中

As with the Heroes module, we must import the Crisis Center module into the AppModule:

Heroes模块中一样,我们必须把危机中心模块导入AppModule中:

app/app.module.ts (import CrisisCenterModule)

import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisCenterModule } from './crisis-center/crisis-center.module'; import { DialogService } from './dialog.service'; @NgModule({ imports: [ CommonModule, FormsModule, HeroesModule, CrisisCenterModule, AppRoutingModule ], declarations: [ AppComponent ], providers: [ DialogService ], bootstrap: [ AppComponent ] }) export class AppModule { }

We also remove the initial crisis center route from our app-routing.module.ts. Our routes are now being provided by our HeroesModule and our CrisisCenter feature modules. We'll keep our app-routing.module.ts file for general routes which we'll cover later in the chapter.

我们还从app.routing.ts中移除了危机中心的初始路由。我们的路由现在是由HeroesModuleCrisisCenter特性模块提供的。 我们将保持app.routing.ts文件中只有通用路由,本章稍后会讲解它。

app/app-routing.module.ts (v3)

import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; const appRoutes: Routes = [ ]; @NgModule({ imports: [ RouterModule.forRoot(appRoutes) ], exports: [ RouterModule ] }) export class AppRoutingModule {}

Redirecting routes

重定向路由

When the application launches, the initial URL in the browser bar is something like:

当应用启动时,浏览器地址栏的初始URL是这样的:

localhost:3000

That doesn't match any of our configured routes which means that our application won't display any component when it's launched. The user must click one of the navigation links to trigger a navigation and display something.

它无法匹配上我们配置过的任何路由,这意味着在应用启动的时候它将不会显示任何组件。 用户必须点击一个导航链接来触发导航并显示点什么。

We prefer that the application display the list of crises as it would if the user clicked the "Crisis Center" link or pasted localhost:3000/crisis-center/ into the address bar. This is our intended default route.

当用户点击了“Crisis Center”链接或者在地址栏粘贴localhost:3000/crisis-center/时,我们更希望该应用能直接显示危机列表。这就是默认路由。

The preferred solution is to add a redirect route that transparently translates from the initial relative URL ('') to the desired default path (/crisis-center):

首选的解决方案是添加一个redirect路由,它会把初始的相对URL('')悄悄翻译成默认路径(/crisis-center)。

{ path: '', redirectTo: '/crisis-center', pathMatch: 'full' },

A redirect route requires a pathMatch property to tell the router how to match a URL to the path of a route. In this app, the router should select the route to the CrisisListComponent when the entire URL matches '', so we set the pathMatch value to 'full'.

“重定向(redirect)路由”需要一个pathMatch属性来告诉路由器如何把URL和路由中的路径进行匹配。 本应用中,路由器应该只有在完整的URL匹配''时才选择指向CrisisListComponent的路由,因此,我们把pathMatch的值设置为'full'

Technically, pathMatch = 'full' results in a route hit when the remaining, unmatched segments of the URL match ''. In our example, the redirect is at the top level of the route configuration tree so the remaining URL and the entire URL are the same thing.

从原理上说,pathMatch = 'full'导致路由器尝试用URL中剩下的、未匹配过的片段去匹配''。 在这个例子中,重定向发生在路由配置树的顶级,所以剩下的URL和完整的URL是完全一样的。

The other possible pathMatch value is 'prefix' which tells the router to match the redirect route when the remaining URL begins with the redirect route's prefix path.

pathMatch的另一个可能的值是'prefix',这会告诉路由器去匹配剩下的URL是否以这个“重定向路由”的前缀路径开头

That's not what we want to do here. If the pathMatch value were 'prefix', every URL would match ''. We could never navigate to /crisis-center/1 because the redirect route would match first and send us to the CrisisListComponent.

显然,在这里我们并不想这样。如果pathMatch的值是'prefix',_每个_URL都会匹配''。 因为这个“重定向路由”会首先匹配上并把我们引向CrisisListComponent,所以我们就永远无法导航到/crisis-center/1了。

We should redirect to the CrisisListComponent only when the entire (remaining) url is ''.

只有当_完整的(剩余的)_URL是''时,我们才应该重定向到CrisisListComponent

Learn more in Victor Savkin's blog post on redirects.

要学习更多,请参见Victor Savkin的博客中 关于重定向的帖子.

We'll discuss redirects in more detail in a future update to this chapter.

我们将在未来的更新中深入讨论重定向问题。

The updated route definitions look like this:

修改过的路由定义看起来是这样的:

app/crisis-center/crisis-center-routing.module.ts (routes v2)

import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisCenterHomeComponent } from './crisis-center-home.component'; import { CrisisListComponent } from './crisis-list.component'; import { CrisisCenterComponent } from './crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail.component'; const crisisCenterRoutes: Routes = [ { path: '', redirectTo: '/crisis-center', pathMatch: 'full' }, { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent, }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ] }) export class CrisisCenterRoutingModule { }

Relative Navigation

相对导航

While building out our Crisis Center feature, we've navigated to the Crisis Detail route using an absolute path that begins with a slash. This navigation starts from the top of our route configuration to find the matching path to our route.

当构建危机中心特性时,我们将使用以斜线开头的绝对路径来导航到危机详情路由。 这次导航会从路由配置的顶部开始查找路由,以匹配路径。

We could continue to use absolute paths to navigate inside our Crisis Center feature, but that makes our links very rigid. If we changed our parent /crisis-center path, we would have to change our link parameters array.

我们可以在危机中心特性区继续使用绝对路径进行导航,但这会让我们的链接过于死板。 如果更改了父路由路径/crisis-center,我们就不得不到处更改链接参数数组。

We can make our links more flexible by using relative navigation with the router.

通过让路由器使用相对导航的方式,我们可以让链接更富有弹性。

The link parameters array supports a directory-like syntax for relative navigation.

链接参数数组通过目录式语法支持相对导航。

./ or no leading slash is relative to the current level.

./或“无前导斜线”时表示相当于当前级别。

../ to go up one level in the route path.

../表示在路由路径中往上走一级。

The relative navigation syntax can be used in combination with a path. If we wanted to navigate from one route path to another sibling route path we could use ../path convention to go up one level and down to the sibling route path.

相对导航的语法可以和路径组合在一起,如果我们要从一个路由路径导航到一个兄弟路由路径, 可以使用../path来简便的导航到上一级然后再进入兄弟路由路径。

In order to navigate relatively using the Router service, we use the ActivatedRoute to give the router knowledge of where we are in the RouterState, which is our tree of activated routes. We do this by adding an object as the second argument in our router.navigate method after the link parameters array specifying the relativeTo property. We set the relativeTo property to our ActivatedRoute and the router will merge our navigation information into to the current URL.

要使用Router进行相对导航,可以使用ActivatedRoute来告诉路由器我们正在RouterState中的什么地方,RouterState是激活路由组成的树。 要做到这一点,我们可以为router.navigate方法中链接参数数组后的对象型参数指定relativeTo属性。 只要把这个relativeTo属性设置为我们的ActivatedRoute,路由器就会把我们的导航信息和当前URL合并在一起。

When using router's navigateByUrl method, the navigation is always absolute.

当使用路由器的navigateByUrl方法时,导航总是绝对的。

用相对方式导航到危机详情

Let's update our Crisis List onSelect method to use relative navigation so we don't have to start from the top of our route configuration. We've already injected the ActivatedRoute into our constructor that we'll need for the relative navigation.

我们来把危机列表中的onSelect方法改成相对导航,以便我们不必从路由配置的顶部开始。 我们已经把相对导航所需的ActivatedRoute注入到了构造函数中。

app/crisis-center/crisis-list.component.ts (constructor)

constructor( private service: CrisisService, private route: ActivatedRoute, private router: Router ) {}

When we visit the Crisis Center, our path is /crisis-center, so we just want to add the id of the Crisis Center to our existing path. When the router navigates, it will use the current path /crisis-center, adding on our id. If our id were 1, the resulting path would be /crisis-center/1.

当我们访问危机中心时,当前路径是/crisis-center,所以我们只要把危机id添加到现有路径中就可以了。当路由器导航时,它使用当前路径/crisis-center并追加上此id。如果id1,结果路径就是/crisis-center/1

app/crisis-center/crisis-list.component.ts (relative navigation)

onSelect(crisis: Crisis) { this.selectedId = crisis.id; // Navigate with relative link this.router.navigate([crisis.id], { relativeTo: this.route }); }

We'll also update the Crisis Detail component to navigate back to our Crisis Center list. We want to go back up a level in the path, so we use to the ../ syntax. If our current id is 1, the resulting path coming from /crisis-center/1 would be /crisis-center.

我们还要修改危机详情组件以便导航回危机中心列表。我们得回到路径的上一级,所以我们使用../语法。如果当前id1,那么结果路径就会从/crisis-center/1变成/crisis-center

app/crisis-center/crisis-detail.component.ts (relative navigation)

gotoCrises() { let crisisId = this.crisis ? this.crisis.id : null; // Pass along the crisis id if available // so that the CrisisListComponent can select that crisis. // Add a totally useless `foo` parameter for kicks. // Relative navigation back to the crises this.router.navigate(['../', { id: crisisId, foo: 'foo' }], { relativeTo: this.route }); }

If we are using a RouterLink to navigate instead of the Router service, we can use the same link parameters array, but we don't have to provide the object with the relativeTo property. The ActivatedRoute is implicit in the RouterLink directive.

如果我们正在使用RouterLink进行导航,而不是Router服务,仍然可以使用相同的链接参数数组,不过我们不用提供带relativeTo属性的对象。在RouterLink指令中ActivatedRoute是默认的。

app/crisis-center/crisis-list.component.ts (relative routerLink)

<a [routerLink]="[crisis.id]">{{ crisis.name }}</a>

Route Guards

路由守卫

Milestone #5: Route Guards

里程碑#5:路由守卫

At the moment, any user can navigate anywhere in the application anytime.

现在,任何用户都能在任何时候导航到任何地方

That's not always the right thing to do.

但有时候这样是不对的。

We can add guards to our route configuration to handle these scenarios.

我们可以往路由配置中添加守卫,来处理这些场景。

A guard's return value controls the router's behavior:

守卫返回一个值,以控制路由器的行为:

The guard can also tell the router to navigate elsewhere, effectively canceling the current navigation.

守卫还可以告诉路由器导航到别处,这样也取消当前的导航。

The guard might return its boolean answer synchronously. But in many cases, the guard can't produce an answer synchronously. The guard could ask the user a question, save changes to the server, or fetch fresh data. These are all asynchronous operations.

守卫可以用同步的方式返回一个布尔值。但在很多情况下,守卫无法用同步的方式给出答案。 守卫可能会向用户问一个问题、把更改保存到服务器,或者获取新数据,而这些都是异步操作。

Accordingly, a routing guard can return an Observable<boolean> or a Promise<boolean> and the router will wait for the observable to resolve to true or false.

因此,路由的守卫可以返回一个Observable<boolean>Promise<boolean>,并且路由器会等待这个可观察对象被解析为truefalse

The router supports multiple kinds of guards:

路由器支持多种守卫:

  1. CanActivate to mediate navigation to a route.

  2. CanActivate来处理导航某路由的情况。

  3. CanActivateChild to mediate navigation to a child route.

  4. CanActivateChild处理导航子路由的情况。

  5. CanDeactivate to mediate navigation away from the current route.

  6. CanDeactivate来处理从当前路由离开的情况。

  7. Resolve to perform route data retrieval before route activation.

  8. Resolve在路由激活之前获取路由数据。

  9. CanLoad to mediate navigation to a feature module loaded asynchronously.

  10. CanLoad来处理异步导航到某特性模块的情况。

We can have multiple guards at every level of a routing hierarchy. The router checks the CanDeactivate and CanActivateChild guards first, from deepest child route to the top. Then it checks the CanActivate guards from the top down to the deepest child route. If the feature module is loaded asynchronously, the CanLoad guard is checked before the module is loaded. If any guard returns false, pending guards that have not completed will be canceled, and the entire navigation is canceled.

在分层路由的每个级别上,我们都可以设置多个守卫。 路由器会先按照从最深的子路由由下往上检查的顺序来检查CanDeactivate守护条件。 然后它会按照从上到下的顺序检查CanActivate守卫。 如果任何守卫返回false,其它尚未完成的守卫会被取消,这样整个导航就被取消了。

Let's look at some examples.

我们来看一些例子。

CanActivate: requiring authentication

CanActivate: 要求认证

Applications often restrict access to a feature area based on who the user is. We could permit access only to authenticated users or to users with a specific role. We might block or limit access until the user's account is activated.

应用程序通常会根据访问者来决定是否授予某个特性区的访问权。 我们可以只对已认证过的用户或具有特定角色的用户授予访问权,还可以阻止或限制用户访问权,直到用户账户激活为止。

The CanActivate guard is the tool to manage these navigation business rules.

CanActivate守卫是一个管理这些导航类业务规则的工具。

Add an admin feature module

添加一个“管理”特性模块

We intend to extend the Crisis Center with some new administrative features. Those features aren't defined yet. So we add a new feature module named AdminModule. We'll follow our same convention by creating an admin folder with a feature module file, route file and supporting components.

我们准备扩展“危机中心”,添加一些新的管理类特性。 这些特性还没有定义过,所以我们先只添加一个名叫AdminModule的占位模块。 我们会遵循与创建admin目录中的特性模块文件、路由文件和支持组件时相同的约定。

Our admin feature module file structure looks like this:

管理特性区的文件是这样的:

app/admin
admin-dashboard.component.ts
admin.component.ts
admin.module.ts
admin-routing.module.ts
manage-crises.component.ts
manage-heroes.component.ts

Our admin feature module contains our AdminComponent used for routing within our feature module, a dashboard route and two unfinished components to manage crises and heroes.

管理特性模块包含AdminComponent,它用于在特性模块内的仪表盘路由以及两个尚未完成的用于管理危机和英雄的组件之间进行路由。

import { Component } from '@angular/core'; @Component({ template: ` <p>Dashboard</p> ` }) export class AdminDashboardComponent { } import { Component } from '@angular/core'; @Component({ template: ` <h3>ADMIN</h3> <nav> <a routerLink="./" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Dashboard</a> <a routerLink="./crises" routerLinkActive="active">Manage Crises</a> <a routerLink="./heroes" routerLinkActive="active">Manage Heroes</a> </nav> <router-outlet></router-outlet> ` }) export class AdminComponent { } import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { AdminComponent } from './admin.component'; import { AdminDashboardComponent } from './admin-dashboard.component'; import { ManageCrisesComponent } from './manage-crises.component'; import { ManageHeroesComponent } from './manage-heroes.component'; import { AdminRoutingModule } from './admin-routing.module'; @NgModule({ imports: [ CommonModule, AdminRoutingModule ], declarations: [ AdminComponent, AdminDashboardComponent, ManageCrisesComponent, ManageHeroesComponent ] }) export class AdminModule {} import { Component } from '@angular/core'; @Component({ template: ` <p>Manage your crises here</p> ` }) export class ManageCrisesComponent { } import { Component } from '@angular/core'; @Component({ template: ` <p>Manage your heroes here</p> ` }) export class ManageHeroesComponent { }

Since our admin dashboard RouterLink is an empty path route in our AdminModule, it is considered a match to any route within our admin feature area. We only want the Dashboard link to be active when we visit that route. We've added an additional binding to our Dashboard routerLink, [routerLinkActiveOptions]="{ exact: true }" which will only mark the ./ link as active when we navigate the to /admin URL and not when we navigate to one the other child routes.

由于AdminModule中管理仪表盘的RouterLink是一个空路径的路由,所以它会匹配到管理特性区的任何路由。但我们只有在访问Dashboard路由时才希望该链接被激活。所以我们往Dashboard这个routerLink上添加了另一个绑定[routerLinkActiveOptions]="{ exact: true }",这样就只有当我们导航到/admin这个URL时才会激活它,而不会在导航到它的某个子路由时。

Our initial admin routing configuration:

我们的初始管理路由配置如下:

app/admin/admin-routing.module.ts (admin routing)

const adminRoutes: Routes = [ { path: 'admin', component: AdminComponent, children: [ { path: '', children: [ { path: 'crises', component: ManageCrisesComponent }, { path: 'heroes', component: ManageHeroesComponent }, { path: '', component: AdminDashboardComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(adminRoutes) ], exports: [ RouterModule ] }) export class AdminRoutingModule {}

Component-Less Route: grouping routes without a component

无组件路由: 不借助组件对路由进行分组

Looking at our child route under the AdminComponent, we have a route with a path and a children property but it's not using a component. We haven't made a mistake in our configuration, because we can use a component-less route.

来看AdminComponent下的子路由,我们有一个带pathchildren的子路由,但它没有使用component。这并不是配置中的失误,而是在使用无组件路由。

We want to group our Crisis Center management routes under the admin path, but we don't need a component just to group those routes under an additional RouterOutlet. This also allows us to guard child routes.

虽然我们希望对admin路径下的危机中心管理类路由进行分组,但并不需要另一个仅用来分组路由的组件。 这同时也允许我们守卫子路由

Next, we'll import the AdminModule into our app.module.ts and add it to the imports array to register our admin routes.

接下来,我们把AdminModule导入到app.module.ts中,并把它加入imports数组中来注册这些管理类路由。

app/app.module.ts (admin module)

import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisCenterModule } from './crisis-center/crisis-center.module'; import { AdminModule } from './admin/admin.module'; import { DialogService } from './dialog.service'; @NgModule({ imports: [ CommonModule, FormsModule, HeroesModule, CrisisCenterModule, AdminModule, AppRoutingModule ], declarations: [ AppComponent ], providers: [ DialogService ], bootstrap: [ AppComponent ] }) export class AppModule { }

And we add a link to the AppComponent shell that users can click to get to this feature.

然后我们往壳组件AppComponent中添加一个链接,让用户能点击它,以访问该特性。

app/app.component.ts (template)

template: ` <h1 class="title">Angular Router</h1> <nav> <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> <a routerLink="/admin" routerLinkActive="active">Admin</a> </nav> <router-outlet></router-outlet> `

Guard the admin feature

守护“管理特性”区

Currently every route within our Crisis Center is open to everyone. The new admin feature should be accessible only to authenticated users.

现在“危机中心”的每个路由都是对所有人开放的。这些新的管理特性应该只能被已登录用户访问。

We could hide the link until the user logs in. But that's tricky and difficult to maintain.

我们可以在用户登录之前隐藏这些链接,但这样会有点复杂并难以维护。

Instead we'll write a CanActivate guard to redirect anonymous users to the login page when they try to reach the admin component.

我们换种方式:写一个CanActivate守卫,当匿名用户尝试访问管理组件时,把它/她重定向到登录页。

This is a general purpose guard — we can imagine other features that require authenticated users — so we create an auth-guard.service.ts in the application root folder.

这是一种具有通用性的守护目标(通常会有其它特性需要登录用户才能访问),所以我们在应用的根目录下创建一个auth-guard.ts文件。

At the moment we're interested in seeing how guards work so our first version does nothing useful. It simply logs to console and returns true immediately, allowing navigation to proceed:

此刻,我们的兴趣在于看看守卫是如何工作的,所以我们第一个版本没做什么有用的事情。它只是往控制台写日志,并且立即返回true,让导航继续:

app/auth-guard.service.ts (excerpt)

import { Injectable } from '@angular/core'; import { CanActivate } from '@angular/router'; @Injectable() export class AuthGuard implements CanActivate { canActivate() { console.log('AuthGuard#canActivate called'); return true; } }

Next we open admin-routing.module.ts, import the AuthGuard class, and update the admin route with a CanActivate guard property that references it:

接下来,打开crisis-center.routes.ts,导入AuthGuard类,修改管理路由并通过CanActivate属性来引用AuthGuard

app/admin/admin-routing.module.ts (guarded admin route)

import { AuthGuard } from '../auth-guard.service'; const adminRoutes: Routes = [ { path: 'admin', component: AdminComponent, canActivate: [AuthGuard], children: [ { path: '', children: [ { path: 'crises', component: ManageCrisesComponent }, { path: 'heroes', component: ManageHeroesComponent }, { path: '', component: AdminDashboardComponent } ], } ] } ]; @NgModule({ imports: [ RouterModule.forChild(adminRoutes) ], exports: [ RouterModule ] }) export class AdminRoutingModule {}

Our admin feature is now protected by the guard, albeit protected poorly.

我们的管理特性区现在受此守卫保护了,不过这样的保护还不够。

Teach AuthGuard to authenticate

AuthGuard进行认证

Let's make our AuthGuard at least pretend to authenticate.

我们先让AuthGuard至少能“假装”进行认证。

The AuthGuard should call an application service that can login a user and retain information about the current user. Here's a demo AuthService:

AuthGuard可以调用应用中的一项服务,该服务能让用户登录,并且保存当前用户的信息。下面是一个AuthService的示范:

app/auth.service.ts (excerpt)

import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/of'; import 'rxjs/add/operator/do'; import 'rxjs/add/operator/delay'; @Injectable() export class AuthService { isLoggedIn: boolean = false; // store the URL so we can redirect after logging in redirectUrl: string; login(): Observable<boolean> { return Observable.of(true).delay(1000).do(val => this.isLoggedIn = true); } logout(): void { this.isLoggedIn = false; } }

Although it doesn't actually log in, it has what we need for this discussion. It has an isLoggedIn flag to tell us whether the user is authenticated. Its login method simulates an API call to an external service by returning an observable that resolves successfully after a short pause. The redirectUrl property will store our attempted URL so we can navigate to it after authenticating.

虽然它不会真的进行登录,但足够让我们进行这个讨论了。 它有一个isLoggedIn标志,用来标识是否用户已经登录过了。 它的login方法会仿真一个对外部服务的API调用,返回一个可观察对象(observable)。在短暂的停顿之后,这个可观察对象就会解析成功。 redirectUrl属性将会保存在URL中,以便认证完之后导航到它。

Let's revise our AuthGuard to call it.

我们这就修改AuthGuard来调用它。

app/auth-guard.service.ts (v2)

import { Injectable } from '@angular/core'; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { AuthService } from './auth.service'; @Injectable() export class AuthGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { let url: string = state.url; return this.checkLogin(url); } checkLogin(url: string): boolean { if (this.authService.isLoggedIn) { return true; } // Store the attempted URL for redirecting this.authService.redirectUrl = url; // Navigate to the login page with extras this.router.navigate(['/login']); return false; } }

Notice that we inject the AuthService and the Router in the constructor. We haven't provided the AuthService yet but it's good to know that we can inject helpful services into our routing guards.

注意,我们把AuthServiceRouter服务注入到构造函数中。 我们还没有提供AuthService,这里要说明的是:可以往路由守卫中注入有用的服务。

This guard returns a synchronous boolean result. If the user is logged in, it returns true and the navigation continues.

该守卫返回一个同步的布尔值。如果用户已经登录,它就返回true,导航会继续。

The ActivatedRouteSnapshot contains the future route that will be activated and the RouterStateSnapshot contains the future RouterState of our application, should we pass through our guard check.

这个ActivatedRouteSnapshot包含了即将被激活的路由,而RouterStateSnapshot包含了该应用即将到达的状态。 它们要通过我们的守卫进行检查。

If the user is not logged in, we store the attempted URL the user came from using the RouterStateSnapshot.url and tell the router to navigate to a login page — a page we haven't created yet. This secondary navigation automatically cancels the current navigation; we return false just to be clear about that.

如果用户还没有登录,我们会用RouterStateSnapshot.url保存用户来自的URL并让路由器导航到登录页(我们尚未创建该页)。 这间接导致路由器自动中止了这次导航,我们返回false并不是必须的,但这样可以更清楚的表达意图。

Add the LoginComponent

添加LoginComponent

We need a LoginComponent for the user to log in to the app. After logging in, we'll redirect to our stored URL if available, or use the default URL. There is nothing new about this component or the way we wire it into the router configuration.

我们需要一个LoginComponent来让用户登录进这个应用。在登录之后,我们跳转到前面保存的URL,如果没有,就跳转到默认URL。 该组件没有什么新内容,我们把它放进路由配置的方式也没什么新意。

We'll register a /login route in our login-routing.module.ts and add the necessary providers to the providers array. In our app.module.ts, we'll import the LoginComponent and add it to our AppModule declarations. We'll also import and add the LoginRoutingModule to our AppModule imports.

我们将在login-routing.module.ts中注册一个/login路由,并把必要的提供商添加providers数组中。 在app.module.ts中,我们导入LoginComponent并把它加入根模块的declarations中。 同时在AppModule中导入并添加LoginRoutingModule

import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { LoginRoutingModule } from './login-routing.module'; import { LoginComponent } from './login.component'; import { DialogService } from './dialog.service'; @NgModule({ imports: [ BrowserModule, FormsModule, HeroesModule, LoginRoutingModule, AppRoutingModule ], declarations: [ AppComponent, LoginComponent ], providers: [ DialogService ], bootstrap: [ AppComponent ] }) export class AppModule { } import { Component } from '@angular/core'; import { Router } from '@angular/router'; import { AuthService } from './auth.service'; @Component({ template: ` <h2>LOGIN</h2> <p>{{message}}</p> <p> <button (click)="login()" *ngIf="!authService.isLoggedIn">Login</button> <button (click)="logout()" *ngIf="authService.isLoggedIn">Logout</button> </p>` }) export class LoginComponent { message: string; constructor(public authService: AuthService, public router: Router) { this.setMessage(); } setMessage() { this.message = 'Logged ' + (this.authService.isLoggedIn ? 'in' : 'out'); } login() { this.message = 'Trying to log in ...'; this.authService.login().subscribe(() => { this.setMessage(); if (this.authService.isLoggedIn) { // Get the redirect URL from our auth service // If no redirect has been set, use the default let redirect = this.authService.redirectUrl ? this.authService.redirectUrl : '/crisis-center/admin'; // Redirect the user this.router.navigate([redirect]); } }); } logout() { this.authService.logout(); this.setMessage(); } } import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { AuthGuard } from './auth-guard.service'; import { AuthService } from './auth.service'; import { LoginComponent } from './login.component'; const loginRoutes: Routes = [ { path: 'login', component: LoginComponent } ]; @NgModule({ imports: [ RouterModule.forChild(loginRoutes) ], exports: [ RouterModule ], providers: [ AuthGuard, AuthService ] }) export class LoginRoutingModule {}

Guards and the service providers they require must be provided at the module-level. This allows the Router access to retrieve these services from the Injector during the navigation process. The same rule applies for feature modules loaded asynchronously.

它们所需的守卫和服务提供商必须在模块一级提供。这让路由器在导航过程中可以通过Injector来取得这些服务。 同样的规则也适用于异步加载的特性模块。

CanActivateChild: guarding child routes

CanActivateChild: 守卫子路由

As we learned about guarding routes with CanActivate, we can also protect child routes with the CanActivateChild guard. The CanActivateChild guard works similarly to the CanActivate guard, but the difference is its run before each child route is activated. We protected our admin feature module from unauthorized access, but we could also protect child routes within our feature module.

就像我们可以通过CanActivate来守卫路由一样,我们也能通过CanActivateChild守卫来保护子路由。CanActivateChild守卫的工作方式和CanActivate守卫很相似,不同之处在于它会在每个子路由被激活之前运行。我们保护了管理特性模块不受未授权访问,也同样可以在特性模块中保护子路由。

Let's extend our AuthGuard to protect when navigating between our admin routes. First we'll open our auth-guard.service.ts and add CanActivateChild interface to our imported tokens from the router package.

让我们扩展一下AuthGuard,让它能在admin路由之间导航时提供保护。首先,打开auth-guard.serivce.ts并从router包中导入CanActiveChild接口。

Next, we'll implement the canActivateChild method with takes the same arguments as the canActivate method, an ActivatedRouteSnapshot and RouterStateSnapshot. The canActivateChild behaves the same way the other guards do, returning an Observable<boolean> or Promise<boolean> for async checks and boolean for sync checks. We'll return a boolean

然后,我们实现canActivateChild方法,它接收与canActivate方法相同的参数:ActivatedRouteSnapshotRouterStateSnapshotcanActivateChild和其它守卫的行为一样,都返回Observable<boolean>Promise<boolean>以支持异步检查,或返回boolean来支持同步检查。 这里我们直接返回boolean

app/auth-guard.service.ts (excerpt)

import { Injectable } from '@angular/core'; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild } from '@angular/router'; import { AuthService } from './auth.service'; @Injectable() export class AuthGuard implements CanActivate, CanActivateChild { constructor(private authService: AuthService, private router: Router) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { let url: string = state.url; return this.checkLogin(url); } canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { return this.canActivate(route, state); } }

We add the same AuthGuard to our component-less admin route to protect all other child routes at one time instead of adding the AuthGuard to each route individually.

我们往“无组件”的管理路由中添加同一个AuthGuard以同时保护所有子路由,而不是挨个添加它们。

app/admin/admin-routing.module.ts (excerpt)

const adminRoutes: Routes = [ { path: 'admin', component: AdminComponent, canActivate: [AuthGuard], children: [ { path: '', canActivateChild: [AuthGuard], children: [ { path: 'crises', component: ManageCrisesComponent }, { path: 'heroes', component: ManageHeroesComponent }, { path: '', component: AdminDashboardComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(adminRoutes) ], exports: [ RouterModule ] }) export class AdminRoutingModule {}

CanDeactivate: handling unsaved changes

CanDeactivate:处理未保存的更改

Back in the "Heroes" workflow, the app accepts every change to a hero immediately without hesitation or validation.

回到“Heroes”工作流,该应用毫不犹豫的接受对英雄的任何修改,不作任何校验。

In the real world, we might have to accumulate the users changes. We might have to validate across fields. We might have to validate on the server. We might have to hold changes in a pending state until the user confirms them as a group or cancels and reverts all changes.

在现实世界中,我们得先把用户的改动积累起来。 我们可能不得不进行跨字段的校验,可能要找服务器进行校验,可能得把这些改动保存成一种待定状态,直到用户或者把这些改动作为一组进行确认或撤销所有改动。

What do we do about unapproved, unsaved changes when the user navigates away? We can't just leave and risk losing the user's changes; that would be a terrible experience.

当用户要导航到外面时,该怎么处理这些既没有审核通过又没有保存过的改动呢? 我们不能马上离开,不在乎丢失这些改动的风险,那显然是一种糟糕的用户体验。

We'd like to pause and let the user decide what to do. If the user cancels, we'll stay put and allow more changes. If the user approves, the app can save.

我们应该暂停,并让用户决定该怎么做。如果用户选择了取消,我们就留下来,并允许更多改动。如果用户选择了确认,那就进行保存。

We still might delay navigation until the save succeeds. If we let the user move to the next screen immediately and the save failed (perhaps the data are ruled invalid), we would have lost the context of the error.

在保存成功之前,我们还可以继续推迟导航。如果我们让用户立即移到下一个界面,而保存却失败了(可能因为数据不符合有效性规则),我们就会丢失该错误的上下文环境。

We can't block while waiting for the server — that's not possible in a browser. We need to stop the navigation while we wait, asynchronously, for the server to return with its answer.

在等待服务器的答复时,我们没法阻塞它 —— 这在浏览器中是不可能的。 我们只能用异步的方式在等待服务器答复之前先停止导航。

We need the CanDeactivate guard.

我们需要CanDeactivate守卫。

Cancel and Save

取消与保存

Our sample application doesn't talk to a server. Fortunately, we have another way to demonstrate an asynchronous router hook.

我们的范例应用不会与服务器通讯。 幸运的是,我们有另一种方式来演示异步的路由器钩子。

Users update crisis information in the CrisisDetailComponent. Unlike the HeroDetailComponent, the user changes do not update the crisis entity immediately. We update the entity when the user presses the Save button. We discard the changes if the user presses the Cancel button.

用户在CrisisDetailComponent中更新危机信息。 与HeroDetailComponent不同,用户的改动不会立即更新危机的实体对象。当用户按下了Save按钮时,我们就更新这个实体对象;如果按了Cancel按钮,那就放弃这些更改。

Both buttons navigate back to the crisis list after save or cancel.

这两个按钮都会在保存或取消之后导航回危机列表。

app/crisis-center/crisis-detail.component.ts (excerpt)

export class CrisisDetailComponent implements OnInit { @HostBinding('@routeAnimation') get routeAnimation() { return true; } @HostBinding('style.display') get display() { return 'block'; } @HostBinding('style.position') get position() { return 'absolute'; } crisis: Crisis; editName: string; cancel() { this.gotoCrises(); } save() { this.crisis.name = this.editName; this.gotoCrises(); } }

What if the user tries to navigate away without saving or canceling? The user could push the browser back button or click the heroes link. Both actions trigger a navigation. Should the app save or cancel automatically?

如果用户尝试不保存或撤销就导航到外面该怎么办? 用户可以按浏览器的后退按钮,或点击英雄的链接。 这些操作都会触发导航。本应用应该自动保存或取消吗?

We'll do neither. Instead we'll ask the user to make that choice explicitly in a confirmation dialog box that waits asynchronously for the user's answer.

我们两个都不采取。我们应该弹出一个确认对话框来要求用户明确做出选择,该对话框会用异步的方式等用户做出选择

We could wait for the user's answer with synchronous, blocking code. Our app will be more responsive ... and can do other work ... by waiting for the user's answer asynchronously. Waiting for the user asynchronously is like waiting for the server asynchronously.

我们也能用同步的方式等用户的答复,阻塞代码。但如果能用异步的方式等待用户的答复,应用就会响应性更好,也能同时做别的事。异步等待用户的答复和等待服务器的答复是类似的。

The DialogService (provided in the AppModule for app-wide use) does the asking.

DialogService(为了在应用级使用,已经注入到了AppModule)就可以做到这些。

It returns a promise that resolves when the user eventually decides what to do: either to discard changes and navigate away (true) or to preserve the pending changes and stay in the crisis editor (false).

它返回promise,当用户最终决定了如何去做时,它就会被解析 —— 或者决定放弃更改直接导航离开(true),或者保留未完成的修改,留在危机编辑器中(false)。

We create a Guard that will check for the presence of a canDeactivate function in our component, in this case being CrisisDetailComponent. We don't need to know the details of how our CrisisDetailComponent confirms deactivation. This makes our guard reusable, which is an easy win for us.

我们创建了一个Guard,它将检查这个组件中canDeactivate函数的工作现场,在这里,它就是CrisisDetailComponent。我们并不需要知道CrisisDetailComponent确认退出激活状态的详情。这让我们的守卫可以被复用,这是一次轻而易举的胜利。

app/can-deactivate-guard.service.ts

import { Injectable } from '@angular/core'; import { CanDeactivate } from '@angular/router'; import { Observable } from 'rxjs/Observable'; export interface CanComponentDeactivate { canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean; } @Injectable() export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> { canDeactivate(component: CanComponentDeactivate) { return component.canDeactivate ? component.canDeactivate() : true; } }

Alternatively, We could make a component-specific CanDeactivate guard for our CrisisDetailComponent. The canDeactivate method provides us with the current instance of our component, the current ActivatedRoute and RouterStateSnapshot in case we needed to access some external information. This would be useful if we only wanted to use this guard for this component and needed to ask the component's properties in or to confirm whether the router should allow navigation away from it.

另外,我们也可以为CrisisDetailComponent创建一个特定的CanDeactivate守卫。在需要访问外部信息时,canDeactivate方法为提供了组件、ActivatedRouteRouterStateSnapshot的当前实例。如果只想为这个组件使用该守卫,并且需要使用该组件属性、或者需要路由器确认是否允许从该组件导航出去时,这个守卫就非常有用。

app/can-deactivate-guard.service.ts (component-specific)

import { Injectable } from '@angular/core'; import { CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { CrisisDetailComponent } from './crisis-center/crisis-detail.component'; @Injectable() export class CanDeactivateGuard implements CanDeactivate<CrisisDetailComponent> { canDeactivate( component: CrisisDetailComponent, route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Promise<boolean> | boolean { // Get the Crisis Center ID console.log(route.params['id']); // Get the current URL console.log(state.url); // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged if (!component.crisis || component.crisis.name === component.editName) { return true; } // Otherwise ask the user with the dialog service and return its // promise which resolves to true or false when the user decides return component.dialogService.confirm('Discard changes?'); } }

Looking back at our CrisisDetailComponent, we have implemented our confirmation workflow for unsaved changes.

看看CrisisDetailComponent组件,我们已经实现了对未保存的更改进行确认的工作流。

app/crisis-center/crisis-detail.component.ts (excerpt)

canDeactivate(): Promise<boolean> | boolean { // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged if (!this.crisis || this.crisis.name === this.editName) { return true; } // Otherwise ask the user with the dialog service and return its // promise which resolves to true or false when the user decides return this.dialogService.confirm('Discard changes?'); }

Notice that the canDeactivate method can return synchronously; it returns true immediately if there is no crisis or there are no pending changes. But it can also return a Promise or an Observable and the router will wait for that to resolve to truthy (navigate) or falsey (stay put).

注意,canDeactivate方法可以同步返回,如果没有危机,或者没有未定的修改,它就立即返回true。但是它也可以返回一个承诺(Promise)或可观察对象(Observable),路由器将等待它们被解析为真值(继续导航)或假值(留下)。

We add the Guard to our crisis detail route in crisis-center-routing.module.ts using the canDeactivate array.

我们往crisis-center.routing.ts的危机详情路由中用canDeactivate数组添加一个Guard(守卫)。

app/crisis-center/crisis-center-routing.module.ts (can deactivate guard)

import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisCenterHomeComponent } from './crisis-center-home.component'; import { CrisisListComponent } from './crisis-list.component'; import { CrisisCenterComponent } from './crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail.component'; import { CanDeactivateGuard } from '../can-deactivate-guard.service'; const crisisCenterRoutes: Routes = [ { path: '', redirectTo: '/crisis-center', pathMatch: 'full' }, { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard] }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ] }) export class CrisisCenterRoutingModule { }

We also need to add the Guard to our main AppRoutingModule providers so the Router can inject it during the navigation process.

我们还要把这个Guard添加到appRoutingModuleproviders中去,以便Router可以在导航过程中注入它。

import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CanDeactivateGuard } from './can-deactivate-guard.service'; const appRoutes: Routes = [ ]; @NgModule({ imports: [ RouterModule.forRoot(appRoutes) ], exports: [ RouterModule ], providers: [ CanDeactivateGuard ] }) export class AppRoutingModule {}

Now we have given our user a safeguard against unsaved changes.

现在,我们已经给了用户一个能保护未保存更改的安全守卫。

Resolve: pre-fetching component data

解析: 提前获取组件数据

In our Hero Detail and Crisis Detail, we waited until the route was activated to fetch our respective hero or crisis.

Hero DetailCrisis Detail中,它们等待路由读取对应的英雄和危机。

This worked well for us, but we can always do better. If we were using a real world api, there may be some delay in when the data we want to display gets returned. We don't want to display a blank component until the data loads in this situation.

这种方式没有问题,但是它们还有进步的空间。 如果我们在使用真实api,很有可能数据返回有延迟,导致无法即时显示。 在这种情况下,直到数据到达前,显示一个空的组件不是最好的用户体验。

We'd like to pre-fetch data from the server so it's ready the moment our route is activated. We'd also like to handle the situation where our data fails to load or some other error condition occurs. This would help us in our Crisis Center if we navigated to an id that doesn't return a record. We could send the user back to the Crisis List where we only show valid crisis centers. We want to delay rendering of our route component until all necessary data has been fetched or some action has occurred.

可以预先从服务器读取数据,这样在路由器被激活时,数据已经返回。同时,我们还需要处理数据返回失败和其它出错情况。这样,在Crisis Center中,对处理导航到一个无返回数据的id有帮助。 我们可以将用户发回只列出有效危机的Crisis List。 我们需要延迟渲染路由组件,来等待所有必要的数据都成功获取或做一些其他操作。

We need the Resolve guard.

我们需要Resolve守卫。

Fetch data before navigating

导航前预先加载路由信息

We'll update our Crisis Detail route to resolve our Crisis before loading the route, or if the user happens to navigate to an invalid crisis center :id, we'll navigate back to our list of existing crises.

我们需要更新Crisis Detail路由,让它先解析必要的危机,再加载路由。或者当用户导航到一个无效的危机:id时,将它们导航回危机列表。

Like the CanActivate and CanDeactivate guards, the Resolve guard is an interface we can implement as a service to resolve route data synchronously or asynchronously. In CrisisDetailComponent, we used the ngOnInit to retrieve the Crisis information. We also navigated the user away from the route if the Crisis was not found. It would be more efficient to perform this action before the route is ever activated.

CanActivateCanDeactivate守卫一样,服务可以实现Resolve守卫接口来同步或异步解析路由数据。 Crisis Detail组件使用ngOnInit来获取Crisis信息。如果Crisis找不到,用户会被导航出去。在路由被激活之前就处理这些情况会更加有效。

We'll create a CrisisDetailResolve service that will handle retrieving the Crisis and navigating the user away if the Crisis does not exist. Then we can be assured that when we activate the CrisisDetailComponent, the associated Crisis will already be available for display.

现在创建一个CrisisDetailResolve服务,用它来处理Crisis数据读取和在Crisis不存在时将用户导航出去。 然后可以确保当激活CrisisDetailComponent时,关联的Crisis已经为显示准备妥当。

Let's create our crisis-detail-resolve.service.ts file within our Crisis Center feature area.

下面在Crisis Center特征区新建的crisis-detail-resolve.service.ts文件:

app/crisis-center/crisis-detail-resolve.service.ts

import { Injectable } from '@angular/core'; import { Router, Resolve, ActivatedRouteSnapshot } from '@angular/router'; import { Crisis, CrisisService } from './crisis.service'; @Injectable() export class CrisisDetailResolve implements Resolve<Crisis> { constructor(private cs: CrisisService, private router: Router) {} resolve(route: ActivatedRouteSnapshot): Promise<Crisis>|boolean { let id = route.params['id']; return this.cs.getCrisis(id).then(crisis => { if (crisis) { return crisis; } else { // id not found this.router.navigate(['/crisis-center']); return false; } }); } }

We'll take the relevant parts of the ngOnInit lifecycle hook in our CrisisDetailComponent and move them into our CrisisDetailResolve guard. We import the Crisis model and CrisisService and also the Router for navigation from our resolve implementation. We want to be explicit about the data we are resolving, so we implement the Resolve interface with a type of Crisis. This lets us know that what we will resolve will match our Crisis model. We inject the CrisisService and Router and implement the resolve method that supports a Promise, Observable or a synchronous return value.

接下来,将CrisisDetailComponentngOnInit生命周期钩子里面相关的部分移动到CrisisDetailResolve守卫里面,然后导入Crisis模型,CrisisService服务和Router。 为了特殊指定什么样的数据需要解析,我们在Resolve接口的实现上指定了Crisis类型。这样告诉我们解析的结果将于Crisis模型对应。 然后注入CrisisServiceRouter,并实现支持PromiseObservable和异步返回值resolve方法。

We'll use our CrisisService.getCrisis method that returns a promise to prevent our route from loading until the data is fetched. If we don't find a valid Crisis, we navigate the user back to the CrisisList, canceling the previous in-flight navigation to the crisis details.

我们使用CrisisService.getCrisis方法来获取一个承诺对象,用于防止路由在成功获取数据之前被加载。如果没有找到对应Crisis,便将用户导航回CrisisList,取消之前导航到危机详情的路由。

Now that our guard is ready, we'll import it in our crisis-center-routing.module.ts and use the resolve object in our route configuration.

解析守卫现在准备好了,将它导入到crisis-center-routing.module.ts中,然后在路由配置中设置resolve对象。

We'll add the CrisisDetailResolve service to our CrisisCenterRoutingModule's providers, so its available to the Router during the navigation process.

接下来,将CrisisDetailResolve服务添加到危机中心路由模块的providers数组中,这样Router在路由过程中可以使用它。

app/crisis-center/crisis-center-routing.module.ts (resolve)

import { CrisisDetailResolve } from './crisis-detail-resolve.service'; const crisisCenterRoutes: Routes = [ { path: '', redirectTo: '/crisis-center', pathMatch: 'full' }, { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard], resolve: { crisis: CrisisDetailResolve } }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ], providers: [ CrisisDetailResolve ] }) export class CrisisCenterRoutingModule { }

Now that we've added our Resolve guard to fetch data before the route loads, we no longer need to do this once we get into our CrisisDetailComponent. We'll update the CrisisDetailComponent to use the ActivatedRoute data, which is where our crisis property from our Resolve guard will be provided. Once activated, all we need to do is set our local crisis and editName properties from our resolved Crisis information. The Crisis is being provided at the time the route component is activated.

因为添加了Resolve守卫并用它在加载路由之前读取数据,所以在加载CrisisDetailComponent后,不再需要读取数据。 因此,可以更新CrisisDetailComponent组件,让它使用Resolve守卫通过crisis属性提供的ActivatedRoute.data。 一旦激活CrisisDetailComponent,我们可以使用解析过的Crisis信息赋值本地属性crisiseditName。 我们也不再需要通过订阅和反订阅ActivatedRoute的参数来获取Crisis,因为它已经在路由组件被激活时被同步提供。

app/crisis-center/crisis-detail.component.ts (ngOnInit v2)

ngOnInit() { this.route.data .subscribe((data: { crisis: Crisis }) => { this.editName = data.crisis.name; this.crisis = data.crisis; }); }

Two critical points

两个关键点

  1. The router interface is optional. We don't inherit from a base class. We simply implement the interface method or not.

  2. 路由器接口是可选的。我们不必从基类中继承它,只需要实现这个接口方法或不实现。

  3. We rely on the router to call the guard. We don't worry about all the ways that the user could navigate away. That's the router's job. We simply write this class and let the router take it from there.

  4. 我们依赖路由器调用此守卫。不必关心用户用哪种方式导航离开,这是路由器的工作。我们只要写出这个类,等路由器从那里取出它就可以了。

The relevant Crisis Center code for this milestone is

本里程碑中与危机中心有关的代码如下:

import { Component } from '@angular/core'; @Component({ selector: 'my-app', template: ` <h1 class="title">Angular Router</h1> <nav> <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> <a routerLink="/admin" routerLinkActive="active">Admin</a> <a routerLink="/login" routerLinkActive="active">Login</a> </nav> <router-outlet></router-outlet> ` }) export class AppComponent { } // #docplaster import { Component } from '@angular/core'; @Component({ template: ` <p>Welcome to the Crisis Center</p> ` }) export class CrisisCenterHomeComponent { } // #docplaster import { Component } from '@angular/core'; @Component({ template: ` <h2>CRISIS CENTER</h2> <router-outlet></router-outlet> ` }) export class CrisisCenterComponent { } import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisCenterHomeComponent } from './crisis-center-home.component'; import { CrisisListComponent } from './crisis-list.component'; import { CrisisCenterComponent } from './crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail.component'; import { CanDeactivateGuard } from '../can-deactivate-guard.service'; import { CrisisDetailResolve } from './crisis-detail-resolve.service'; const crisisCenterRoutes: Routes = [ { path: '', redirectTo: '/crisis-center', pathMatch: 'full' }, { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard], resolve: { crisis: CrisisDetailResolve } }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ], providers: [ CrisisDetailResolve ] }) export class CrisisCenterRoutingModule { } import 'rxjs/add/operator/switchMap'; import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router, Params } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { Crisis, CrisisService } from './crisis.service'; @Component({ template: ` <ul class="items"> <li *ngFor="let crisis of crises | async" [class.selected]="isSelected(crisis)" (click)="onSelect(crisis)"> <span class="badge">{{ crisis.id }}</span> {{ crisis.name }} </li> </ul> <router-outlet></router-outlet> ` }) export class CrisisListComponent implements OnInit { crises: Observable<Crisis[]>; selectedId: number; constructor( private service: CrisisService, private route: ActivatedRoute, private router: Router ) {} isSelected(crisis: Crisis) { return crisis.id === this.selectedId; } ngOnInit() { this.crises = this.route.params .switchMap((params: Params) => { this.selectedId = +params['id']; return this.service.getCrises(); }); } onSelect(crisis: Crisis) { this.selectedId = crisis.id; // Navigate with relative link this.router.navigate([crisis.id], { relativeTo: this.route }); } } import { Component, OnInit, HostBinding, trigger, transition, animate, style, state } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; import { Crisis } from './crisis.service'; import { DialogService } from '../dialog.service'; @Component({ template: ` <div *ngIf="crisis"> <h3>"{{ editName }}"</h3> <div> <label>Id: </label>{{ crisis.id }}</div> <div> <label>Name: </label> <input [(ngModel)]="editName" placeholder="name"/> </div> <p> <button (click)="save()">Save</button> <button (click)="cancel()">Cancel</button> </p> </div> `, styles: ['input {width: 20em}'], animations: [ trigger('routeAnimation', [ state('*', style({ opacity: 1, transform: 'translateX(0)' }) ), transition(':enter', [ style({ opacity: 0, transform: 'translateX(-100%)' }), animate('0.2s ease-in') ]), transition(':leave', [ animate('0.5s ease-out', style({ opacity: 0, transform: 'translateY(100%)' })) ]) ]) ] }) export class CrisisDetailComponent implements OnInit { @HostBinding('@routeAnimation') get routeAnimation() { return true; } @HostBinding('style.display') get display() { return 'block'; } @HostBinding('style.position') get position() { return 'absolute'; } crisis: Crisis; editName: string; constructor( private route: ActivatedRoute, private router: Router, public dialogService: DialogService ) {} ngOnInit() { this.route.data .subscribe((data: { crisis: Crisis }) => { this.editName = data.crisis.name; this.crisis = data.crisis; }); } cancel() { this.gotoCrises(); } save() { this.crisis.name = this.editName; this.gotoCrises(); } canDeactivate(): Promise<boolean> | boolean { // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged if (!this.crisis || this.crisis.name === this.editName) { return true; } // Otherwise ask the user with the dialog service and return its // promise which resolves to true or false when the user decides return this.dialogService.confirm('Discard changes?'); } gotoCrises() { let crisisId = this.crisis ? this.crisis.id : null; // Pass along the crisis id if available // so that the CrisisListComponent can select that crisis. // Add a totally useless `foo` parameter for kicks. // Relative navigation back to the crises this.router.navigate(['../', { id: crisisId, foo: 'foo' }], { relativeTo: this.route }); } } import { Injectable } from '@angular/core'; import { Router, Resolve, ActivatedRouteSnapshot } from '@angular/router'; import { Crisis, CrisisService } from './crisis.service'; @Injectable() export class CrisisDetailResolve implements Resolve<Crisis> { constructor(private cs: CrisisService, private router: Router) {} resolve(route: ActivatedRouteSnapshot): Promise<Crisis>|boolean { let id = route.params['id']; return this.cs.getCrisis(id).then(crisis => { if (crisis) { return crisis; } else { // id not found this.router.navigate(['/crisis-center']); return false; } }); } } export class Crisis { constructor(public id: number, public name: string) { } } const CRISES = [ new Crisis(1, 'Dragon Burning Cities'), new Crisis(2, 'Sky Rains Great White Sharks'), new Crisis(3, 'Giant Asteroid Heading For Earth'), new Crisis(4, 'Procrastinators Meeting Delayed Again'), ]; let crisesPromise = Promise.resolve(CRISES); import { Injectable } from '@angular/core'; @Injectable() export class CrisisService { static nextCrisisId = 100; getCrises() { return crisesPromise; } getCrisis(id: number | string) { return crisesPromise .then(crises => crises.find(crisis => crisis.id === +id)); } } import { Injectable } from '@angular/core'; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild } from '@angular/router'; import { AuthService } from './auth.service'; @Injectable() export class AuthGuard implements CanActivate, CanActivateChild { constructor(private authService: AuthService, private router: Router) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { let url: string = state.url; return this.checkLogin(url); } canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { return this.canActivate(route, state); } checkLogin(url: string): boolean { if (this.authService.isLoggedIn) { return true; } // Store the attempted URL for redirecting this.authService.redirectUrl = url; // Navigate to the login page this.router.navigate(['/login']); return false; } } import { Injectable } from '@angular/core'; import { CanDeactivate } from '@angular/router'; import { Observable } from 'rxjs/Observable'; export interface CanComponentDeactivate { canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean; } @Injectable() export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> { canDeactivate(component: CanComponentDeactivate) { return component.canDeactivate ? component.canDeactivate() : true; } }

Query Parameters and Fragments

查询参数及片段

In our route parameters example, we only dealt with parameters specific to our route, but what if we wanted optional parameters available to all routes? This is where our query parameters come into play and serve a special purpose in our application.

在这个查询参数例子中,我们只为路由指定了参数,但是该如何定义一些所有路由中都可用的可选参数呢? 要达到这个目的,该“查询参数”大显身手了。

Fragments refer to certain elements on the page identified with an id attribute.

片段可以引用页面中带有特定id属性的元素.

We'll update our AuthGuard to provide a session_id query that will remain after navigating to another route.

接下来,我们将更新AuthGuard来提供session_id查询参数,在导航到其它路由后,它还会存在。

We'll also provide an arbitrary anchor fragment, which we would use to jump to a certain point on our page.

我们还将随意提供一个锚点片段,它用来跳转到页面中指定的位置。

We'll add the NavigationExtras object to our router.navigate method that navigates us to our /login route.

我们还将为router.nativate方法传入一个NavigationExtras对象,用来导航到/login路由。

app/auth-guard.service.ts (v3)

import { Injectable } from '@angular/core'; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild, NavigationExtras } from '@angular/router'; import { AuthService } from './auth.service'; @Injectable() export class AuthGuard implements CanActivate, CanActivateChild { constructor(private authService: AuthService, private router: Router) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { let url: string = state.url; return this.checkLogin(url); } canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { return this.canActivate(route, state); } checkLogin(url: string): boolean { if (this.authService.isLoggedIn) { return true; } // Store the attempted URL for redirecting this.authService.redirectUrl = url; // Create a dummy session id let sessionId = 123456789; // Set our navigation extras object // that contains our global query params and fragment let navigationExtras: NavigationExtras = { queryParams: { 'session_id': sessionId }, fragment: 'anchor' }; // Navigate to the login page with extras this.router.navigate(['/login'], navigationExtras); return false; } }

We can also preserve query parameters and fragments across navigations without having to re-provide them when navigating. In our LoginComponent, we'll add an object as the second argument in our router.navigate function and provide the preserveQueryParams and preserveFragment to pass along the current query parameters and fragment to the next route.

还可以再导航之间保留查询参数和片段,而无需再次再导航中提供。在LoginComponent中的router.navigate方法中,添加第二个参数,该对象提供了preserveQueryParamspreserveFragment,用于传递到当前的查询参数中并为下一个路由提供片段。

app/login.component.ts (preserve)

// Set our navigation extras object // that passes on our global query params and fragment let navigationExtras: NavigationExtras = { preserveQueryParams: true, preserveFragment: true }; // Redirect the user this.router.navigate([redirect], navigationExtras);

Since we'll be navigating to our Admin Dashboard route after logging in, we'll update it to handle our query parameters and fragment.

由于要在登录后导航到危机管理特征区的路由,所以我们还得更新它,来处理这些全局查询参数和片段。

app/admin/admin-dashboard.component.ts (v2)

import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/map'; @Component({ template: ` <p>Dashboard</p> <p>Session ID: {{ sessionId | async }}</p> <a id="anchor"></a> <p>Token: {{ token | async }}</p> ` }) export class AdminDashboardComponent implements OnInit { sessionId: Observable<string>; token: Observable<string>; constructor(private route: ActivatedRoute) {} ngOnInit() { // Capture the session ID if available this.sessionId = this.route .queryParams .map(params => params['session_id'] || 'None'); // Capture the fragment if available this.token = this.route .fragment .map(fragment => fragment || 'None'); } }

Query Parameters and Fragments are also available through the ActivatedRoute service available to route components. Just like our route parameters, query parameters and fragments are provided as an Observable. For our updated Crisis Admin component we'll feed the Observable directly into our template using the AsyncPipe, which will handle unsubscribing from the Observable for us when the component is destroyed.

查询参数片段可通过Router服务的routerState属性使用。和路由参数类似,全局查询参数和片段也是Observable对象。 在更新过的英雄管理组件中,我们将直接把Observable传给模板,借助AsyncPipe在组件被销毁时自动取消Observable的订阅。

pop out the window

When running in plunker, pop out the preview window by clicking the blue 'X' button in the upper right corner.

当在plunker中运行时,可以点击右上角的蓝色'X'按钮来弹出预览窗口。

Following the steps in this process, we can click on the Admin button, that takes us to the Login page with our provided query params and fragment. After we click the login button, we notice that we have been redirected to the Admin Dashboard page with our query params and fragment still intact. We can use these persistent bits of information for things that need to be provided with across pages interaction like authentication tokens or session ids.

按照下列步骤试验下:点击Crisis Admin按钮,它会带着我们提供的“查询参数”和“片段”跳转到登录页。 点击登录按钮,我们就会被带到Crisis Admin页,仍然带着上一步提供的“查询参数”和“片段”。 我们可以用这些持久化信息来携带需要为每个页面都提供的信息,如认证令牌或会话的ID等。

The query params and fragment can also be preserved using a RouterLink with the preserveQueryParams and preserveFragment bindings respectively.

“查询参数”和“片段”也可以分别用RouterLink中的preserveQueryParamspreserveFragment保存。

Milestone #6: Asynchronous Routing

里程碑5:异步路由

As we have completed our milestones, our application has naturally gotten larger. As we continue to build out feature areas our overall application size will get larger also. At some point we'll reach a tipping point in where our application takes a significant enough time to load. This is not a viable long term solution.

完成上面的里程碑后,我们的应用程序很自然的长大了。在继续构建特征区的过程中,应用的尺寸将会变得更大。在某一个时间点,我们将达到一个顶点,应用 将会需要过多的时间来加载。长远来看,这不是一个办法。

So how do we combat this problem? We introduce asynchronous routing into our application and take advantage of loading feature areas lazily. This buys us multiple things:

如何才能解决这个问题呢?我们引进了异步路由到应用程序中,并获得懒惰加载特征区域的能力。这样给我们带来了下列好处:

These are all things we want to have in our application, so let's apply this to our current setup. We've already made great strides by organizing our application into four modules: AppModule, HeroesModule, AdminModule and CrisisCenterModule. Our AdminModule is the area of our application that would be scoped to a small set of users, so we'll take advantage of asynchronous routing and only load the Admin feature area when requested.

我们接下来在当前的项目中添加这些特征。现在已经有一系列模块将应用组织为四大块:AppModule, HeroesModule, AdminModuleCrisisCenterModuleAdminModule在我们的应用中只被小部分用户访问, 所以我们利用异步路由来实现只有在请求时才加载Admin特性区域。

Lazy-Loading route configuration

惰性加载路由配置

We'll start by adding an admin route to our app-routing.module.ts file. We want to load our Admin module asynchronously, so we'll use the loadChildren property in our route config where previously we used the children property to include our child routes.

首先,把admin路由添加到 app-routing.module.ts文件中。我们想要异步加载Admin模块, 就得在路由配置中使用loadChildren属性,而以前我们已经用children属性包含进了这些子路由。

We'll also change our admin path in our admin-routing.module.ts to an empty path. The Router supports empty path routes, which we can use for grouping routes together without adding anything additional paths to the URL. Our users will still visit /admin and our AdminComponent still serves as our Routing Component which contains our child routes.

接下来,在admin.routing.ts中,把adminpath更改为空路径。路由器支持空路径路由,它可以在不必把别的路径添加到URL中的情况下,将多个路由组合到一起。用户还是可以访问/crisis-centerCrisisCenterComponent组件还是包含了子级路由的路由组件

const appRoutes: Routes = [ { path: 'admin', loadChildren: 'app/admin/admin.module#AdminModule', } ]; @NgModule({ imports: [ RouterModule.forRoot(appRoutes) ], exports: [ RouterModule ], providers: [ CanDeactivateGuard ] }) export class AppRoutingModule {} import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { AdminComponent } from './admin.component'; import { AdminDashboardComponent } from './admin-dashboard.component'; import { ManageCrisesComponent } from './manage-crises.component'; import { ManageHeroesComponent } from './manage-heroes.component'; import { AuthGuard } from '../auth-guard.service'; const adminRoutes: Routes = [ { path: '', component: AdminComponent, canActivate: [AuthGuard], children: [ { path: '', canActivateChild: [AuthGuard], children: [ { path: 'crises', component: ManageCrisesComponent }, { path: 'heroes', component: ManageHeroesComponent }, { path: '', component: AdminDashboardComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(adminRoutes) ], exports: [ RouterModule ] }) export class AdminRoutingModule {}

The loadChildren property is used by the Router to map to our bundle we want to lazy-load, in this case being the AdminModule.

路由器用loadChildren属性来映射我们希望惰性加载的捆文件,这里是AdminModule

If we look closer at the loadChildren string, we can see that it maps directly to our admin.module.ts file where we previously built out our Admin feature area. After the path to the file we use a # to denote where our file path ends and to tell the Router the name of our AdminModule. If we look in our admin.module.ts file, we can see it matches name of our exported module class.

仔细看loadChildren字符串,就会发现它直接映射到了我们以前在管理特性区构建的admin.module.ts文件。在文件路径后面,我们使用#来标记出文件路径的末尾,并告诉路由器AdminModule的名字。打开admin.module.ts文件,我们就会看到它正是我们所导出的模块类的名字。

app/admin/admin.module.ts (export)

export class AdminModule {}

The loadChildren property is used by the Router to map to our bundle we want to lazy-load, in this case being the AdminModule. The router will take our loadChildren string and dynamically load in our AdminModule, add its routes to our configuration dynamically and then load the requested route. This will only happen when the route is first requested and the module will be immediately be available for subsequent requests.

路由器用loadChildren属性来映射我们希望惰性加载的捆文件,这里是AdminModule。路由器将接收我们的loadChildren字符串,并把它动态加载进AdminModule,它的路由被动态合并到我们的配置中,然后加载所请求的路由。但只有在首次加载该路由时才会这样做,后续的请求都会立即完成。

Angular provides a built-in module loader that supports SystemJS to load modules asynchronously. If we were using another bundling tool, such as Webpack, we would use the Webpack mechanism for asynchronously loading modules.

Angular提供一个内置模块加载器,支持SystemJS来异步加载模块。如果我们使用其它捆绑工具比如Webpack,则使用Webpack的机制来异步加载模块。

We've built our feature area, we've updated our route configuration to take advantage of lazy-loading, now we have to do the final step to break our AdminModule into a completely separate module. In our app.module.ts, we'll remove our AdminModule from the imports array since we'll be loading it on-demand an we'll remove the imported AdminModule.

我们构建了特性区,更新了路由配置来实现惰性加载,现在该做最后一步:将AdminModule分离到一个彻底独立的模块。因为现在按需加载AdminModule,所以在app.module.ts中,从imports数组中删除它。

app/app.module.ts (async admin module)

import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisCenterModule } from './crisis-center/crisis-center.module'; import { LoginRoutingModule } from './login-routing.module'; import { LoginComponent } from './login.component'; import { DialogService } from './dialog.service'; @NgModule({ imports: [ BrowserModule, FormsModule, HeroesModule, CrisisCenterModule, LoginRoutingModule, AppRoutingModule ], declarations: [ AppComponent, LoginComponent ], providers: [ DialogService ], bootstrap: [ AppComponent ] }) export class AppModule { }

CanLoad Guard: guarding against loading of feature modules

CanLoad守卫: 保护特性模块的加载

We're already protecting our AdminModule with a CanActivate guard that prevents the user from accessing the admin feature area unless authorized. We're currently loading the admin routing asynchronously when requested, checking the user access and redirecting to the login page if not authorized. Ideally, we only want to load the AdminModule if the user is logged in and prevent the AdminModule and its routing from being loaded until then.

我们已经使用CanAcitvate保护AdminModule了,它会阻止对管理特性区的匿名访问。我们在请求时可以异步加载管理类路由,检查用户的访问权,如果用户未登录,则跳转到登陆页面。但更理想的是,我们只在用户已经登录的情况下加载AdminModule,并且直到加载完才放行到它的路由。

The CanLoad guard covers this scenario.

CanLoad守卫适用于这个场景。

We can use the CanLoad guard to only load the AdminModule once the user is logged in and attempts to access the admin feature area. We'll update our existing AuthGuard to support the CanLoad guard. We'll import the CanLoad interface and the Route the guard provides when called that contains the requested path.

我们可以用CanLoad守卫来保证只在用户已经登录并尝试访问管理特性区时才加载一次AdminModule。我们这就升级AuthGuard来支持CanLoad守卫。我们先导入CanLoad接口,它被调用时守卫会提供一个Route参数,其中包含所请求的路径。

We'll add the interface to our service, and then we'll implement the interface. Since our AuthGuard already checks the user's logged in state, we can pass that access check to our canLoad method. The Route in the canLoad method provides a path which comes from our route configuration.

我们还要把此接口加入到服务中,并实现它。由于我们的AuthGuard已经能检查用户的登录状态了,所以把canLoad方法的权限检查工作直接转给它。 canLoad方法中的Route参数提供了一个路径,它来自我们的路由配置。

app/auth-guard.service.ts (can load guard)

import { Injectable } from '@angular/core'; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild, NavigationExtras, CanLoad, Route } from '@angular/router'; import { AuthService } from './auth.service'; @Injectable() export class AuthGuard implements CanActivate, CanActivateChild, CanLoad { constructor(private authService: AuthService, private router: Router) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { let url: string = state.url; return this.checkLogin(url); } canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { return this.canActivate(route, state); } canLoad(route: Route): boolean { let url = `/${route.path}`; return this.checkLogin(url); } checkLogin(url: string): boolean { if (this.authService.isLoggedIn) { return true; } // Store the attempted URL for redirecting this.authService.redirectUrl = url; // Create a dummy session id let sessionId = 123456789; // Set our navigation extras object // that contains our global query params and fragment let navigationExtras: NavigationExtras = { queryParams: { 'session_id': sessionId }, fragment: 'anchor' }; // Navigate to the login page with extras this.router.navigate(['/login'], navigationExtras); return false; } }

Next, we'll import the AuthGuard into our app-routing.module.ts and add the AuthGuard to the canLoad array for our admin route. Now our admin feature area is only loaded when the proper access has been granted.

接下来,我们就把AuthGuard导入到app.routing.ts中,并把AuthGuard添加到admin路由的canLoad数组中。现在admin特性区就只有当获得访问授权时才会被加载了。

app/app-routing.module.ts (can load guard)

import { AuthGuard } from './auth-guard.service'; const appRoutes: Routes = [ { path: 'admin', loadChildren: 'app/admin/admin.module#AdminModule', canLoad: [AuthGuard] } ]; @NgModule({ imports: [ RouterModule.forRoot(appRoutes) ], exports: [ RouterModule ], providers: [ CanDeactivateGuard ] }) export class AppRoutingModule {}

Pre-Loading: background loading of feature areas

预加载: 在后台加载特征区域

We've learned how to load modules on-demand, but we can also take advantage of loading feature areas modules in advance. The Router supports pre-loading of asynchronous feature areas prior to navigation to their respective URL. Pre-loading allows us to to load our initial route quickly, while other feature modules are loaded in the background. Once we navigate to those areas, they will have already been loaded as if they were included in our initial bundle.

我们已经学会了如何按需加载模块,我们还可以预先加载特征区域。路由器支持在导航到特征区域URL之前,异步预加载它们。 预加载允许我们在快速加载初始路由的同时,在后台加载其他特征模块。当导航到这些区域时,它们已经被加载了,就像它们被包含在初始包中一样。

Each time a successful navigation happens, the Router will look through our configuration for lazy loaded feature areas and react based on the provided strategy.

每次导航成功发生时,路由器将查看惰性加载的特征区域的配置,并根据提供的策略作出反应。

The Router supports two pre-loading strategies by default:

路由器默认支持两种预加载策略:

The Router also supports custom preloading strategies for fine control over which modules to pre-load.

路由器还支持自定义预加载策略,用来精细控制预加载。

We'll update our CrisisCenterModule to be loaded lazily by default and use the PreloadAllModules strategy to load all lazy loaded modules as soon as possible.

我们将更新CrisisCenterModule,让它默认惰性加载并使用PreloadAllModules策略来尽快加载所有惰性加载模块。

The PreloadAllModules strategy does not load feature areas protected by a CanLoad guard and this is by design. The CanLoad guard blocks loading of feature module assets until authorized to do so. If you want to both preload a module and guard against unauthorized access, use the CanActivate guard instead.

PreloadAllModules策略不会加载被CanLoad守卫保护的特征区域,这是因为Angular是这样设计的。 CanLoad守卫阻挡加载特征模块资源,直到授权为止。如果你希望预加载一个模块并保护未授权访问,使用CanActivate守卫。

We'll update our route configuration to lazy load the CrisisCenterModule. We follow the same process as we did when we loaded the AdminModule asynchronously. In the crisis-center-routing.module.ts, we'll change the crisis-center path to an empty path route.

使用与加载异步AdminModule一样流程,我们将更新路由配置,来惰性加载CrisisCenterModule。 在crisis-center-routing.module.ts中,将crisis-center的路径修改为空路径

We'll move our redirect and crisis-center route to our AppRoutingModule routes and use the loadChildren string to load the CrisisCenterModule. The redirect is also changed to load the /heroes route on initial load.

接下来,将redirectcrisis-center路由移动到AppRoutingModule路由,并使用loadChildren字符串来加载CrisisCenterModuleredirect也被修改为初始加载/heroes路由。

Once we're finished, we'll remove the CrisisCenterModule from our AppModule's imports.

然后将CrisisCenterModule移到AppModuleimports中。

Here are the updated modules before enabling preload:

下面是打开预加载之前的模块修改版:

import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { LoginRoutingModule } from './login-routing.module'; import { LoginComponent } from './login.component'; import { DialogService } from './dialog.service'; @NgModule({ imports: [ BrowserModule, FormsModule, HeroesModule, LoginRoutingModule, AppRoutingModule ], declarations: [ AppComponent, LoginComponent ], providers: [ DialogService ], bootstrap: [ AppComponent ] }) export class AppModule { } import { NgModule } from '@angular/core'; import { RouterModule, Routes, } from '@angular/router'; import { CanDeactivateGuard } from './can-deactivate-guard.service'; import { AuthGuard } from './auth-guard.service'; const appRoutes: Routes = [ { path: 'admin', loadChildren: 'app/admin/admin.module#AdminModule', canLoad: [AuthGuard] }, { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: 'crisis-center', loadChildren: 'app/crisis-center/crisis-center.module#CrisisCenterModule' } ]; @NgModule({ imports: [ RouterModule.forRoot( appRoutes ) ], exports: [ RouterModule ], providers: [ CanDeactivateGuard ] }) export class AppRoutingModule {} import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisCenterHomeComponent } from './crisis-center-home.component'; import { CrisisListComponent } from './crisis-list.component'; import { CrisisCenterComponent } from './crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail.component'; import { CanDeactivateGuard } from '../can-deactivate-guard.service'; import { CrisisDetailResolve } from './crisis-detail-resolve.service'; const crisisCenterRoutes: Routes = [ { path: '', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard], resolve: { crisis: CrisisDetailResolve } }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ], providers: [ CrisisDetailResolve ] }) export class CrisisCenterRoutingModule { }

The second argument in the RouterModule.forRoot method takes an object for additional configuration options. We import the PreloadAllModules token from the router package and set the configuration option's preloadingStrategy property with this PreloadAllModules token. This tells the built-in Router pre-loader to immediately load all unguarded feature areas that use loadChildren.

RouterModule.forRoot方法的第二个参数接受一个附加配置选项对象。 我们从路由器包导入PreloadAllModules令牌,并将这个配置选项的preloadingStrategy属性设置为PreloadAllModules令牌。 这样,内置的路由器立刻预加载所有使用loadChildren未受保护的特征区域。

app/app-routing.module.ts (preload all)

import { NgModule } from '@angular/core'; import { RouterModule, Routes, PreloadAllModules } from '@angular/router'; import { CanDeactivateGuard } from './can-deactivate-guard.service'; import { AuthGuard } from './auth-guard.service'; const appRoutes: Routes = [ { path: 'admin', loadChildren: 'app/admin/admin.module#AdminModule', canLoad: [AuthGuard] }, { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: 'crisis-center', loadChildren: 'app/crisis-center/crisis-center.module#CrisisCenterModule' } ]; @NgModule({ imports: [ RouterModule.forRoot( appRoutes , { preloadingStrategy: PreloadAllModules } ) ], exports: [ RouterModule ], providers: [ CanDeactivateGuard ] }) export class AppRoutingModule {}

Now when we visit http://localhost:3000, the /heroes route will load in the foreground, while the CrisisCenterModule and any other asynchronous feature modules are eagerly loaded in the background, waiting for us to navigate to them.

现在,访问 http://localhost:3000 时,/heroes路由将在前台加载,同时,CrisisCenterModule和其他异步特征模块将在后台被主动加载,等待我们导航到它们。

Custom Pre-Loading Strategy

自定义预加载策略

Pre-loading all modules works well in some situations, but in some cases we need more control over what gets loaded eagerly. This becomes more clear as we load our application on a mobile device, or a low bandwidth connection. We may only want to preload certain feature modules based on user metrics or other data points we gather over time. The Router lets us have more control with a custom preloading strategy.

在一些情况下,预加载所有模块很合适。但是在另外一些用例中,我们需要选择主动加载哪些模块。在移动设备上或者网速很低的情况下加载应用显得尤其明显。 根据用户指标或者逐步收集的数据,我们可能只想预加载某些特征模块。路由器通过自定义预加载策略为我们提供了更多控制。

We can define our own strategy the same way the PreloadAllModules modules strategy was provided to our RouterModule.forRoot configuration object.

使用将PreloadAllModules模块策略提供给RouterModule.forRoot配置对象一样方式,我们可以定义自己的策略。

Since we want to take advantage of this, we'll add a custom strategy that only preloads the modules we select. We'll enable the preloading by using the Route Data, which, as we learned, is an object to store arbitrary route data and and resolve data.

因为想利用这点,我们将添加自定义策略,预加载我们选择的模块。为了启用预加载,我们使用Route Data,正如我们学过的,它是储存的路由数据和解析数据对象。

We'll add a custom preload boolean to our crisis-center route data that we'll use with our custom strategy. To see it in action, we'll add the route.path to the preloadedModules array in our custom strategy service. We'll also log a message to the console for the preloaded module.

我们将自定义preload布尔值添加到crisis-center路由数据,自定义策略将使用它。然后在自定义策略服务中将route.path添加到preloadedModules数组。 我们还将在控制台中为预加载模块输出一条消息。

app/app-routing.module.ts (route data preload)

{ path: 'crisis-center', loadChildren: 'app/crisis-center/crisis-center.module#CrisisCenterModule', data: { preload: true } }

To create our custom strategy we'll need to implement the abstract PreloadingStrategy class and the preload method. The preload method is called for each route that loads its feature module asynchronously and determines whether to preload it. The preload method takes two arguments, the first being the Route that provides the route configuration and a function that preloads the feature module.

为了创建自定义策略,我们将需要实现抽象类PreloadingStrategypreload方法。在异步加载特征模块和决定是否预加载它们时,路由器调用preload方法。 preload方法有两个参数,第一个参数Route提供路由配置,第二个参数是预加载特征模块的函数。

We'll name our strategy PreloadSelectedModules since we only want to preload based on certain criteria. Our custom strategy looks for the preload boolean value in our Route Data and if its true, it calls the load function provided by the built-in Router pre-loader that eagerly loads feature modules.

我们将命名自己的策略为PreloadSelectedModules,因为我们想加载符合特定条件的模块。 自定义策略在Route Data中查询preload布尔值,如果它为true,就调用内置Router提供的load函数预主动加载这些特征模块。

app/selective-preload-strategy.ts (preload selected modules)

import 'rxjs/add/observable/of'; import { Injectable } from '@angular/core'; import { PreloadingStrategy, Route } from '@angular/router'; import { Observable } from 'rxjs/Observable'; @Injectable() export class PreloadSelectedModules implements PreloadingStrategy { preloadedModules: string[] = []; preload(route: Route, load: Function): Observable<any> { if (route.data && route.data['preload']) { // add the route path to our preloaded module array this.preloadedModules.push(route.path); // log the route path to the console console.log('Preloaded: ' + route.path); return load(); } else { return Observable.of(null); } } }

In order to use our custom preloading strategy, we import it into our app-routing.module.ts and replace the PreloadAllModules strategy. We also add the PreloadSelectedModules strategy to the AppRoutingModule providers array. This allows the Router pre-loader to inject our custom strategy.

要使用我们的自定义预加载策略,将它导入到app-routing.module.ts并替换PreloadAllModules策略。 我们还将PreloadSelectedModules策略添加到AppRoutingModuleproviders数组中。这样,路由器的预加载器可以注入我们的自定义策略。

To confirm our CrisisCenterModule is being pre-loaded, we'll display our preloadedModules in the Admin dashboard. We already know how to use an ngFor loop, so we'll skip over the details here. Since the PreloadSelectedModules is just a service, we can inject it into the AdminDashboardComponent and wire it up to our list.

要确认CrisisCenterModule是否被预加载,我们将在Admin管理控制台显示preloadedModules。 我们已经知道如何使用ngFor循环,所以在这里跳过了一些细节。因为PreloadSelectedModules只是一个服务,我们可以将其注入到AdminDashboardComponent并连接到列表中:

app/admin/admin-dashboard.component.ts (preloaded modules)

import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { PreloadSelectedModules } from '../selective-preload-strategy'; import 'rxjs/add/operator/map'; @Component({ template: ` <p>Dashboard</p> <p>Session ID: {{ sessionId | async }}</p> <a id="anchor"></a> <p>Token: {{ token | async }}</p> Preloaded Modules <ul> <li *ngFor="let module of modules">{{ module }}</li> </ul> ` }) export class AdminDashboardComponent implements OnInit { sessionId: Observable<string>; token: Observable<string>; modules: string[]; constructor( private route: ActivatedRoute, private preloadStrategy: PreloadSelectedModules ) { this.modules = preloadStrategy.preloadedModules; } ngOnInit() { // Capture the session ID if available this.sessionId = this.route .queryParams .map(params => params['session_id'] || 'None'); // Capture the fragment if available this.token = this.route .fragment .map(fragment => fragment || 'None'); } }

Once our application is loaded to our initial route, the CrisisCenterModule is loaded eagerly. We can verify this by logging in to the Admin feature area and noting that the crisis-center is listed in the Preloaded Modules and logged to the console. We can continue to add feature modules to be selectively loaded eagerly.

一旦应用加载到初始路由,CrisisCenterModule被主动加载了。要验证它,登录到Admin特征区域,注意crisis-center被列到Preloaded Modules并输出到控制台。 我们可以继续添加特征区域,以便有选择的加载它们。

Wrap Up

总结

We've covered a lot of ground in this chapter and the application is too big to reprint here. Please visit the and where you can download the final source code.

本章中涉及到了很多背景知识,而且本应用程序也太大了,所以没法在这里显示。请访问在线例子,在那里你可以下载最终的源码。

Appendices

附录

The balance of this chapter is a set of appendices that elaborate some of the points we covered quickly above.

本章剩下的部分是一组附录,它详尽阐述了我们曾匆匆带过的一些知识点。

The appendix material isn't essential. Continued reading is for the curious.

该附件中的内容不是必须的,感兴趣的人才需要阅读它。

附录:链接参数数组

We've mentioned the Link Parameters Array several times. We've used it several times.

我们已经数次提及链接参数数组,也用过好几次了。

A link parameters array holds the ingredients for router navigation:

链接参数数组保存路由导航时所需的成分:

We can bind the RouterLink directive to such an array like this:

我们可以把RouterLink指令绑定到一个数组,就像这样:

<a [routerLink]="['/heroes']">Heroes</a>

We've written a two element array when specifying a route parameter like this

在指定路由参数时,我们写过一个双元素的数组,就像这样:

this.router.navigate(['/hero', hero.id]);

We can provide optional route parameters in an object like this:

我们可以在对象中提供可选的路由参数,就像这样:

<a [routerLink]="['/crisis-center', { foo: 'foo' }]">Crisis Center</a>

These three examples cover our needs for an app with one level routing. The moment we add a child router, such as the Crisis Center, we create new link array possibilities.

这三个例子覆盖了我们在单级路由的应用中所需的一切。在添加一个像危机中心一样的子路由时,我们创建新链接数组组合。

Recall that we specified a default child route for Crisis Center so this simple RouterLink is fine.

回忆一下,我们曾为危机中心指定过一个默认的子路由,以便能使用这种简单的RouterLink

<a [routerLink]="['/crisis-center']">Crisis Center</a>

Let's parse it out.

让我们把它分解出来:

Let's take it a step further. This time we'll build a link parameters array that navigates from the root of the application down to the "Dragon Crisis".

在下一步,我们会用到它。这次,我们要构建一个从根组件往下导航到“巨龙危机”时的链接参数数组:

It looks like this!

看起来是这样的:

<a [routerLink]="['/crisis-center', 1]">Dragon Crisis</a>

If we wanted to, we could redefine our AppComponent template with Crisis Center routes exclusively:

如果想,我们还能单独使用危机中心的路由来重定义AppComponent的模板。

template: ` <h1 class="title">Angular Router</h1> <nav> <a [routerLink]="['/crisis-center']">Crisis Center</a> <a [routerLink]="['/crisis-center/1', { foo: 'foo' }]">Dragon Crisis</a> <a [routerLink]="['/crisis-center/2']">Shark Crisis</a> </nav> <router-outlet></router-outlet> `

In sum, we can write applications with one, two or more levels of routing. The link parameters array affords the flexibility to represent any routing depth and any legal sequence of route paths, (required) router parameters and (optional) route parameter objects.

总结:我们可以用一级、两级或多级路由来写应用程序。 链接参数数组提供了用来表示任意深度路由的链接参数数组以及任意合法的路由参数序列、必须的路由器参数以及可选的路由参数对象。

Appendix: Why use an ngOnInit method

附录:为什么要使用ngOnInit方法

We implemented an ngOnInit method in many of our Component classes. We did so, for example, in the HeroDetailComponent. We might have put the ngOnInit logic inside the constructor instead. We didn't for a reason. The reason is testability.

我们在很多组件类中实现了ngOnInit方法。比如在HeroDetailComponent中就这么用过。也可以把ngOnInit中的逻辑放在构造函数中,但为了一个理由而没那么做,这个理由就是可测试性

A constructor that has major side-effects can be difficult to test because it starts doing things as soon as we create a test instance. In this case, it might have made a request to a remote server, something it shouldn't do under test. It may even be impossible to reach the server in the test environment.

有显著副作用的构造函数很难测试,因为它在创建测试实例时就开始做事了。这种情况下,它可能已经向远程服务器发起了请求,但有些事情在测试时没法做。可能在测试环境下无法访问服务器。

The better practice is to limit what the constructor can do. Mostly it should stash parameters in local variables and perform simple instance configuration.

更好地实践是限制一下构造函数能做什么。通常它会把参数保存到局部变量中,以及执行简单的实例配置工作。

Yet we want an instance of this class to get the hero data from the HeroService soon after it is created. How do we ensure that happens if not in the constructor?

然而我们还是需要该类的实例在创建完之后尽快从HeroService中获取英雄数据。如果不能放在构造函数中又该怎么办?

Angular detects when a component has certain lifecycle methods like ngOnInit and ngOnDestroy and calls them at the appropriate moment.

Angular会负责检测组件是否具有特定的生命周期方法,比如ngOnInitngOnDestroy,并在合适的时机调用它们。

Angular will call ngOnInit when we navigate to the HeroDetailComponent, we'll get the id from the ActivatedRoute params and ask the server for the hero with that id.

Angular会在我们导航到HeroDetailComponent时调用ngOnInit,我们将从ActivatedRoute的路由参数中取得id,并向服务器请求具有这个id的英雄。

We too can call that ngOnInit method in our tests if we wish ... after taking control of the injected HeroService and (perhaps) mocking it.

在获得了已注入的HeroService实例并(可能)做好模拟(Mock)之后,我们可以随时在测试中调用ngOnInit方法。

Appendix: LocationStrategy and browser URL styles

附录:LocationStrategy以及浏览器URL样式

When the router navigates to a new component view, it updates the browser's location and history with a URL for that view. This is a strictly local URL. The browser shouldn't send this URL to the server and should not reload the page.

当路由器导航到一个新的组件视图时,它会用该视图的URL来更新浏览器的当前地址以及历史。 严格来说,这个URL其实是本地的,浏览器不会把该URL发给服务器,并且不会重新加载此页面。

Modern HTML 5 browsers support history.pushState, a technique that changes a browser's location and history without triggering a server page request. The router can compose a "natural" URL that is indistinguishable from one that would otherwise require a page load.

现代HTML 5浏览器支持history.pushState API, 这是一项可以改变浏览器的当前地址和历史,却又不会触发服务端页面请求的技术。 路由器可以合成出一个“自然的”URL,它看起来和那些需要进行页面加载的URL没什么区别。

Here's the Crisis Center URL in this "HTML 5 pushState" style:

下面是危机中心的URL在“HTML 5 pushState”风格下的样子:

localhost:3002/crisis-center/

Older browsers send page requests to the server when the location URL changes ... unless the change occurs after a "#" (called the "hash"). Routers can take advantage of this exception by composing in-application route URLs with hashes. Here's a "hash URL" that routes to the Crisis Center

老旧的浏览器在当前地址的URL变化时总会往服务器发送页面请求……唯一的例外规则是:当这些变化位于“#”(被称为“hash”)后面时不会发送。通过把应用内的路由URL拼接在#之后,路由器可以获得这条“例外规则”带来的优点。下面是到危机中心路由的“hash URL”:

localhost:3002/src/#/crisis-center/

The Router supports both styles with two LocationStrategy providers:

路由器通过两种LocationStrategy提供商来支持所有这些风格:

  1. PathLocationStrategy - the default "HTML 5 pushState" style.

  2. PathLocationStrategy - 默认的策略,支持“HTML 5 pushState”风格。

  3. HashLocationStrategy - the "hash URL" style.

  4. HashLocationStrategy - 支持“hash URL”风格。

The RouterModule.forRoot function sets the LocationStrategy to the PathLocationStrategy, making it the default strategy. We can switch to the HashLocationStrategy with an override during the bootstrapping process if we prefer it.

RouterModule.forRoot函数把LocationStrategy设置成了PathLocationStrategy,使其成为了默认策略。 我们可以在启动过程中改写(override)它,来切换到HashLocationStrategy风格 —— 如果我们更喜欢这种。

Learn about "providers" and the bootstrap process in the Dependency Injection chapter

要学习关于“提供商”和启动过程的更多知识,参见依赖注入一章。

Which Strategy is Best?

哪种策略更好?

We must choose a strategy and we need to make the right call early in the project. It won't be easy to change later once the application is in production and there are lots of application URL references in the wild.

我们必须选择一种策略,并且在项目的早期就这么干。一旦该应用进入了生产阶段,要改起来可就不容易了,因为外面已经有了大量对应用URL的引用。

Almost all Angular projects should use the default HTML 5 style. It produces URLs that are easier for users to understand. And it preserves the option to do server-side rendering later.

几乎所有的Angular项目都会使用默认的HTML 5风格。它生成的URL更易于被用户理解,它也为将来做服务端渲染预留了空间。

Rendering critical pages on the server is a technique that can greatly improve perceived responsiveness when the app first loads. An app that would otherwise take ten or more seconds to start could be rendered on the server and delivered to the user's device in less than a second.

在服务器端渲染指定的页面,是一项可以在该应用首次加载时大幅提升响应速度的技术。那些原本需要十秒甚至更长时间加载的应用,可以预先在服务端渲染好,并在少于一秒的时间内完整呈现在用户的设备上。

This option is only available if application URLs look like normal web URLs without hashes (#) in the middle.

只有当应用的URL看起来像是标准的Web URL,中间没有hash(#)时,这个选项才能生效。

Stick with the default unless you have a compelling reason to resort to hash routes.

除非你有强烈的理由不得不使用hash路由,否则就应该坚决使用默认的HTML 5路由风格。

HTML 5 URLs and the <base href>

HTML 5 URL与<base href>

While the router uses the "HTML 5 pushState" style by default, we must configure that strategy with a base href

由于路由器默认使用“HTML 5 pushState”风格,所以我们必须用一个base href来配置该策略(Strategy)。

The preferred way to configure the strategy is to add a <base href> element tag in the <head> of the index.html.

配置该策略的首选方式是往index.html<head>中添加一个<base href> element标签。

<base href="/">

Without that tag, the browser may not be able to load resources (images, css, scripts) when "deep linking" into the app. Bad things could happen when someone pastes an application link into the browser's address bar or clicks such a link in an email link.

如果没有此标签,当通过“深链接”进入该应用时,浏览器就不能加载资源(图片、CSS、脚本)。如果有人把应用的链接粘贴进浏览器的地址栏或从邮件中点击应用的链接时,这种问题就发生。

Some developers may not be able to add the <base> element, perhaps because they don't have access to <head> or the index.html.

有些开发人员可能无法添加<base>元素,这可能是因为它们没有访问<head>index.html的权限。

Those developers may still use HTML 5 URLs by taking two remedial steps:

它们仍然可以使用HTML 5格式的URL,但要采取两个步骤进行补救:

  1. Provide the router with an appropriate APP_BASE_HREF value.

  2. 用适当的APP_BASE_HREF值提供(provide)路由器。

  3. Use absolute URLs for all web resources: css, images, scripts, and template html files.

  4. 对所有Web资源使用绝对地址:CSS、图片、脚本、模板HTML。

Learn about the APP_BASE_HREF in the API Guide.

你可以到API指南中学习关于APP_BASE_HREF的更多知识。

HashLocationStrategy

HashLocationStrategy

We can go old-school with the HashLocationStrategy by providing the useHash: true in an object as the second argument of the RouterModule.forRoot in our AppModule.

我们可以在根模块的RouterModule.forRoot的第二个参数中传入一个带有useHash: true的对象,以回到基于HashLocationStrategy的传统方式。

app/app.module.ts (hash URL strategy)

import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { Routes, RouterModule } from '@angular/router'; import { AppComponent } from './app.component'; const routes: Routes = [ ]; @NgModule({ imports: [ BrowserModule, FormsModule, RouterModule.forRoot(routes, { useHash: true }) // .../#/crisis-center/ ], declarations: [ AppComponent ], providers: [ ], bootstrap: [ AppComponent ] }) export class AppModule { }