This guide covers the router's primary features, illustrating them through the evolution
of a small application that you can run live in the browser / downloadable example.
To see the URL changes in the browser address bar of the live example,
open it again in the Plunker editor by clicking the icon in the upper right,
then pop out the preview window by clicking the blue 'X' button in the upper right corner.
概览
Overview
浏览器具有我们熟悉的导航模式:
The browser is a familiar model of application navigation:
在地址栏输入URL,浏览器就会导航到相应的页面。
Enter a URL in the address bar and the browser navigates to a corresponding page.
在页面中点击链接,浏览器就会导航到一个新页面。
Click links on the page and the browser navigates to a new page.
点击浏览器的前进和后退按钮,浏览器就会在你的浏览历史中向前或向后导航。
Click the browser's back and forward buttons and the browser navigates
backward and forward through the history of pages you've seen.
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.
It can pass optional parameters along to the supporting view component that help it decide what specific content to present.
You can bind the router to links on a page and it will navigate to
the appropriate application view when the user clicks a link.
You 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.
This guide proceeds in phases, marked by milestones, starting from a simple two-pager
and building toward a modular, multi-view design with child routes.
在接触细节之前,我们先来介绍关于路由的一些核心概念。
An introduction to a few core router concepts will help orient you to the details that follow.
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.
如果app文件夹是该应用的根目录(就像我们的范例应用一样),那就把href的值设置为下面这样:
If the app folder is the application root, as it is for the sample application,
set the href value exactly as shown here.
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.
Import what you need from it as you would from any other Angular package.
A routed Angular application has one singleton instance of the Router service.
When the browser's URL changes, that router looks for a corresponding Route
from which it can determine the component to display.
A router has no routes until you configure it.
The following example creates four route definitions, configures the router via the RouterModule.forRoot method,
and adds the result to the AppModule's imports array.
Each Route maps a URL path to a component.
There are no leading slashes in the path.
The router parses and builds the final URL for you,
allowing you to use both relative and absolute paths when navigating between application views.
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.
You'll learn more about route parameters later in this guide.
The data property in the third route is a place to store arbitrary data associated with
this specific route. The data property is accessible within each activated route. Use it to store
items such as page titles, breadcrumb text, and other read-only, static data.
You'll use the resolve guard to retrieve dynamic data later in the guide.
The empty path in the fourth route represents the default path for the application,
the place to go when the path in the URL is empty, as it typically is at the start.
This default route redirects to the route for the /heroes URL and, therefore, will display the HeroesListComponent.
最后一个路由中的**路径是一个通配符。当所请求的URL不匹配前面定义的路由表中的任何路径时,路由器就会选择此路由。
这个特性可用于显示“404 - Not Found”页,或自动重定向到其它路由。
The ** path in the last route is a wildcard. The router will select this route
if the requested URL doesn't match any paths for routes defined earlier in the configuration.
This is useful for displaying a "404 - Not Found" page or redirecting to another route.
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 the configuration above, routes with a static path are listed first, followed by an empty path route,
that matches the default route.
The wildcard route comes last because it matches every URL and should be selected only if no other routes are matched first.
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 HeroListComponentafter a RouterOutlet that you've placed in the host view's HTML.
<router-outlet></router-outlet><!-- Routed views go here -->
Now you have routes configured and a place to render them, but
how do you navigate? The URL could arrive directly from the browser address bar.
But most of the time you navigate as a result of some user action such as the click of
an anchor tag.
The RouterLink directives on the anchor tags give the router control over those elements.
The navigation paths are fixed, so you can assign a string to the routerLink (a "one-time" binding).
Had the navigation path been more dynamic, you could have bound to a template expression that
returned an array of route link parameters (the link parameters array).
The router resolves that array into a complete URL.
The RouterLinkActive directive on each anchor tag helps visually distinguish the anchor for the currently selected "active" route.
The router adds the active CSS class to the element when the associated RouterLink becomes active.
You can add this directive to the anchor or to its parent element.
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. You can access the current RouterState from anywhere in the
application using the Router service and the routerState property.
Each ActivatedRoute in the RouterState provides methods to traverse up and down the route tree
to get information from parent, child and sibling routes.
The application has a configured router.
The shell 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.
下面是一些路由器中的关键词汇及其含义:
Here are the key Router terms and their meanings:
路由器部件
Router Part
含义
Meaning
Router(路由器)
Router
为激活的URL显示应用组件。管理从一个组件到另一个组件的导航
Displays the application component for the active URL.
Manages navigation from one component to the next.
RouterModule(路由器模块)
RouterModule
一个独立的Angular模块,用于提供所需的服务提供商,以及用来在应用视图之间进行导航的指令。
A separate Angular module that provides the necessary service providers
and directives for navigating through application views.
Routes(路由数组)
Routes
定义了一个路由数组,每一个都会把一个URL路径映射到一个组件。
Defines an array of Routes, each mapping a URL path to a component.
Route(路由)
Route
定义路由器该如何根据URL模式(pattern)来导航到组件。大多数路由都由路径和组件类构成。
Defines how the router should navigate to a component based on a URL pattern.
Most routes consist of a path and a component type.
RouterOutlet(路由出口)
RouterOutlet
该指令(<router-outlet>)用来标记出路由器该在哪里显示视图。
The directive (<router-outlet>) that marks where the router displays a view.
The directive for binding a clickable HTML element to
a route. Clicking an element with a routerLink directive
that is bound to a link parameters array triggers a navigation.
The directive for adding/removing classes from an HTML element when an associated
routerLink contained on or inside the element becomes active/inactive.
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.
RouterState(路由器状态)
RouterState
路由器的当前状态包含了一棵由程序中激活的路由构成的树。它包含一些用于遍历路由树的快捷方法。
The current state of the router including a tree of the currently activated
routes together with convenience methods for traversing the route tree.
An array that the router interprets as a routing instruction.
You can bind that array toa RouterLink or pass the array as an argument to
the Router.navigate method.
路由组件
Routing component
一个带有RouterOutlet的Angular组件,它根据路由器的导航来显示相应的视图。
An Angular component with a RouterOutlet that displays views based on router navigations.
This guide describes development of a multi-page routed sample application.
Along the way, it highlights design decisions and describes key features of the router such as:
把应用的各个特性组织成模块。
Organizing the application features into modules.
导航到组件(Heroes链接到“英雄列表”组件)。
Navigating to a component (Heroes link to "Heroes List").
包含一个路由参数(当路由到“英雄详情”时,把该英雄的id传进去)。
Including a route parameter (passing the Hero id while routing to the "Hero Detail").
子路由(危机中心特性有一组自己的路由)。
Child routes (the Crisis Center has its own routes).
CanActivate守卫(检查路由的访问权限)。
The CanActivate guard (checking route access).
CanActivateChild守卫(检查子路由的访问权限)。
The CanActivateChild guard (checking child route access).
CanDeactivate守卫(询问是否丢弃未保存的更改)。
The CanDeactivate guard (ask permission to discard unsaved changes).
Resolve守卫(预先获取路由数据)。
The Resolve guard (pre-fetching route data).
惰性加载特性模块。
Lazy loading feature modules.
CanLoad守卫(在加载特性模块之前进行检查)。
The CanLoad guard (check before loading feature module assets).
The guide proceeds as a sequence of milestones as if you were building the app step-by-step.
But, it is not a tutorial and it glosses over details of Angular application construction
that are more thoroughly covered elsewhere in the documentation.
Alter the name.
Click the "Back" button and the app returns to the heroes list which displays the changed hero name.
Notice that the name change took effect immediately.
Had you clicked the browser's back button instead of the "Back" button,
the app would have returned you to the heroes list as well.
Angular app navigation updates the browser history as normal web navigation does.
现在,点击危机中心链接,前往危机列表页。
Now click the Crisis Center link for a list of ongoing crises.
Select a crisis and the application takes you to a crisis editing screen.
The Crisis Detail appears in a child view on the same page, beneath the list.
修改危机的名称。
注意,危机列表中的相应名称并没有修改。
Alter the name of a crisis.
Notice that the corresponding name in the crisis list does not change.
Unlike Hero Detail, which updates as you type,
Crisis Detail changes are temporary until you 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.
先不要点击这些按钮。
而是点击浏览器的后退按钮,或者点击“Heroes”链接。
Do not click either button yet.
Click the browser back button or the "Heroes" link instead.
我们会看到弹出了一个对话框。
Up pops a dialog box.
我们可以回答“确定”以放弃这些更改,或者回答“取消”来继续编辑。
You can say "OK" and lose your changes or click "Cancel" and continue editing.
Behind this behavior is the router's CanDeactivate guard.
The guard gives you a chance to clean-up or ask the user's permission before navigating away from the current view.
Admin和Login按钮用于演示路由器的其它能力,本章稍后的部分会讲解它们。我们现在先不管它。
The Admin and Login buttons illustrate other router capabilities to be covered later in the guide.
This short introduction will do for now.
我们这就开始本应用的第一个里程碑。
Proceed to the first application milestone.
里程碑1:从路由器开始
Milestone 1: Getting started with the router
开始本应用的一个简版,它在两个空路由之间导航。
Begin with a simple version of the app that navigates between two empty views.
The router uses the browser's
history.pushState
for navigation. Thanks to pushState, you can make in-app URL paths look the way you want them to
look, e.g. localhost:3000/crisis-center. The in-app URLs can be indistinguishable from server URLs.
Modern HTML5 browsers were the first to support pushState which is why many people refer to these URLs as
"HTML5 style" URLs.
HTML 5风格的导航是路由器的默认值。请到下面的附录浏览器URL风格中学习为什么首选“HTML 5”风格、如何调整它的行为,以及如何在必要时切换回老式的hash(#)风格。
HTML5 style navigation is the router default.
In the LocationStrategy and browser URL styles Appendix,
learn why HTML5 style is preferred, how to adjust its behavior, and how to switch to the
older hash (#) style, if necessary.
You must add a
<base href> element
to the app's index.html for pushState routing to work.
The browser uses the <base href> value to prefix relative URLs when referencing
CSS files, scripts, and images.
Add the <base> element just after the <head> tag.
If the app folder is the application root, as it is for this application,
set the href value in index.htmlexactly as shown here.
A live coding environment like Plunker sets the application base address dynamically so you can't specify a fixed address.
That's why the example code replaces the <base href...> with a script that writes the <base> tag on the fly.
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.
通过一些路由来配置路由器,我们可以教它如何进行导航。
You teach the router how to navigate by configuring it with routes.
定义路由
Define routes
路由器必须用“路由定义”的列表进行配置。
A router must be configured with a list of route definitions.
Each definition translates to a Route object which has two things: a
path, the URL path segment for this route; and a
component, the component associated with this route.
The router draws upon its registry of definitions when the browser URL changes
or when application code tells the router to navigate along a route path.
直白的说,我们可以这样解释第一个路由:
In simpler terms, you might say this of the first route:
When the browser's location URL changes to match the path segment /crisis-center, then
the router activates an instance of the CrisisListComponent and displays its view.
When the application requests navigation to the path /crisis-center, the router
activates an instance of CrisisListComponent, displays its view, and updates the
browser's address location and history with the URL for that path.
Here is the first configuration. Pass the array of routes, appRoutes, to the RouterModule.forRoot method.
It returns a module, containing the configured Router service provider, plus other providers that the routing library requires.
Once the application is bootstrapped, the Router performs the initial navigation based on the current browser URL.
Adding the configured RouterModule to the AppModule is sufficient for simple route configurations.
As the application grows, you'll want to refactor the routing configuration into a separate file
and create a Routing Module, a special type of Service Module dedicated to the purpose
of routing in feature modules.
在AppModule中提供RouterModule,让该路由器在应用的任何地方都能被使用。
Providing the RouterModule in the AppModule makes the Router available everywhere in the application.
The root AppComponent is the application shell. It has a title, a navigation bar with two links,
and a router outlet where the router swaps views on and off the page. Here's what you get:
该组件所对应的模板是这样的:
The corresponding component template looks like this:
You can also add more contextual information to the RouterLink by providing query string parameters
or a URL fragment for jumping to different areas on the 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.
The template expression to the right of the equals (=) contains a space-delimited string of CSS classes
that the Router will add when this link is active (and remove when the link is inactive).
You can also set the RouterLinkActive directive to a string of classes such as [routerLinkActive]="active fluffy"
or bind it to a component property that returns such a string.
The RouterLinkActive directive toggles css classes for active RouterLinks based on the current RouterState.
This cascades down through each level of the route tree, so parent and child router links can be active at the same time.
To override this behavior, you 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.
RouterLink, RouterLinkActive and RouterOutlet are directives provided by the Angular RouterModule package.
They are readily available for you to use in the template.
app.component.ts目前是这样的:
The current state of app.component.ts looks like this:
You've created two routes in the app so far, one to /crisis-center and the other to /heroes.
Any other URL causes the router to throw an error and crash the app.
可以添加一个通配符路由来拦截所有无效的URL,并优雅的处理它们。
通配符路由的path是两个星号(**),它会匹配任何 URL。
当路由器匹配不上以前定义的那些路由时,它就会选择这个路由。
通配符路由可以导航到自定义的“404 Not Found”组件,也可以重定向到一个现有路由。
Add a wildcard route to intercept invalid URLs and handle them gracefully.
A wildcard route has a path consisting of two asterisks. It matches every URL.
The router will select this route if it can't match a route earlier in the configuration.
A wildcard route can navigate to a custom "404 Not Found" component or redirect to an existing route.
The router selects the route with a first match wins strategy.
Wildcard routes are the least specific routes in the route configuration.
Be sure it is the last route in the configuration.
Instead of adding the "/sidekicks" route, define a wildcard route instead and have it navigate to a simple PageNotFoundComponent.
src/app/app.module.ts (wildcard)
{ path:'**', component:PageNotFoundComponent}
创建PageNotFoundComponent,以便在用户访问无效网址时显示它。
Create the PageNotFoundComponent to display when users visit invalid URLs.
src/app/not-found.component.ts (404 component)
import{Component}from'@angular/core';@Component({template:'<h2>Page not found</h2>'})exportclassPageNotFoundComponent{}
像其它组件一样,把PageNotFoundComponent添加到AppModule的声明中。
As with the other components, add the PageNotFoundComponent to the AppModule declarations.
现在,当用户访问/sidekicks或任何无效的URL时,浏览器就会显示“Page not found”。
浏览器的地址栏仍指向无效的URL。
Now when the user visits /sidekicks, or any other invalid URL, the browser displays "Page not found".
The browser address bar continues to point to the invalid URL.
把默认路由设置为英雄列表
The default route to heroes
应用启动时,浏览器地址栏中的初始URL是这样的:
When the application launches, the initial URL in the browser bar is something like:
That doesn't match any of the configured routes which means that the application won't display any component when it's launched.
The user must click one of the links to trigger a navigation and display a component.
It would be nicer if the application had a default route that displayed the list of heroes immediately,
just as it will when the user clicks the "Heroes" link or pastes localhost:3000/heroes into the address bar.
The preferred solution is to add a redirect route that translates the initial relative URL ('')
to the desired default path (/heroes). The browser address bar shows .../heroes as if you'd navigated there directly.
Add the default route somewhere above the wildcard route.
It's just above the wildcard route in the following excerpt showing the complete appRoutes for this milestone.
A redirect route requires a pathMatch property to tell the router how to match a URL to the path of a route.
The router throws an error if you don't.
In this app, the router should select the route to the HeroListComponent only when the entire URL matches '',
so set the pathMatch value to 'full'.
Technically, pathMatch = 'full' results in a route hit when the remaining, unmatched segments of the URL match ''.
In this example, the redirect is in a top level route so the remaining URL and the entire URL are the same thing.
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都会匹配上''。
Don't do that here.
If the pathMatch value were 'prefix', every URL would match ''.
尝试把它设置为'prefix',然后点击Go to sidekicks按钮。别忘了,它是一个无效URL,本应显示“Page not found”页。
但是,我们看到了“英雄列表”页。在地址栏中输入一个无效的URL,我们又被路由到了/heroes。
每一个URL,无论有效与否,都会匹配上这个路由定义。
Try setting it to 'prefix' then click the Go to sidekicks button.
Remember that's a bad URL and you should see the "Page not found" page.
Instead, you're still on the "Heroes" page.
Enter a bad URL in the browser address bar.
You're instantly re-routed to /heroes.
Every URL, good or bad, that falls through to this route definition
will be a match.
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.
In the initial route configuration, you provided a simple setup with two routes used
to configure the application for routing. This is perfectly fine for simple routing.
As the application grows and you make use of more Router features, such as guards,
resolvers, and child routing, you'll naturally want to refactor the routing configuration into its own file.
We recommend moving the routing information into a special-purpose module called a Routing Module.
路由模块有一系列特性:
The Routing Module has several characteristics:
把路由这个关注点从其它应用类关注点中分离出去
Separates routing concerns from other application concerns.
测试特性模块时,可以替换或移除路由模块
Provides a module to replace or remove when testing the application.
为路由服务提供商(包括守卫和解析器等)提供一个共同的地方
Provides a well-known location for routing service providers including guards and resolvers.
Import the CrisisListComponent and the HeroListComponent components
just like you did in the app.module.ts. Then move the Router imports
and routing configuration, including RouterModule.forRoot, into this routing module.
Finally, re-export the Angular RouterModule by adding it to the module exports array.
By re-exporting the RouterModule here and importing AppRoutingModule in AppModule,
the components declared in AppModule will have access to router directives such as RouterLink and RouterOutlet.
做完这些之后,该文件变成了这样:
After these steps, the file should look like this.
Next, update the app.module.ts file,
first importing the newly created AppRoutingModule from app-routing.module.ts,
then replacing RouterModule.forRoot in the imports array with the AppRoutingModule.
The Routing Modulereplaces 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 (for example, AppRoutingModule) when the configuration is simple and
merge the routing configuration directly into the companion module (for example, AppModule).
我们建议你选择其中一种模式,并坚持模式的一致性。
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.
里程碑 #2 英雄特征区
Milestone 3: Heroes feature
我们刚刚学习了如何用RouterLink指令进行导航。接下来我们将到:
You've seen how to navigate using the RouterLink directive.
Now you'll learn the following:
用模块把应用和路由组织为一些特性区
Organize the app and routes into feature areas using modules.
命令式地从一个组件导航到另一个组件
Navigate imperatively from one component to another.
通过路由传递必要信息和可选信息
Pass required and optional information in route parameters.
While you could continue to add files to the src/app/ folder,
that is unrealistic and ultimately not maintainable.
Most developers prefer to put each feature area in its own folder.
You are about to break up the app into different feature modules, each with its own concerns.
Then you'll import into the main module and navigate among them.
添加英雄管理功能
Add heroes functionality
按照下列步骤:
Follow these steps:
创建src/app/heroes文件夹,我们将会把英雄管理功能的实现文件放在这里。
Create the src/app/heroes folder; you'll be adding files implementing hero management there.
在app目录下删除占位用的hero-list.component.ts文件。
Delete the placeholder hero-list.component.ts that's in the app folder.
在src/app/heroes目录下创建新的hero-list.component.ts文件。
Create a new hero-list.component.ts under src/app/heroes.
The heroes feature has two interacting components, the hero list and the hero detail.
The list view is self-sufficient; you navigate to it, it gets a list of heroes and displays them.
详情视图就不同了。它要显示一个特定的英雄,但是它本身却无法知道显示哪一个,此信息必须来自外部。
The detail view is different. It displays a particular hero. It can't know which hero to show on its own.
That information must come from outside.
When the user selects a hero from the list, the app should navigate to the detail view
and show that hero.
You tell the detail view which hero to display by including the selected hero's id in the route URL.
Put 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 src/app/heroes folder.
Consider giving each feature module its own route configuration file.
It may seem like overkill early when the feature routes are simple.
But routes have a tendency to grow more complex and consistency in patterns pays off over time.
There is a small but critical difference.
In the AppRoutingModule, you used the static RouterModule.forRoot method to register the routes and application level service providers.
In a feature module you use the static forChild method.
Only call RouterModule.forRoot in the root AppRoutingModule
(or the AppModule if that's where you register top level application routes).
In any other module, you must call the RouterModule.forChild method to register additional routes.
Open heroes.module.ts.
Import the HeroRoutingModule token from heroes-routing.module.ts and
add it to the imports array of the HeroesModule.
The finished HeroesModule looks like this:
Routes provided by feature modules are combined together into their imported module's routes by the router.
This allows you to continue defining the feature module routes without modifying the main route configuration.
Remove the HeroListComponent from the AppModule's declarations because it's now provided by the HeroesModule.
This is important. There can be only one owner for a declared component.
In this case, the Heroes module is the owner of the Heroes components and is making them available to
components in the AppModule via the HeroesModule.
As a result, the AppModule no longer has specific knowledge of the hero feature, its components, or its route details.
You 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.
经过这些步骤,AppModule变成了这样:
After these steps, the AppModule should look like this:
When all routes were in one AppRoutingModule,
you put the default and wildcard routes last, after the /heroes route,
so that the router had a chance to match a URL to the /heroes route before
hitting the wildcard route and navigating to "Page not found".
Each routing module augments the route configuration in the order of import.
If you list AppRoutingModule first, the wildcard route will be registered
before the hero routes.
The wildcard route — which matches every URL —
will intercept the attempt to navigate to a hero route.
Reverse the routing modules and see for yourself that
a click of the heroes link results in "Page not found".
Learn about inspecting the runtime router configuration
below.
Notice the :id token in the path. That creates a slot in the path for a Route Parameter.
In this case, the router will insert the id of a hero into that slot.
Embedding the route parameter token, :id,
in the route definition path is a good choice for this 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.
The template defines an *ngFor repeater such as you've seen before.
There's a (click)event binding to the component's
onSelect method which you implement as follows:
The component's onSelect calls the router's navigate method with a link parameters array.
You can use this same syntax in a RouterLink if you decide later to navigate in HTML template rather than in component code.
After navigating to the HeroDetailComponent, you expect to see the details of the selected hero.
You need two pieces of information: the routing path to the component and the hero's id.
The route path and parameters are available through an injected router service called the
ActivatedRoute.
It has a great deal of useful information including:
url: 该路由路径的Observable对象。它的值是一个由路径中各个部件组成的字符串数组。
url: An Observable of the route path(s), represented as an array of strings for each part of the route path.
Later, in the ngOnInit method, you use the ActivatedRoute service to retrieve the parameters for the route,
pull the hero id from the parameters and retrieve the hero to display.
Put this data access logic in the ngOnInit method rather than inside the constructor to improve the component's testability.
Angular calls the ngOnInit method shortly after creating an instance of the HeroDetailComponent
so the hero will be retrieved in time to use it.
ngOnInit(){this.route.params// (+) converts string 'id' to a number.switchMap((params:Params)=>this.service.getHero(+params['id'])).subscribe((hero:Hero)=>this.hero = hero);}
Since the parameters are provided as an Observable, you use the switchMap operator to
provide them for the id parameter by name and tell the HeroService to fetch the hero with that id.
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.
In this example, you retrieve the route params from an Observable.
That implies that the route params can change during the lifetime of this component.
They might. By default, the router re-uses a component instance when it re-navigates to the same component type
without visiting a different component first. The route parameters could change each time.
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.
You 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.
Unfortunately, ngOnInit is only called once per component instantiation.
You need a way to detect when the route parameters change from within the same instance.
The observable params property handles that beautifully.
当在组件中订阅一个可观察对象时,我们通常总是要在组件销毁时取消这个订阅。
When subscribing to an observable in a component, you almost always arrange to unsubscribe when the component is destroyed.
The ActivatedRoute and its observables are insulated from the Router itself.
The Router destroys a routed component when it is no longer needed and the injected ActivatedRoute dies with it.
不过,我们仍然可以随意取消订阅,这不会造成任何损害,而且也不是一项坏的实践。
Feel free to unsubscribe anyway. It is harmless and never a bad practice.
This application won't re-use the HeroDetailComponent.
The user always returns to the hero list to select another hero to view.
There's no way to navigate from one hero detail to another hero detail
without visiting the list component in between.
Therefore, the router creates a new HeroDetailComponent instance every time.
The route.snapshot provides the initial value of the route parameters.
You can access the parameters directly without subscribing or adding observable operators.
It's much simpler to write and read:
ngOnInit(){// (+) converts string 'id' to a numberlet id =+this.route.snapshot.params['id'];this.service.getHero(id).then((hero:Hero)=>this.hero = hero);}
Remember: you only get the initial value of the parameters with this technique.
Stick with the observable params approach if there's even a chance that the router
could re-use the component.
This sample stays with the observable params strategy just in case.
The router navigate method takes the same one-item link parameters array
that you can bind to a [routerLink] directive.
It holds the path to the HeroListComponent:
Use route parameters to specify a required parameter value within the route URL
as you do when navigating to the HeroDetailComponent in order to view the hero with id15:
You can also add optional information to a route request.
For example, when returning to the heroes list from the hero detail view,
it would be nice if the viewed hero was preselected in the list.
You'll implement this feature in a moment by including the viewed hero's id
in the URL as an optional parameter when returning from the HeroDetailComponent.
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'.
These kinds of parameters don't fit easily in a URL path. Even if you could define a suitable URL token scheme,
doing so greatly complicates the pattern matching required to translate an incoming URL to a named route.
Optional parameters are the ideal vehicle for conveying arbitrarily complex information during navigation.
Optional parameters aren't involved in pattern matching and afford flexibility of expression.
The router supports navigation with optional parameters as well as required route parameters.
Define optional parameters in a separate object after you define the required route parameters.
In general, prefer a required route parameter when
the value is mandatory (for example, if necessary to distinguish one route path from another);
prefer an optional parameter when the value is optional, complex, and/or multivariate.
When navigating to the HeroDetailComponent you specified the requiredid of the hero-to-edit in the
route parameter and made it the second item of the link parameters array.
The router embedded the id value in the navigation URL because you had defined it
as a route parameter with an :id placeholder token in the route path:
When the user clicks the back button, the HeroDetailComponent constructs another link parameters array
which it uses to navigate back to the HeroListComponent.
Now you have a reason. You'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.
Send the id with an object that contains an optionalid parameter.
For demonstration purposes, there's an extra junk parameter (foo) in the object that the HeroListComponent should ignore.
Here's the revised navigation statement:
src/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.// Include a junk 'foo' property for fun.this.router.navigate(['/heroes',{ id: heroId, foo:'foo'}]);}
该应用仍然能工作。点击“back”按钮返回英雄列表视图。
The application still works. Clicking "back" returns to the hero list view.
To see the URL changes in the browser address bar of the live example,
open it again in the Plunker editor by clicking the icon in the upper right,
then pop out the preview window by clicking the blue 'X' button in the upper right corner.
它应该是这样的,不过也取决于你在哪里运行它:
It should look something like this, depending on where you run it:
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 you may not have seen before.
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.
The syntax may seem strange to you 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.
ActivatedRoute服务中的路由参数
Route parameters in the ActivatedRoute service
英雄列表仍没有改变,没有哪个英雄列被加亮显示。
The list of heroes is unchanged. No hero row is highlighted.
The live example / downloadable exampledoes highlight the selected
row because it demonstrates the final state of the application which includes the steps you're about to cover.
At the moment this guide is describing the state of affairs prior to those steps.
Previously, when navigating from the HeroListComponent to the HeroDetailComponent,
you subscribed to the route params Observable and made it available to the HeroDetailComponent
in the ActivatedRoute service.
You injected that service in the constructor of the HeroDetailComponent.
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 you subscribe to those values, set the selectedId,
and get the heroes.
Finally, update the 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:
Defines two transitions, one to ease the component in from the left of the screen as it enters the application view (:enter),
the other to animate the component down as it leaves the application view (:leave).
我们可以为其它路由组件用不同的转场效果创建更多触发器。现在这个触发器已经足够当前的里程碑用了。
You could create more triggers with different transitions for other route components. This trigger is sufficient for the current milestone.
Back in the HeroDetailComponent, import the slideInDownAnimation from './animations.ts.
Add the HostBinding decorator to the imports from @angular/core; you'll need it in a moment.
The '@routeAnimation' passed to the first @HostBinding matches
the name of the slideInDownAnimationtrigger, routeAnimation.
Set the routeAnimation property to true because you only care about the :enter and :leave states.
另外两个@HostBinding属性指定组件的外观和位置。
The other two @HostBinding properties style the display and position of the component.
Applying route animations to individual components works for a simple demo, but in a real life app,
it is better to animate routes based on route paths.
里程碑#3的总结
Milestone 3 wrap up
我们学到了如何:
You've learned how to do the following:
把应用组织成特性区
Organize the app into feature areas.
命令式的从一个组件导航到另一个
Navigate imperatively from one component to another.
通过路由参数传递信息,并在组件中订阅它们
Pass information along in route parameters and subscribe to them in the component.
把这个特性分区模块导入根模块AppModule
Import the feature area NgModule into the AppModule.
把动画应用到路由组件上
Apply animations to the route component.
做完这些修改之后,目录结构是这样的:
After these changes, the folder structure looks like this:
router-sample
src
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
index.html
styles.css
tsconfig.json
node_modules ...
package.json
这里是当前版本的范例程序相关文件。
Here are the relevant files for this version of the sample application.
exportclassCrisis{
constructor(public id: number,public name:string){}}const CRISES =[newCrisis(1,'Dragon Burning Cities'),newCrisis(2,'Sky Rains Great White Sharks'),newCrisis(3,'Giant Asteroid Heading For Earth'),newCrisis(4,'Procrastinators Meeting Delayed Again'),];
The resulting crisis center is a foundation for introducing a new concept—child routing.
You can leave Heroes in its current state as a contrast with the Crisis Center
and decide later if the differences are worthwhile.
Like most shells, the CrisisCenterComponent class is very simple, simpler even than AppComponent:
it has no business logic, and its template has no links, just a title and
<router-outlet> for the crisis center child views.
Unlike AppComponent, and most other components, it lacks a selector.
It doesn't need one since you don't embed this component in a parent template,
instead you use the router to navigate to it.
Create a crisis-center-routing.module.ts file as you did the heroes-routing.module.ts file.
This time, you define child routeswithin the parent crisis-center route.
Notice that the parent crisis-center route has a children property
with a single route containing the CrisisListComponent. The CrisisListComponent route
also has a children array with two routes.
The router displays the components of these routes in the RouterOutlet
of the CrisisCenterComponent, not in the RouterOutlet of the AppComponent shell.
CrisisListComponent包含危机列表和一个RouterOutlet,用以显示Crisis Center Home和Crisis Detail这两个路由组件。
The CrisisListComponent contains the crisis list and a RouterOutlet to
display the Crisis Center Home and Crisis Detail route components.
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 you select different crises.
作为对比,回到Hero Detail路由时,每当我们选择了不同的英雄时,该组件都会被重新创建。
In contrast, back in the Hero Detail route, the component was recreated each time you selected a different hero.
At the top level, paths that begin with / refer to the root of the application.
But child routes extend the path of the parent route.
With each step down the route tree,
you add a slash followed by the route path, unless the path is empty.
如果把该逻辑应用到危机中心中的导航,那么父路径就是/crisis-center。
Apply that logic to navigation within the crisis center for which the parent path is /crisis-center.
Remove the initial crisis center route from the app-routing.module.ts.
The feature routes are now provided by the HeroesModule and the CrisisCenter modules.
我们将保持app.routing.ts文件中只有通用路由,本章稍后会讲解它。
The app-routing.module.ts file retains the top-level application routes such as the default and wildcard routes.
You could continue to use absolute paths like this to navigate inside the Crisis Center
feature, but that pins the links to the parent routing structure.
If you changed the parent /crisis-center path, you would have to change the link parameters array.
You can free the links from this dependency by defining paths that are relative to the current URL segment.
Navigation within the feature area remains intact even if you change the parent route path to the feature.
例子如下:
Here's an example:
在链接参数数组中,路由器支持“目录式”语法来指导我们如何查询路由名:
The router supports directory-like syntax in a link parameters list to help guide route name lookup:
./或无前导斜线形式是相对于当前级别的。
./ or no leading slash is relative to the current level.
You can combine relative navigation syntax with an ancestor path.
If you must navigate to a sibling route, you could use the ../<sibling> convention to go up
one level, then over and down the sibling route path.
To navigate a relative path with the Router.navigate method, you must supply the ActivatedRoute
to give the router knowledge of where you are in the current route tree.
After the link parameters array, add an object with a relativeTo property set to the ActivatedRoute.
The router then calculates the target URL based on the active route's location.
当调用路由器的navigateByUrl时,总是要指定完整的绝对路径。
Always specify the complete absolute path when calling router's navigateByUrl method.
用相对URL导航到危机详情
Navigate to crisis detail with a relative URL
把危机列表的onSelect方法改成使用相对导航,以便我们不用每次都从路由配置的顶层开始。
Update the Crisis ListonSelect method to use relative navigation so you don't have
to start from the top of the route configuration.
我们已经注入过了ActivatedRoute,我们需要它来和相对导航路径组合在一起。
You've already injected the ActivatedRoute that you need to compose the relative navigation path.
If you were using a RouterLink to navigate instead of the Router service, you'd use the same
link parameters array, but you wouldn't provide the object with the relativeTo property.
The ActivatedRoute is implicit in a RouterLink directive.
Notice that the path goes up a level using the ../ syntax.
If the current crisis id is 3, the resulting path back to the crisis list is /crisis-center/;id=3;foo=foo.
The popup should stay open, even when switching between pages in the application, until the user closes it
by sending the message or canceling.
Clearly you can't put the popup in the same outlet as the other pages.
Until now, you've defined a single outlet and you've nested child routes
under that outlet to group routes together.
The router only supports one primary unnamed outlet per template.
A template can also have any number of named outlets.
Each named outlet has its own set of routes with their own components.
Multiple outlets can be displaying different content, determined by different routes, all at the same time.
在AppComponent中添加一个名叫“popup”的出口,就在无名出口的下方。
Add an outlet named "popup" in the AppComponent, directly below the unnamed outlet.
Create a new component named ComposeMessageComponent in src/app/compose-message.component.ts.
It displays a simple form with a header, an input box for the message,
and two buttons, "Send" and "Cancel".
The path and component properties should be familiar.
There's a new property, outlet, set to 'popup'.
This route now targets the popup outlet and the ComposeMessageComponent will display there.
Although the compose route is pinned to the "popup" outlet, that's not sufficient for wiring the route to a RouterLink directive.
You have to specify the named outlet in a link parameters array and bind it to the RouterLink with a property binding.
The link parameters array contains an object with a single outlets property whose value
is another object keyed by one (or more) outlet names.
In this case there is only the "popup" outlet property and its value is another link parameters array that specifies the compose route.
意思是,当用户点击此链接时,在路由出口popup中显示与compose路由相关联的组件。
You are in effect saying, when the user clicks this link, display the component associated with the compose route in the popup outlet.
当有且只有一个无名出口时,外部对象中的这个outlets对象并不是必须的。
This outlets object within an outer object was completely unnecessary
when there was only one route and one unnamed outlet to think about.
路由器假设这个路由指向了无名的主出口,并为我们创建这些对象。
The router assumed that your route specification targeted the unnamed primary outlet
and created these objects for you.
Routing to a named outlet has revealed a previously hidden router truth:
you can target multiple outlets with multiple routes in the same RouterLink directive.
这里我们实际上没能这样做。要想指向命名出口,我们就得使用一种更强大也更啰嗦的语法。
You're not actually doing that here.
But to target a named outlet, you must use the richer, more verbose syntax.
第二路由导航:在导航期间合并路由
Secondary route navigation: merging routes during navigation
导航到危机中心并点击“Contact”,我们将会在浏览器的地址栏看到如下URL:
Navigate to the Crisis Center and click "Contact".
you should see something like the following URL in the browser address bar.
http://.../crisis-center(popup:compose)
这个URL中有意思的部分是...后面的这些:
The interesting part of the URL follows the ...:
crisis-center是主导航。
The crisis-center is the primary navigation.
圆括号包裹的部分是第二路由。
Parentheses surround the secondary route.
第二路由包括一个出口名称(popup)、一个冒号分隔符和第二路由的路径(compose)。
The secondary route consists of an outlet name (popup), a colon separator, and the secondary route path (compose).
点击Heroes链接,并再次查看URL:
Click the Heroes link and look at the URL again.
http://.../heroes(popup:compose)
主导航的部分变化了,而第二路由没有变。
The primary navigation part has changed; the secondary route is the same.
路由器在导航树中对两个独立的分支保持追踪,并在URL中对这棵树进行表达。
The router is keeping track of two separate branches in a navigation tree and generating a representation of that tree in the URL.
You can add many more outlets and routes, at the top level and in nested levels, creating a navigation tree with many branches.
The router will generate the URL to go with it.
You can tell the router to navigate an entire tree at once by filling out the outlets object mentioned above.
Then pass that object inside a link parameters array to the router.navigate method.
有空的时候你可以自行试验这些可能性。
Experiment with these possibilities at your leisure.
Each secondary outlet has its own navigation, independent of the navigation driving the primary outlet.
Changing a current route that displays in the primary outlet has no effect on the popup outlet.
That's why the popup stays visible as you navigate among the crises and heroes.
Clicking the "send" or "cancel" buttons does clear the popup view.
To see how, look at the closePopup() method again:
src/app/compose-message.component.ts (closePopup)
closePopup(){// Providing a `null` value to the named outlet// clears the contents of the named outletthis.router.navigate([{ outlets:{ popup:null}}]);}
Like the array bound to the ContactRouterLink in the AppComponent,
this one includes an object with an outlets property.
The outlets property value is another object with outlet names for keys.
The only named outlet is 'popup'.
This time, the value of 'popup' is null. That's not a route, but it is a legitimate value.
Setting the popup RouterOutlet to null clears the outlet and removes
the secondary popup route from the current URL.
里程碑5:路由守卫
Milestone 5: Route guards
现在,任何用户都能在任何时候导航到任何地方。
但有时候这样是不对的。
At the moment, any user can navigate anywhere in the application anytime.
That's not always the right thing to do.
该用户可能无权导航到目标组件。
Perhaps the user is not authorized to navigate to the target component.
可能用户得先登录(认证)。
Maybe the user must login (authenticate) first.
在显示目标组件前,我们可能得先获取某些数据。
Maybe you should fetch some data before you display the target component.
在离开组件前,我们可能要先保存修改。
You might want to save pending changes before leaving a component.
我们可能要询问用户:你是否要放弃本次更改,而不用保存它们?
You might ask the user if it's OK to discard pending changes rather than save them.
我们可以往路由配置中添加守卫,来处理这些场景。
You can add guards to the route configuration to handle these scenarios.
守卫返回一个值,以控制路由器的行为:
A guard's return value controls the router's behavior:
如果它返回true,导航过程会继续
If it returns true, the navigation process continues.
如果它返回false,导航过程会终止,且用户会留在原地。
If it returns false, the navigation process stops and the user stays put.
守卫还可以告诉路由器导航到别处,这样也取消当前的导航。
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.
You can have multiple guards at every level of a routing hierarchy.
The router checks the CanDeactivate() and CanActivateChild() guards first, from the 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.
我们会在接下来的小节中看到一些例子。
There are several examples over the next few sections.
Applications often restrict access to a feature area based on who the user is.
You could permit access only to authenticated users or to users with a specific role.
You might block or limit access until the user's account is activated.
CanActivate守卫是一个管理这些导航类业务规则的工具。
The CanActivate guard is the tool to manage these navigation business rules.
In this next section, you'll extend the crisis center with some new administrative features.
Those features aren't defined yet.
But you can start by adding a new feature module named AdminModule.
创建一个admin目录,它带有一个特性模块文件、一个路由配置文件和一些支持性组件。
Create an admin folder with a feature module file, a routing configuration file, and supporting components.
The admin feature module contains the AdminComponent used for routing within the
feature module, a dashboard route and two unfinished components to manage crises and heroes.
Since the admin dashboard RouterLink is an empty path route in the AdminComponent, it
is considered a match to any route within the admin feature area.
You only want the Dashboard link to be active when the user visits that route.
Adding an additional binding to the Dashboard routerLink,
[routerLinkActiveOptions]="{ exact: true }", marks the ./ link as active when
the user navigates to the /admin URL and not when navigating to any of the child routes.
Looking at the child route under the AdminComponent, there is a path and a children
property but it's not using a component.
You haven't made a mistake in the configuration.
You've defined a component-less route.
The goal is to group the Crisis Center management routes under the admin path.
You don't need a component to do it.
A component-less route makes it easier to guard child routes.
This is a general purpose guard—you can imagine other features
that require authenticated users—so you create an
auth-guard.service.ts in the application root folder.
At the moment you're interested in seeing how guards work so the first version does nothing useful.
It simply logs to console and returns true immediately, allowing navigation to proceed:
The AuthGuard should call an application service that can login a user and retain information about the current user.
Here's a demo AuthService:
src/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()exportclassAuthService{
isLoggedIn:boolean=false;// store the URL so we can redirect after logging in
redirectUrl:string;
login():Observable<boolean>{returnObservable.of(true).delay(1000).do(val =>this.isLoggedIn =true);}
logout():void{this.isLoggedIn =false;}}
Although it doesn't actually log in, it has what you need for this discussion.
It has an isLoggedIn flag to tell you 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 the attempted URL so you can navigate to it after authenticating.
我们这就修改AuthGuard来调用它。
Revise the AuthGuard to call it.
src/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()exportclassAuthGuardimplementsCanActivate{
constructor(private authService:AuthService,private router:Router){}
canActivate(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):boolean{let url:string= state.url;returnthis.checkLogin(url);}
checkLogin(url:string):boolean{if(this.authService.isLoggedIn){returntrue;}// Store the attempted URL for redirectingthis.authService.redirectUrl = url;// Navigate to the login page with extrasthis.router.navigate(['/login']);returnfalse;}}
Notice that you inject the AuthService and the Router in the constructor.
You haven't provided the AuthService yet but it's good to know that you can inject helpful services into routing guards.
该守卫返回一个同步的布尔值。如果用户已经登录,它就返回true,导航会继续。
This guard returns a synchronous boolean result.
If the user is logged in, it returns true and the navigation continues.
The ActivatedRouteSnapshot contains the future route that will be activated and the RouterStateSnapshot
contains the futureRouterState of the application, should you pass through the guard check.
If the user is not logged in, you store the attempted URL the user came from using the RouterStateSnapshot.url and
tell the router to navigate to a login page—a page you haven't created yet.
This secondary navigation automatically cancels the current navigation; checkLogin() returns
false just to be clear about that.
You need a LoginComponent for the user to log in to the app. After logging in, you'll redirect
to the stored URL if available, or use the default URL.
There is nothing new about this component or the way you wire it into the router configuration.
Register a /login route in the login-routing.module.ts and add the necessary providers to the providers
array. In app.module.ts, import the LoginComponent and add it to the AppModuledeclarations.
Import and add the LoginRoutingModule to the AppModule imports as well.
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.
You can also protect child routes with the CanActivateChild guard.
The CanActivateChild guard is similar to the CanActivate guard.
The key difference is that it runs before any child route is activated.
我们要保护管理特性模块,防止它被非授权访问,还要保护这个特性模块内部的那些子路由。
You protected the admin feature module from unauthorized access.
You should also protect child routes within the feature module.
Extend the AuthGuard to protect when navigating between the admin routes.
Open auth-guard.service.ts and add the CanActivateChild interface to the imported tokens from the router package.
Next, implement the CanActivateChild method which takes the same arguments as the CanActivate method:
an ActivatedRouteSnapshot and RouterStateSnapshot.
The CanActivateChild method can return an Observable<boolean> or Promise<boolean> for
async checks and a boolean for sync checks.
This one returns a boolean:
Add the same AuthGuard to the component-less admin route to protect all other child routes at one time
instead of adding the AuthGuard to each route individually.
In the real world, you might have to accumulate the users changes.
You might have to validate across fields.
You might have to validate on the server.
You 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 you do about unapproved, unsaved changes when the user navigates away?
You can't just leave and risk losing the user's changes; that would be a terrible experience.
It's better to pause and let the user decide what to do.
If the user cancels, you'll stay put and allow more changes.
If the user approves, the app can save.
You still might delay navigation until the save succeeds.
If you let the user move to the next screen immediately and
the save were to fail (perhaps the data are ruled invalid), you would lose the context of the error.
You can't block while waiting for the server—that's not possible in a browser.
You need to stop the navigation while you wait, asynchronously, for the server
to return with its answer.
我们需要CanDeactivate守卫。
You need the CanDeactivate guard.
Cancel and save
我们的范例应用不会与服务器通讯。
幸运的是,我们有另一种方式来演示异步的路由器钩子。
The sample application doesn't talk to a server.
Fortunately, you 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.
Instead, the app updates the entity when the user presses the Save button and
discards the changes when the user presses the Cancel button.
这两个按钮都会在保存或取消之后导航回危机列表。
Both buttons navigate back to the crisis list after save or cancel.
src/app/crisis-center/crisis-detail.component.ts (cancel and save methods)
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?
都不行。我们应该弹出一个确认对话框来要求用户明确做出选择,该对话框会用异步的方式等用户做出选择。
This demo does neither. Instead, it asks the user to make that choice explicitly
in a confirmation dialog box that waits asynchronously for the user's
answer.
You could wait for the user's answer with synchronous, blocking code.
The 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.
DialogService(为了在应用级使用,已经注入到了AppModule)就可以做到这些。
The DialogService, provided in the AppModule for app-wide use, does the asking.
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).
Create a guard that checks for the presence of a canDeactivate method in a component—any component.
The CrisisDetailComponent will have this method.
But the guard doesn't have to know that.
The guard shouldn't know the details of any component's deactivation method.
It need only detect that the component has a canDeactivate() method and call it.
This approach makes the guard reusable.
Alternatively, you could make a component-specific CanDeactivate guard for the CrisisDetailComponent.
The canDeactivate() method provides you with the current
instance of the component, the current ActivatedRoute,
and RouterStateSnapshot in case you needed to access
some external information. This would be useful if you only
wanted to use this guard for this component and needed to get
the component's properties or confirm whether the router should allow navigation away from it.
import{Injectable}from'@angular/core';import{CanDeactivate,ActivatedRouteSnapshot,RouterStateSnapshot}from'@angular/router';import{CrisisDetailComponent}from'./crisis-center/crisis-detail.component';@Injectable()exportclassCanDeactivateGuardimplementsCanDeactivate<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 unchangedif(!component.crisis || component.crisis.name === component.editName){returntrue;}// Otherwise ask the user with the dialog service and return its// promise which resolves to true or false when the user decidesreturn component.dialogService.confirm('Discard changes?');}}
看看CrisisDetailComponent组件,我们已经实现了对未保存的更改进行确认的工作流。
Looking back at the CrisisDetailComponent, it implements the confirmation workflow for unsaved changes.
canDeactivate():Promise<boolean>|boolean{// Allow synchronous navigation (`true`) if no crisis or the crisis is unchangedif(!this.crisis ||this.crisis.name ===this.editName){returntrue;}// Otherwise ask the user with the dialog service and return its// promise which resolves to true or false when the user decidesreturnthis.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 falsy (stay put).
This worked well, but there's a better way.
If you were using a real world API, there might be some delay before the data to display is returned from the server.
You don't want to display a blank component while waiting for the data.
It's preferable to pre-fetch data from the server so it's ready the
moment the route is activated. This also allows you to handle errors before routing to the component.
There's no point in navigating to a crisis detail for an id that doesn't have a record.
It'd be better to send the user back to the Crisis List that shows only valid crisis centers.
总之,你希望的是只有当所有必要数据都已经拿到之后,才渲染这个路由组件。
In summary, you want to delay rendering the routed component until all necessary data have been fetched.
The experience might be better if all of this were handled first, before the route is activated.
A CrisisDetailResolver service could retrieve a Crisis or navigate away if the Crisis does not exist
before activating the route and creating the CrisisDetailComponent.
在“危机中心”特性区中创建crisis-detail-resolver.service.ts文件。
Create the crisis-detail-resolver.service.ts file within the Crisis Center feature area.
Take the relevant parts of the crisis retrieval logic in CrisisDetailComponent.ngOnInit
and move them into the CrisisDetailResolver.
Import the Crisis model, CrisisService, and the Router
so you can navigate elsewhere if you can't fetch the crisis.
为了更明确一点,可以实现一个带有Crisis类型的Resolve接口。
Be explicit. Implement the Resolve interface with a type of Crisis.
Inject the CrisisService and Router and implement the resolve() method.
That method could return a Promise, an Observable, or a synchronous return value.
The CrisisService.getCrisis method returns a promise.
Return that promise to prevent the route from loading until the data is fetched.
If it doesn't return a valid Crisis, navigate the user back to the CrisisListComponent,
canceling the previous in-flight navigation to the CrisisDetailComponent.
The CrisisDetailComponent should no longer fetch the crisis.
Update the CrisisDetailComponent to get the crisis from the ActivatedRoute.data.crisis property instead;
that's where you said it should be when you re-configured the route.
It will be there when the CrisisDetailComponent ask for it.
The router's Resolve interface is optional.
The CrisisDetailResolver doesn't inherit from a base class.
The router looks for that method and calls it if found.
Rely on the router to call the resolver.
Don't worry about all the ways that the user could navigate away.
That's the router's job. Write this class and let the router take it from there.
本里程碑中与危机中心有关的代码如下:
The relevant Crisis Center code for this milestone follows.
In the route parameters example, you only dealt with parameters specific to
the route, but what if you wanted optional parameters available to all routes?
This is where query parameters come into play.
Add the NavigationExtras object to the router.navigate method that navigates you to the /login route.
src/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()exportclassAuthGuardimplementsCanActivate,CanActivateChild{
constructor(private authService:AuthService,private router:Router){}
canActivate(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):boolean{let url:string= state.url;returnthis.checkLogin(url);}
canActivateChild(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):boolean{returnthis.canActivate(route, state);}
checkLogin(url:string):boolean{if(this.authService.isLoggedIn){returntrue;}// Store the attempted URL for redirectingthis.authService.redirectUrl = url;// Create a dummy session idlet sessionId =123456789;// Set our navigation extras object// that contains our global query params and fragmentlet navigationExtras:NavigationExtras={
queryParams:{'session_id': sessionId },
fragment:'anchor'};// Navigate to the login page with extrasthis.router.navigate(['/login'], navigationExtras);returnfalse;}}
You can also preserve query parameters and fragments across navigations without having to provide them
again when navigating. In the LoginComponent, you'll add an object as the
second argument in the router.navigate function
and provide the preserveQueryParams and preserveFragment to pass along the current query parameters
and fragment to the next route.
src/app/login.component.ts (preserve)
// Set our navigation extras object// that passes on our global query params and fragmentlet navigationExtras:NavigationExtras={
preserveQueryParams:true,
preserveFragment:true};// Redirect the userthis.router.navigate([redirect], navigationExtras);
由于要在登录后导航到危机管理特征区的路由,所以我们还得更新它,来处理这些全局查询参数和片段。
Since you'll be navigating to the Admin Dashboard route after logging in, you'll update it to handle the
query parameters and fragment.
src/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>
`})exportclassAdminDashboardComponentimplementsOnInit{
sessionId:Observable<string>;
token:Observable<string>;
constructor(private route:ActivatedRoute){}
ngOnInit(){// Capture the session ID if availablethis.sessionId =this.route
.queryParams
.map(params=>params['session_id']||'None');// Capture the fragment if availablethis.token =this.route
.fragment
.map(fragment => fragment ||'None');}}
Query Parameters and Fragments are also available through the ActivatedRoute service.
Just like route parameters, the query parameters and fragments are provided as an Observable.
The updated Crisis Admin component feeds the Observable directly into the template using the AsyncPipe.
To see the URL changes in the browser address bar of the live example,
open it again in the Plunker editor by clicking the icon in the upper right,
then pop out the preview window by clicking the blue 'X' button in the upper right corner.
Now, you can click on the Admin button, which takes you to the Login
page with the provided query params and fragment. After you click the login button, notice that
you have been redirected to the Admin Dashboard page with the query params and fragment still intact.
我们可以用这些持久化信息来携带需要为每个页面都提供的信息,如认证令牌或会话的ID等。
You can use these persistent bits of information for things that need to be provided across pages like
authentication tokens or session ids.
As you've worked through the milestones, the application has naturally gotten larger.
As you continue to build out feature areas, the overall application size will continue to grow.
At some point you'll reach a tipping point where the application takes long time to load.
You're already made part way there. By organizing the application into modules—AppModule,
HeroesModule, AdminModule and CrisisCenterModule—you
have natural candidates for lazy loading.
Some modules, like AppModule, must be loaded from the start.
But others can and should be lazy loaded.
The AdminModule, for example, is needed by a few authorized users, so
you should only load it when requested by the right people.
惰性加载路由配置
Lazy Loading route configuration
把admin-routing.module.ts中的admin路径从'admin'改为空路径''。
Change the adminpath in the admin-routing.module.ts from 'admin' to an empty string, '', the empty path.
The Router supports empty path routes;
use them to group routes together without adding any additional path segments to the URL.
Users will still visit /admin and the AdminComponent still serves as the Routing Component containing child routes.
Give it a loadChildren property (not a children property!), set to the address of the AdminModule.
The address is the AdminModule file location (relative to the app root),
followed by a # separator,
followed by the name of the exported module class, AdminModule.
When the router navigates to this route, it uses the loadChildren string to dynamically load the AdminModule.
Then it adds the AdminModule routes to its current route configuration.
Finally, it loads the requested route to the destination admin component.
The lazy loading and re-configuration happen just once, when the route is first requested;
the module and routes are available immediately for subsequent requests.
Angular provides a built-in module loader that supports SystemJS to load modules asynchronously. If you were
using another bundling tool, such as Webpack, you would use the Webpack mechanism for asynchronously loading modules.
Take the final step and detach the admin feature set from the main application.
The root AppModule must neither load nor reference the AdminModule or its files.
You're already protecting the AdminModule with a CanActivate guard that prevents unauthorized users from
accessing the admin feature area.
It redirects to the login page if the user is not authorized.
But the router is still loading the AdminModule even if the user can't visit any of its components.
Ideally, you'd only load the AdminModule if the user is logged in.
Open auth-guard.service.ts.
Import the CanLoad interface from @angular/router.
Add it to the AuthGuard class's implements list.
Then implement canLoad as follows:
The router sets the canLoad() method's route parameter to the intended destination URL.
The checkLogin() method redirects to that URL once the user has logged in.
Now import the AuthGuard into the AppRoutingModule and add the AuthGuard to the canLoad
array for the admin route.
The completed admin route looks like this:
This may seem like what the app has been doing all along. Not quite.
The AppModule is loaded when the application starts; that's eager loading.
Now the AdminModule loads only when the user clicks on a link; that's lazy loading.
Preloading is something in between.
Consider the Crisis Center.
It isn't the first view that a user sees. By default, the Heroes are the first view.
For the smallest initial payload and fastest launch time,
you should eagerly load the AppModule and the HeroesModule.
You could lazy load the Crisis Center.
But you're almost certain that the user will visit the Crisis Center within minutes of launching the app.
Ideally, the app would launch with just the AppModule and the HeroesModule loaded
and then, almost immediately, load the CrisisCenterModule in the background.
By the time the user navigates to the Crisis Center, its module will have been loaded and ready to go.
After each successful navigation, the router looks in its configuration for an unloaded module that it can preload.
Whether it preloads a module, and which modules it preloads, depends upon the preload strategy.
Router内置了两种预加载策略:
The Router offers two preloading strategies out of the box:
完全不预加载,这是默认值。惰性加载的特性区仍然会按需加载。
No preloading at all which is the default. Lazy loaded feature areas are still loaded on demand.
Out of the box, the router either never preloads, or preloads every lazy load module.
The Router also supports custom preloading strategies for
fine control over which modules to preload and when.
In this next section, you'll update the CrisisCenterModule to load lazily
by default and use the PreloadAllModules strategy
to load it (and all other lazy loaded modules) as soon as possible.
The second argument in the RouterModule.forRoot method takes an object for additional configuration options.
The preloadingStrategy is one of those options.
Add the PreloadAllModules token to the forRoot call:
When you visit http://localhost:3000, the /heroes route loads immediately upon launch
and the router starts loading the CrisisCenterModule right after the HeroesModule loads.
意外的是,AdminModule没有预加载,有什么东西阻塞了它。
Surprisingly, the AdminModule does not preload. Something is blocking it.
You added a CanLoad guard to the route in the AdminModule a few steps back
to block loading of that module until the user is authorized.
That CanLoad guard takes precedence over the preload strategy.
Preloading every lazy loaded modules works well in many situations,
but it isn't always the right choice, especially on mobile devices and over low bandwidth connections.
You may choose to preload only certain feature modules, based on user metrics and other business and technical factors.
使用自定义预加载策略,我们可以控制路由器预加载哪些路由以及如何加载。
You can control what and how the router preloads with a custom preloading strategy.
In this section, you'll add a custom strategy that only preloads routes whose data.preload flag is set to true.
Recall that you can add anything to the data property of a route.
import'rxjs/add/observable/of';import{Injectable}from'@angular/core';import{PreloadingStrategy,Route}from'@angular/router';import{Observable}from'rxjs/Observable';@Injectable()exportclassSelectivePreloadingStrategyimplementsPreloadingStrategy{
preloadedModules:string[]=[];
preload(route:Route, load:()=>Observable<any>):Observable<any>{if(route.data && route.data['preload']){// add the route path to the preloaded module arraythis.preloadedModules.push(route.path);// log the route path to the console
console.log('Preloaded: '+ route.path);return load();}else{returnObservable.of(null);}}}
An implementation of preload must return an Observable.
If the route should preload, it returns the observable returned by calling the loader function.
If the route should not preload, it returns an Observable of null.
在这个例子中,preload方法只有在路由的data.preload标识为真时才会加载该路由。
In this sample, the preload method loads the route if the route's data.preload flag is truthy.
Once the application loads the initial route, the CrisisCenterModule is preloaded.
Verify this by logging in to the Admin feature area and noting that the crisis-center is listed in the Preloaded Modules.
It's also logged to the browser's console.
You put a lot of effort into configuring the router in several routing module files
and were careful to list them in the proper order.
Are routes actually evaluated as you planned?
How is the router really configured?
You can inspect the router's current configuration any time by injecting it and
examining its config property.
For example, update the AppModule as follows and look in the browser console window
to see the finished route configuration.
You've covered a lot of ground in this guide and the application is too big to reprint here.
Please visit the live example / downloadable example
where you can download the final source code.
附录
Appendices
本章剩下的部分是一组附录,它详尽阐述了我们曾匆匆带过的一些知识点。
The balance of this guide is a set of appendices that
elaborate some of the points you covered quickly above.
该附件中的内容不是必须的,感兴趣的人才需要阅读它。
The appendix material isn't essential. Continued reading is for the curious.
附录:链接参数数组
Appendix: link parameters array
链接参数数组保存路由导航时所需的成分:
A link parameters array holds the following ingredients for router navigation:
指向目标组件的那个路由的路径(path)
The path of the route to the destination component.
必备路由参数和可选路由参数,它们将进入该路由的URL
Required and optional route parameters that go into the route URL.
我们可以把RouterLink指令绑定到一个数组,就像这样:
You can bind the RouterLink directive to such an array like this:
<a [routerLink]="['/heroes']">Heroes</a>
在指定路由参数时,我们写过一个双元素的数组,就像这样:
You've written a two element array when specifying a route parameter like this:
this.router.navigate(['/hero', hero.id]);
我们可以在对象中提供可选的路由参数,就像这样:
You can provide optional route parameters in an object like this:
These three examples cover the need for an app with one level routing.
The moment you add a child router, such as the crisis center, you create new link array possibilities.
回忆一下,我们曾为危机中心指定过一个默认的子路由,以便能使用这种简单的RouterLink。
Recall that you specified a default child route for the crisis center so this simple RouterLink is fine.
In sum, you 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.
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.
Modern HTML5 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.
下面是危机中心的URL在“HTML 5 pushState”风格下的样子:
Here's the Crisis Center URL in this "HTML5 pushState" style:
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.
localhost:3002/src/#/crisis-center/
路由器通过两种LocationStrategy提供商来支持所有这些风格:
The router supports both styles with two LocationStrategy providers:
The RouterModule.forRoot function sets the LocationStrategy to the PathLocationStrategy,
making it the default strategy.
You can switch to the HashLocationStrategy with an override during the bootstrapping process if you prefer it.
You must choose a strategy and you 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.
Almost all Angular projects should use the default HTML5 style.
It produces URLs that are easier for users to understand.
And it preserves the option to do server-side rendering later.
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.
只有当应用的URL看起来像是标准的Web URL,中间没有hash(#)时,这个选项才能生效。
This option is only available if application URLs look like normal web URLs
without hashes (#) in the middle.
除非你有强烈的理由不得不使用hash路由,否则就应该坚决使用默认的HTML 5路由风格。
Stick with the default unless you have a compelling reason to
resort to hash routes.
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.
You can go old-school with the HashLocationStrategy by
providing the useHash: true in an object as the second argument of the RouterModule.forRoot
in the AppModule.