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.
延续上一步教程
Where you left off
在继续《英雄指南》之前,我们先验证一下目录结构:
Before continuing with the Tour of Heroes, verify that you have the following structure.
angular-tour-of-heroes
src
app
app.component.ts
app.module.ts
hero.service.ts
hero.ts
hero-detail.component.ts
mock-heroes.ts
main.ts
index.html
styles.css
systemjs.config.js
tsconfig.json
node_modules ...
package.json
让应用代码保持转译和运行
Keep the app transpiling and running
打开终端/控制台窗口,运行如下命令:
Enter the following command in the terminal window:
This command runs the TypeScript compiler in "watch mode", recompiling automatically when the code changes.
The command simultaneously launches the app in a browser and refreshes the browser when the code changes.
在后续构建《英雄指南》过程中,应用能持续运行,而不用中断服务来编译或刷新浏览器。
You can keep building the Tour of Heroes without pausing to recompile or refresh the browser.
行动计划
Action plan
下面是我们的计划:
Here's the plan:
把AppComponent变成应用程序的“壳”,它只处理导航
Turn AppComponent into an application shell that only handles navigation.
把现在由AppComponent关注的英雄们移到一个独立的HeroesComponent中
Relocate the Heroes concerns within the current AppComponent to a separate HeroesComponent.
添加路由
Add routing.
创建一个新的DashboardComponent组件
Create a new DashboardComponent.
把仪表盘加入导航结构中
Tie the Dashboard into the navigation structure.
路由是导航的另一个名字。路由器就是从一个视图导航到另一个视图的机制。
Routing is another name for navigation. The router is the mechanism for navigating from view to view.
拆分 AppComponent
Splitting the AppComponent
现在的应用会加载AppComponent组件,并且立刻显示出英雄列表。
The current app loads AppComponent and immediately displays the list of heroes.
我们修改后的应用将提供一个壳,它会选择仪表盘和英雄列表视图之一,然后默认显示它。
The revised app should present a shell with a choice of views (Dashboard and Heroes)
and then default to one of them.
AppComponent is already dedicated to Heroes.
Instead of moving the code out of AppComponent, rename it to HeroesComponent
and create a separate AppComponent shell.
步骤如下:
Do the following:
把app.component.ts文件改名为heroes.component.ts。
Rename the app.component.ts file to heroes.component.ts.
把AppComponent类改名为HeroesComponent(注意,只在这一个文件中改名)。
Rename the AppComponent class as HeroesComponent (rename locally, only in this file).
Go back to the HeroesComponent and remove the HeroService from its providers array.
We are promoting this service from the HeroesComponent to the root NgModule.
We do not want two copies of this service at two different levels of our app.
Instead of displaying automatically, heroes should display after users click a button.
In other words, users should be able to navigate to the list of heroes.
The Angular router is an external, optional Angular NgModule called RouterModule.
The router is a combination of multiple provided services (RouterModule),
multiple directives (RouterOutlet, RouterLink, RouterLinkActive),
and a configuration (Routes). You'll configure the routes first.
The forRoot() method is called because a configured router is provided at the app's root.
The forRoot() method supplies the Router service providers and directives needed for routing, and
performs the initial navigation based on the current browser URL.
If you paste the path, /heroes, into the browser address bar at the end of the URL,
the router should match it to the heroes route and display the HeroesComponent.
However, you have to tell the router where to display the component.
To do this, you can add a <router-outlet> element at the end of the template.
RouterOutlet is one of the directives provided by the RouterModule.
The router displays each component immediately below the <router-outlet> as users navigate through the app.
Users shouldn't have to paste a route URL into the address bar.
Instead, add an anchor tag to the template that, when clicked, triggers navigation to the HeroesComponent.
Note the routerLink binding in the anchor tag.
The RouterLink directive (another of the RouterModule directives) is bound to a string
that tells the router where to navigate when the user clicks the link.
Since the link is not dynamic, a routing instructionis defined with a one-time binding to the route path.
Looking back at the route configuration, you can confirm that '/heroes' is the path of the route to the HeroesComponent.
The AppComponent is now attached to a router and displays routed views.
For this reason, and to distinguish it from other kinds of components,
this component type is called a router component.
Routing only makes sense when multiple views exist.
To add another view, create a placeholder DashboardComponent, which users can navigate to and from.
To teach app.module.ts to navigate to the dashboard,
import the dashboard component and
add the following route definition to the Routes array of definitions.
Currently, the browser launches with / in the address bar.
When the app starts, it should show the dashboard and
display a /dashboard URL in the browser address bar.
我们可以使用重定向路由来实现它。把下面的内容添加到路由定义数组中:
To make this happen, use a redirect route. Add the following
to the array of route definitions:
Earlier, you removed the HeroService from the providers array of HeroesComponent
and added it to the providers array of AppModule.
That move created a singleton HeroService instance, available to all components of the app.
Angular injects HeroService and you can use it in the DashboardComponent.
获取英雄数据
Get heroes
打开dashboard.component.ts文件,并添加下列import语句。
In dashboard.component.ts, add the following import statements.
We need OnInit interface because we'll initialize the heroes in the ngOnInit method as we've done before.
We need the Hero and HeroService symbols in order to reference those types.
我们现在就实现DashboardComponent类,像这样:
Now create the DashboardComponent class like this:
src/app/dashboard.component.ts (class)
exportclassDashboardComponentimplementsOnInit{
heroes:Hero[]=[];
constructor(private heroService:HeroService){}
ngOnInit():void{
this.heroService.getHeroes()
.then(heroes =>this.heroes = heroes.slice(1,5));
}
}
我们在之前的HeroesComponent中也看到过类似的逻辑:
This kind of logic is also used in the HeroesComponent:
创建一个heroes数组属性。
Define a heroes array property.
在构造函数中注入HeroService,并且把它保存在一个私有的heroService字段中。
Inject the HeroService in the constructor and hold it in a private heroService field.
在 Angular 的ngOnInit生命周期钩子里面调用服务来获得英雄数据。
Call the service to get heroes inside the Angular ngOnInit() lifecycle hook.
在仪表盘中我们用Array.slice方法提取了四个英雄(第2、3、4、5个)。
In this dashboard you specify four heroes (2nd, 3rd, 4th, and 5th) with the Array.slice method.
刷新浏览器,在这个新的仪表盘中就看到了四个英雄。
Refresh the browser to see four hero names in the new dashboard.
While the details of a selected hero displays at the bottom of the HeroesComponent,
users should be able to navigate to the HeroDetailComponent in the following additional ways:
从Dashboard(仪表盘)导航到一个选定的英雄。
From the dashboard to a selected hero.
从Heroes(英雄列表)导航到一个选定的英雄。
From the heroes list to a selected hero.
把一个指向该英雄的“深链接” URL 粘贴到浏览器的地址栏。
From a "deep link" URL pasted into the browser address bar.
The new route is unusual in that you must tell the HeroDetailComponent which hero to show.
You didn't have to tell the HeroesComponent or the DashboardComponent anything.
The /detail/ part of the URL is constant. The trailing numeric id changes from hero to hero.
You need to represent the variable part of the route with a parameter (or token) that stands for the hero's id.
You didn't add a 'Hero Detail' link to the template because users
don't click a navigation link to view a particular hero;
they click a hero name, whether the name displays on the dashboard or in the heroes list.
要想支持“点击英雄”,就得先对HeroDetailComponent进行修改,好让我们能导航到它。
You don't need to add the hero clicks until the HeroDetailComponent
is revised and ready to be navigated to.
修改HeroDetailComponent
Revise the HeroDetailComponent
在重写HeroDetailComponent之前,我们先看看它现在的样子:
Here's what the HeroDetailComponent looks like now:
You'll no longer receive the hero in a parent component property binding.
The new HeroDetailComponent should take the id parameter from the params Observable
in the ActivatedRoute service and use the HeroService to fetch the hero with that id.
先添加下列导入语句:
Add the following imports:
src/app/hero-detail.component.ts
// Keep the Input import for now, you'll remove it later:
Inside the ngOnInit() lifecycle hook, use the params Observable to
extract the id parameter value from the ActivatedRoute service
and use the HeroService to fetch the hero with that id.
If a user re-navigates to this component while a getHero request is still processing,
switchMap cancels the old request and then calls HeroService.getHero() again.
As described in the ActivatedRoute: the one-stop-shop for route information
section of the Routing & Navigation page,
the Router manages the observables it provides and localizes
the subscriptions. The subscriptions are cleaned up when the component is destroyed, protecting against
memory leaks, so you don't need to unsubscribe from the route paramsObservable.
In the previous code snippet, HeroService doesn't have a getHero() method. To fix this issue,
open HeroService and add a getHero() method that filters the heroes list from getHeroes() by id.
To navigate somewhere else, users can click one of the two links in the AppComponent or click the browser's back button.
Now add a third option, a goBack() method that navigates backward one step in the browser's history stack
using the Location service you injected previously.
Going back too far could take users out of the app.
In a real app, you can prevent this issue with the CanDeactivate guard.
Read more on the CanDeactivate page.
然后,我们通过一个事件绑定把此方法绑定到模板底部的 Back(后退)按钮上。
You'll wire this method with an event binding to a Back button that you'll add to the component template.
Although the dashboard heroes are presented as button-like blocks, they should behave like anchor tags.
When hovering over a hero block, the target URL should display in the browser status bar
and the user should be able to copy the link or open the hero detail view in a new tab.
To achieve this effect, reopen dashboard.component.html and replace the repeated <div *ngFor...> tags
with <a> tags. Change the opening <a> tag to the following:
Notice the [routerLink] binding.
As described in the Router links section of this page,
top-level navigation in the AppComponent template has router links set to fixed paths of the
destination routes, "/dashboard" and "/heroes".
这次,我们绑定了一个包含链接参数数组的表达式。
该数组有两个元素,目标路由和一个用来设置当前英雄的 id 值的路由参数。
This time, you're binding to an expression containing a link parameters array.
The array has two elements: the path of
the destination route and a route parameter set to the value of the current hero's id.
Almost 20 lines of AppModule are devoted to configuring four routes.
Most applications have many more routes and they add guard services
to protect against unwanted or unauthorized navigations. (Read more about guard services in the Route Guards
section of the Routing & Navigation page.)
Routing considerations could quickly dominate this module and obscure its primary purpose, which is to
establish key facts about the entire app for the Angular compiler.
It's a good idea to refactor the routing configuration into its own class.
The current RouterModule.forRoot() produces an Angular ModuleWithProviders,
a class dedicated to routing should be a routing module.
For more information, see the Milestone #2: The Routing Module
section of the Routing & Navigation page.
按约定,路由模块的名字应该包含 “Routing”,并与导航到的组件所在的模块的名称看齐。
By convention , a routing module name contains the word "Routing" and
aligns with the name of the module that declares the components navigated to.
The Routing Module adds RouterModule to exports so that the components in the companion module have access to Router declarables ,
such as RouterLink and RouterOutlet.
无declarations!声明是关联模块的任务。
There are no declarations. Declarations are the responsibility of the companion module.
如果有守卫服务,把它们添加到本模块的providers中(本例子中没有守卫服务)。
If you have guard services, the Routing Module adds module providers. (There are none in this example.)
修改 AppModule
Update AppModule
删除AppModule中的路由配置,并导入AppRoutingModule
(使用 ES import语句导入,并将它添加到NgModule.imports列表)。
Delete the routing configuration from AppModule and import the AppRoutingModule.
Use an ES import statement and add it to the NgModule.imports list.
下面是修改后的AppModule,与重构前的对比:
Here is the revised AppModule, compared to its pre-refactor state:
In the HeroesComponent,
the current template exhibits a "master/detail" style with the list of heroes
at the top and details of the selected hero below.
You'll no longer show the full HeroDetailComponent here.
Instead, you'll display the hero detail on its own page and route to it as you did in the dashboard.
However, when users select a hero from the list, they won't go to the detail page.
Instead, they'll see a mini detail on this page and have to click a button to navigate to the full detail page.
添加 mini 版英雄详情
Add the mini detail
在模板底部原来放<hero-detail>的地方添加下列 HTML 片段:
Add the following HTML fragment at the bottom of the template where the <hero-detail> used to be:
The hero's name is displayed in capital letters because of the uppercase pipe
that's included in the interpolation binding, right after the pipe operator ( | ).
First, move the template contents from heroes.component.ts
into a new heroes.component.html file.
Don't copy the backticks. As for heroes.component.ts, you'll
come back to it in a minute. Next, move the
styles contents into a new heroes.component.css file.
Now, back in the component metadata for heroes.component.ts,
delete template and styles, replacing them with
templateUrl and styleUrls respectively.
Set their properties to refer to the new files.
The HeroesComponent navigates to the HeroesDetailComponent in response to a button click.
The button's click event is bound to a gotoDetail() method that navigates imperatively
by telling the router where to go.
该方法需要对组件类做一些修改:
This approach requires the following changes to the component class:
从 Angular 路由器库导入Router
Import the Router from the Angular router library.
在构造函数中注入Router(与HeroService一起)
Inject the Router in the constructor, along with the HeroService.
实现gotoDetail(),调用路由器的navigate()方法
Implement gotoDetail() by calling the router navigate() method.
Note that you're passing a two-element link parameters array—a
path and the route parameter—to
the router navigate() method, just as you did in the [routerLink] binding
back in the DashboardComponent.
Here's the revised HeroesComponent class:
刷新浏览器,并开始点击。
我们能在应用中导航:从仪表盘到英雄详情再回来,从英雄列表到 mini 版英雄详情到英雄详情,再回到英雄列表。
我们可以在仪表盘和英雄列表之间跳来跳去。
Refresh the browser and start clicking.
Users can navigate around the app, from the dashboard to hero details and back,
from heroes list to the mini detail to the hero details and back to the heroes again.
我们已经满足了在本章开头设定的所有导航需求。
You've met all of the navigational requirements that propelled this page.
The app is functional but it needs styling.
The dashboard heroes should display in a row of rectangles.
You've received around 60 lines of CSS for this purpose, including some simple media queries for responsive design.
Add a hero-detail.component.css to the app
folder and refer to that file inside
the styleUrls array as you did for DashboardComponent.
Also, in hero-detail.component.ts, remove the hero property @Input decorator
and its import.
The provided CSS makes the navigation links in the AppComponent look more like selectable buttons.
You'll surround those links in <nav> tags.
在app目录下添加一个app.component.css文件,内容如下:
Add an app.component.css file to the app folder with the following content.
src/app/app.component.css (navigation styles)
h1 {
font-size:1.2em;
color:#999;
margin-bottom:0;
}
h2 {
font-size:2em;
margin-top:0;
padding-top:0;
}
nav a {
padding:5px10px;
text-decoration: none;
margin-top:10px;
display:inline-block;
background-color:#eee;
border-radius:4px;
}
nav a:visited, a:link {
color:#607D8B;
}
nav a:hover {
color:#039be5;
background-color:#CFD8DC;
}
nav a.active {
color:#039be5;
}
routerLinkActive指令
The routerLinkActive directive
Angular路由器提供了routerLinkActive指令,我们可以用它来为匹配了活动路由的 HTML 导航元素自动添加一个 CSS 类。
我们唯一要做的就是为它定义样式。真好!
The Angular router provides a routerLinkActive directive you can use to
add a class to the HTML navigation element whose route matches the active route.
All you have to do is define the style for it.
When you add styles to a component, you keep everything a component needs—HTML,
the CSS, the code—together in one convenient place.
It's easy to package it all up and re-use the component somewhere else.
我们也可以在所有组件之外创建应用级样式。
You can also create styles at the application level outside of any component.
The designers provided some basic styles to apply to elements across the entire app.
These correspond to the full set of master styles that you installed earlier during setup.
Here's an excerpt: