Angular_02_入门_中篇_Service_Routing

最终效果如下:


附录:

github地址: https://github.com/ixixii/angular_tutorial_demo.git

cd /Users/beyond/sg_angular/angular_01/angular-tutorial-demo

git status 

git add src/app/

git commit -m 'first commit'

git push https://github.com/ixixii/angular_tutorial_demo.git master 


.



Angular2官方文档地址:  https://angular.io/tutorial/toh-pt1

由于内容太长,分为上中下三篇


上篇已经介绍了1-5小点

中篇从第6点 Services章节开始


Services

The Tour of Heroes HeroesComponent is currently getting and displaying fake data.

After the refactoring in this tutorial, HeroesComponent will be lean and focused on supporting the view. 


It will also be easier to unit-test with a mock service.


Why services

Components shouldn't fetch or save data directly 

and they certainly shouldn't knowingly present fake data. 

They should focus on presenting data and delegate data access to a service.


In this tutorial, you'll create a HeroService that all application classes can use to get heroes. 

Instead of creating that service with new

you'll rely on Angular dependency injection to inject it into the HeroesComponent constructor.


Services are a great way to share information among classes that don't know each other


You'll create a MessageService and inject it in two places:

  1. in HeroService which uses the service to send a message.
  2. in MessagesComponent which displays that message.


Create the HeroService

Using the Angular CLI, create a service called hero.

content_copyng generate service hero

The command generates skeleton HeroService class in src/app/hero.service.ts 

The HeroService class should look like the following example.


src/app/hero.service.ts (new service)
content_copyimport { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class HeroService {

  constructor() { }

}

@Injectable()  services

Notice that the new service imports the Angular Injectable symbol 

and annotates the class with the @Injectable()decorator.


 This marks the class as one that participates in the dependency injection system


The HeroService class is going to provide an injectable service, 

and it can also have its own injected dependencies. 

尽管 It doesn't have any dependencies yet, but it will soon.


The @Injectable() decorator accepts a metadata object for the service, (意思是需要传入一个对象作为参数)

the same way the @Component() decorator did for your component classes.


Get hero data

The HeroService could get hero data from anywhere—a web service, local storage, or a mock data source.


Removing data access from components means : 

you can change your mind about the implementation anytime, without touching any components. 

They don't know how the service works.


The implementation in this tutorial will continue to deliver mock heroes.


Import the Hero and HEROES. (在hero.service.ts文件中编写)

content_copyimport { Hero } from './hero';
import { HEROES } from './mock-heroes';


Add a getHeroes method to return the mock heroes.

content_copygetHeroes(): Hero[] {
  return HEROES;
}


Provide the HeroService

You must make the HeroService available to the dependency injection system 

before Angular can inject it into the HeroesComponent, as you will do below


You do this by registering a provider

A provider is something that can create or deliver a service; 

in this case, it instantiates the HeroService class to provide the service.


Now, you need to make sure that the HeroService is registered as the provider of this service. 


You are registering it with an injector

which is the object that is responsible for choosing and injecting the provider where it is required.


By default, the Angular CLI command ng generate service registers a provider with the root injector for your service 

by including provider metadata in the @Injectable decorator.


If you look at the @Injectable() statement right before the HeroService class definition, 

you can see that the providedIn metadata value is 'root':

content_copy@Injectable({
  providedIn: 'root',
})

When you provide the service at the root level, 

Angular creates a single, shared instance of HeroService and injects into any class that asks for it. 


Registering the provider in the @Injectable metadata also allows Angular to optimize an app 

by removing the service if it turns out not to be used after all.

If you need to, you can register providers at different levels: 

1. in the HeroesComponent

2. in the AppComponent

3. in the AppModule


For instance, you could have told the CLI to provide the service at the module level automatically 

by appending --module=app.

content_copyng generate service hero --module=app

To learn more about providers and injectors, 

see the Dependency Injection guide.



The HeroService is now ready to plug into the HeroesComponent.

This is a interim(过渡的临时的) code sample that will allow you to provide and use the HeroService

At this point, the code will differ from the HeroService in the "final code review".


Update HeroesComponent

Open the HeroesComponent class file.

Delete the HEROES import, because you won't need that anymore. (数据源数组现在由service提供)

Import the HeroService instead.

src/app/heroes/heroes.component.ts (import HeroService)
content_copyimport { HeroService } from '../hero.service';

Replace the definition of the heroes property with a simple declaration.

content_copyheroes: Hero[];


Inject the HeroService

Add a private heroService parameter of type HeroService to the constructor(给HeroesComponent构造函数加一个参数).

核心代码: private heroService: HeroService  冒号后面是参数类型

content_copyconstructor(private heroService: HeroService) { }


The parameter simultaneously defines a private heroService property (同时定义了一个私有的成员属性)

and identifies it as a HeroService injection site. (将它识别为 HeroService的注入位置)


When Angular creates a HeroesComponent

the Dependency Injection system sets the heroService parameter to the singleton instance of HeroService.


Add getHeroes()

Create a function to retrieve the heroes from the service. (在heroes.component.ts文件里)

content_copygetHeroes(): void {
  this.heroes = this.heroService.getHeroes();
}




Call it in ngOnInit

While 尽管  you could call getHeroes() in the constructor,  但是 that's not the best practice.


Reserve the constructor for simple initialization 

such as wiring constructor parameters to properties.  (wire这个词这儿用得新鲜,头回见到)


The constructor shouldn't do anything. (尤其是耗时阻塞性质的)

It certainly shouldn't call a function that makes HTTP requests to a remote server as a real data service would.


Instead, call getHeroes() inside the ngOnInit lifecycle hook(生命周期函数) 

and let Angular call ngOnInit at an appropriate time 

after constructing a HeroesComponent instance.


content_copyngOnInit() {
  this.getHeroes();
}


See it run

After the browser refreshes, the app should run as before, 

showing a list of heroes and a hero detail view when you click on a hero name.

如图所示: 

 



Observable data

The HeroService.getHeroes() method has a synchronous signature(同步签名是什么鬼???)

which implies that the HeroService can fetch heroes synchronously. 


The HeroesComponent consumes the getHeroes() result  (这个consume用得好)

as if heroes could be fetched synchronously.

(heroes.component.ts文件getHeroes方法内)

content_copythis.heroes = this.heroService.getHeroes();

This will not work in a real app. 

You're getting away with it now because the service currently returns mock heroes. (get away with sth 侥幸成功)


But soon the app will fetch heroes from a remote server, 

which is an inherently(天性地) asynchronous operation.


The HeroService must wait for the server to respond, 

getHeroes() cannot return immediately with hero data, 

and the browser will not block while the service waits.


HeroService.getHeroes() must have an asynchronous signature of some kind.

1. It can take a callback. 

2. It could return a Promise

3. It could return an Observable.


In this tutorial, HeroService.getHeroes() will return an Observable in part 

because it will eventually use the Angular HttpClient.get method to fetch the heroes 

and HttpClient.get() returns an Observable.


什么是RxJS

RxJSReactiveX编程理念的JavaScript版本。

ReactiveX来自微软,它是一种针对异步数据流的编程。

简单来说,它将一切数据,包括HTTP请求,DOM事件或者普通数据等包装成流的形式,

然后用强大丰富的  操作符  对流进行处理,使你能以同步编程的方式处理异步数据,

并组合不同的  操作符  来轻松优雅的实现你所需要的功能。



Observable HeroService

Observable is one of the key classes in the RxJS library.


In a later tutorial on HTTP

you'll learn that Angular's HttpClient methods return RxJS Observables


In this tutorial, you'll simulate(模拟) getting data from the server with the RxJS  of() function.

1. Open the HeroService file 

2. and import the Observable and of symbols from RxJS.

src/app/hero.service.ts (Observable imports)
content_copyimport { Observable, of } from 'rxjs';

Replace the getHeroes method with this one. 

content_copygetHeroes(): Observable<Hero[]> {
  return of(HEROES);
}

of(HEROES) returns an Observable<Hero[]> 

that emits a single value, the array of mock heroes.

In the HTTP tutorial, you'll call HttpClient.get<Hero[]>() 

which also returns an Observable<Hero[]> 

that emits a single value, an array of heroes from the body of the HTTP response.

Subscribe in HeroesComponent

The HeroService.getHeroes method used to(以前) return a Hero[]

Now it returns an Observable<Hero[]>.


You'll have to adjust to that difference in HeroesComponent.

1. Find the getHeroes method 

2. and replace it with the following code : 

(shown side-by-side with the previous version for comparison)

代码如下:


比如之前的代码: 




Observable.subscribe() is the critical difference. (类似于Promise的.then回调)


The previous version assigns an array of heroes to the component's heroes property. 

The assignment occurs synchronously同步, as if the server could return heroes instantly 

or the browser could freeze the UI while it waited for the server's response.


That won't work when the HeroService is actually making requests of a remote server.


The new version waits for the Observable to emit the array of heroes

— which could happen now or several minutes from now. 


Then subscribe passes the emitted array to the callback, 

which sets the component's heroes property.


This asynchronous 异步 approach will work when the HeroService requests heroes from the server.


Show messages

In this section you will

  • add a MessagesComponent that displays app messages at the bottom of the screen.
  • create an injectable, app-wide MessageService for sending messages to be displayed
  • inject MessageService into the HeroService
  • display a message when HeroService fetches heroes successfully.


Create MessagesComponent

Use the CLI to create the MessagesComponent.

content_copyng generate component messages

The CLI creates the component files in the src/app/messages folder 

and declare MessagesComponent in AppModule.





如果app.module.ts出现了文本错误, 改一下就好了

如下所示:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
// 表单双向绑定
import { FormsModule } from '@angular/forms';


import { AppComponent } from './app.component';
import { HeroesComponent } from './heroes/heroes.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
import { MessagesComponent } from './messages/messages.component';


@NgModule({
  declarations: [
    AppComponent,
    HeroesComponent,
    HeroDetailComponent,
    MessagesComponent
  ],
  imports: [
    BrowserModule,
    // 添加双向绑定所需要的模块
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }


Modify the AppComponent template to display the generated MessagesComponent

将新建的子组件MessagesComponent 放到父组件的template模板中去

/src/app/app.component.html
<h1>{{title}}</h1>
<app-heroes></app-heroes>
<app-messages></app-messages>

You should see the default paragraph from MessagesComponent at the bottom of the page.

如下所示: 



Create the MessageService

Use the CLI to create the MessageService in src/app.

(在项目根目录下执行)

content_copyng generate service message



Open MessageService and replace its contents with the following.

维护一个消息数组

提供一个添加消息, 和 一个清空消息的方法

/src/app/message.service.ts 代码如下:
  1. import { Injectable } from '@angular/core';
  2.  
  3. @Injectable({
  4. providedIn: 'root',
  5. })
  6. export class MessageService {
  7. messages: string[] = [];
  8.  
  9. add(message: string) {
  10. this.messages.push(message);
  11. }
  12.  
  13. clear() {
  14. this.messages = [];
  15. }
  16. }


The service exposes its cache of messages and two methods: 

1.  add() a message to the cache 

2.   clear() the cache.


Inject it into the HeroService

1. Re-open the HeroService 

2. and import the MessageService.

/src/app/hero.service.ts (import MessageService)
import { MessageService } from './message.service';

Modify the constructor with a parameter that declares a private messageService property. 

Angular will inject the singleton MessageService into that property when it creates the HeroService.

constructor(private messageService: MessageService) { }

This is a typical "service-in-service" scenario (典型场景)

you inject the MessageService into the HeroService 

which is injected into the HeroesComponent.


Send a message from HeroService

Modify the getHeroes method to send a message when the heroes are fetched.

getHeroes(): Observable<Hero[]> {
  // TODO: send the message _after_ fetching the heroes
  this.messageService.add('HeroService: fetched heroes');
  return of(HEROES);
}


Display the message from HeroService

The MessagesComponent should display all messages, i

ncluding the message sent by the HeroService when it fetches heroes.


Open MessagesComponent and import the MessageService.

/src/app/messages/messages.component.ts (import MessageService)
import { MessageService } from '../message.service';


Modify the constructor with a parameter that declares a public messageService property. 

Angular will inject the singleton MessageService into that property when it creates the MessagesComponent.

将自动创建一个成员变量(指定为公有的)

constructor(public messageService: MessageService) {}

The messageService property must be public 

because you're about to bind to it in the template.

Angular only binds to public component properties.


Bind to the MessageService

Replace the CLI-generated MessagesComponent template with the following.

src/app/messages/messages.component.html
content_copy<div *ngIf="messageService.messages.length">

  <h2>Messages</h2>
  <button class="clear"
          (click)="messageService.clear()">clear</button>
  <div *ngFor='let message of messageService.messages'> {{message}} </div>

</div>


This template binds directly to the component's messageService属性(public).

  • The *ngIf only displays the messages area if there are messages to show.
  • An *ngFor presents the list of messages in repeated <div> elements.
  • An Angular event binding binds the button's click event to MessageService.clear().


The messages will look better 

when you add the private CSS styles to messages.component.css 

as listed in one of the "final code review" tabs below.

messages.component.css代码如下:

/* MessagesComponent's private CSS styles */
h2 {
  color: red;
  font-family: Arial, Helvetica, sans-serif;
  font-weight: lighter;
}
body {
  margin: 2em;
}
body, input[text], button {
  color: crimson;
  font-family: Cambria, Georgia;
}
 
button.class_btn_clear {
  font-family: Arial;
  background-color: #eee;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer;
  cursor: hand;
}
button:hover {
  background-color: #cfd8dc;
}
button:disabled {
  background-color: #eee;
  color: #aaa;
  cursor: auto;
}
button.class_btn_clear {
  color: #888;
  margin-bottom: 12px;
}



The browser refreshes and the page displays the list of heroes. 

Scroll to the bottom to see the message from the HeroService in the message area. 

Click the "clear" button and the message area disappears.

效果如下:



Final code review

Here are the code files discussed on this page and your app should look like this live example / download example.



src/app/hero.service.ts代码如下:

import { Injectable } from '@angular/core';
// rxjs 异步变同步
import { Observable, of } from 'rxjs'
// 
import { MessageService } from './message.service'



// 导入model
import { Hero } from './hero'
// 导入数组
import { HEROES } from './mock-heroes'

@Injectable({
  providedIn: 'root'
})
export class HeroService {
  // 构造函数, 自动生成一个私有的属性,并接收来自外部传递过来的MessageService
  constructor(private messageService: MessageService) {
  	// do nothing
  }

  // 提供数据源,异步变同步
  getHeroes(): Objservable<Hero[]> {
    // 当服务器返回数据回来时,提示一下
    this.messageService.add('服务器返回数据了')
  	// 使用of
  	return of(HEROES);
  }
}

src/app/message.service.ts代码如下:

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class MessageService {
  // 维护一个属性
  messages: string[] = [];
  // 提供一个add和一个clear方法
  add(message: string){
  	this.messages.push(message);
  }

  clear(){
  	this.messages = [];
  }
  constructor() { }
}

src/app/heroes/heroes.component.ts 代码如下:

import { Component, OnInit } from '@angular/core';
// 先导入上级目录的     .ts后缀可省
import { Hero } from '../hero'
// 数组 由service 提供
import { HeroService } from '../hero.service'


@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css']
})

export class HeroesComponent implements OnInit {
  // 数据源为一个空数组
  heroes : Hero[];

  

  // 用户点击的hero,默认为空
  selectedHero: Hero;

  // 绑定事件, 参数名是hero, 类型是Hero, 返回值是void
  onSelectFunction(hero: Hero) : void {
  	// this 关键字
  	this.selectedHero = hero;
  	console.log('点击了')
  }

  // 给构造函数加个参数
  constructor(private heroService: HeroService) { }

  // 提供一个函数 返回值是void
  getHeroes(): void {
  	// 异步请求结果(类似Promise.then)
  	this.heroService.getHeroes().subscribe(heroes => 
  		this.heroes = heroes;
  	)
  }

  ngOnInit() {
  	console.log('ngOnInit')
	// 这儿才进行数据的初始化
  	this.getHeroes()

  }

}

src/app/messages/messages.component.ts 代码如下:

import { Component, OnInit } from '@angular/core';
// 导入MessageService
import { MessageService } from '../message.service'

@Component({
  selector: 'app-messages',
  templateUrl: './messages.component.html',
  styleUrls: ['./messages.component.css']
})
export class MessagesComponent implements OnInit {
  // 貌似private也可以, 组件模板要用到消息列表
  constructor(public messageService : MessageService) { }

  ngOnInit() {
  }

}

src/app/messages/messages.component.html 代码如下:

<!-- 当MessageService 中的 messages数组 有数据的时候,才 -->
<div *ngIf="messageService.messages.length">
	<h2>Messages</h2>

	<button class="class_btn_clear"
			(click)="messageService.clear()">
			清空消息
	</button>

	<div *ngFor="let message of messageService.messages">
		{{ message }}
	</div>
</div>

src/app/messages/messages.component.css 代码如下:

/* MessagesComponent's private CSS styles */
h2 {
  color: red;
  font-family: Arial, Helvetica, sans-serif;
  font-weight: lighter;
}
body {
  margin: 2em;
}
body, input[text], button {
  color: crimson;
  font-family: Cambria, Georgia;
}
 
button.class_btn_clear {
  font-family: Arial;
  background-color: #eee;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer;
  cursor: hand;
}
button:hover {
  background-color: #cfd8dc;
}
button:disabled {
  background-color: #eee;
  color: #aaa;
  cursor: auto;
}
button.class_btn_clear {
  color: #888;
  margin-bottom: 12px;
}

src/app/app.module.ts 代码如下:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
// 表单双向绑定
import { FormsModule } from '@angular/forms';


import { AppComponent } from './app.component';
import { HeroesComponent } from './heroes/heroes.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
import { MessagesComponent } from './messages/messages.component';


@NgModule({
  declarations: [
    AppComponent,
    HeroesComponent,
    HeroDetailComponent,
    MessagesComponent
  ],
  imports: [
    BrowserModule,
    // 添加双向绑定所需要的模块
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

src/app/app.component.html 代码如下:

<!--The content below is only a placeholder and can be replaced.-->
<h1>{{title}}</h1>

<!-- 组件 -->
<app-heroes></app-heroes>

<!-- 提示消息 -->
<app-messages></app-messages>







Summary

  • You refactored data access to the HeroService class.
  • You registered the HeroService as the provider of its service at the root level so that it can be injected anywhere in the app.
  • You used Angular Dependency Injection to inject it into a component.
  • You gave the HeroService get data method an asynchronous signature.
  • You discovered Observable and the RxJS Observable library.
  • You used RxJS of() to return an observable of mock heroes (Observable<Hero[]>).
  • The component's ngOnInit lifecycle hook calls the HeroService method, not the constructor.
  • You created a MessageService for loosely-coupled (松耦合) communication between classes.
  • The HeroService injected into a component    is created with another injected service, MessageService.



Routing 重点,路由

There are new requirements for the Tour of Heroes app:

  • Add a Dashboard view.
  • Add the ability to navigate between the Heroes and Dashboard views.
  • When users click a hero name in either view, navigate to a detail view of the selected hero.
  • When users click a deep link in an email, open the detail view for a particular hero.

When you’re done, users will be able to navigate the app like this:


效果如下:



动态效果如下:




Add the AppRoutingModule

An Angular best practice is to load and configure the router in a separate, top-level module 

that is dedicated to routing and imported by the root AppModule.


By convention, the module class name is AppRoutingModule 

and it belongs in the app-routing.module.ts in the src/app folder.


Use the CLI to generate it.

content_copyng generate module app-routing --flat --module=app

--flat puts the file in src/app instead of its own folder.
--module=app tells the CLI to register it in the imports array of the AppModule.



The generated file looks like this:

src/app/app-routing.module.ts (generated)
content_copyimport { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

@NgModule({
  imports: [
    CommonModule
  ],
  declarations: []
})
export class AppRoutingModule { }

You generally don't declare components in a routing module 

so you can delete the @NgModule.declarations array and delete CommonModule references too.


You'll configure the router with Routes in the RouterModule 

so import those two symbols (即Routes 和 RouterModule) from the @angular/router library.


Add an @NgModule.exports array with RouterModule in it. 

Exporting RouterModule makes router directives available for use in the AppModule components that will need them.

AppRoutingModule looks like this now:

src/app/app-routing.module.ts (v1)
content_copyimport { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

@NgModule({
  exports: [ RouterModule ]
})
export class AppRoutingModule {}


Add routes  (在app-routing.module.ts文件)

Routes tell the router which view to display 

when a user clicks a link or pastes a URL into the browser address bar.


A typical Angular Route has two properties:

  1. path: a string that matches the URL in the browser address bar.
  2. component: the component that the router should create when navigating to this route.


You intend to navigate to the HeroesComponent 

when the URL is something like localhost:4200/heroes.


Import the HeroesComponent so you can reference it in a Route

Then define an array of routes with a single route to that component.


import { HeroesComponent }      from './heroes/heroes.component';
// 先定义一个啥? 如果报错先不管,继续往下看
const routes: Routes = [
  { path: 'heroes', component: HeroesComponent }
];

Once you've finished setting up, 

the router will match that URL to path: 'heroes' and display the HeroesComponent.



RouterModule.forRoot()

You first must initialize the router 

and start it listening for browser location changes.


Add RouterModule to the @NgModule.imports array 

and configure it with the routes in one step by calling RouterModule.forRoot() within the imports array, like this:

(在文件app-routing.module.ts)  

代码如下:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HeroesComponent } from './heroes/heroes.component'

// 必须先写在最上面
const routes: Routes = [
	{
		path: 'heroes',
		component: HeroesComponent
	}
];
//  这个必须写在后面
@NgModule({

  imports: [RouterModule.forRoot(routes)]
  exports: [RouterModule]

})



export class AppRoutingModule {}



The method is called forRoot() 

because you configure the router at the application's root level. 

The forRoot()method supplies the service providers and directives needed for routing, 

and performs the initial navigation based on the current browser URL.


Add RouterOutlet

1. Open the AppComponent template 

2. replace the <app-heroes> element with a <router-outlet> element.

src/app/app.component.html (router-outlet)
content_copy<h1>{{title}}</h1>
<router-outlet></router-outlet>
<app-messages></app-messages>


You removed <app-heroes> because you will only display the HeroesComponent when the user navigates to it.

The <router-outlet> tells the router where to display routed views. 

(这一点跟Vue-Router中<router-view></router-view>一样)


The RouterOutlet is one of the router directives 

that became available to the AppComponent 

because AppModule imports AppRoutingModule which exported RouterModule.


Try it

You should still be running with this CLI command.

content_copyng serve --open

The browser should refresh and display the app title but not the list of heroes.


Look at the browser's address bar. The URL ends in /

The route path to HeroesComponent is /heroes.

Append /heroes to the URL in the browser address bar. 

You should see the familiar heroes master/detail view.

效果如下:




Add a navigation link (routerLink) (类似Vue Router 中的 <router-link>)

Users shouldn't have to paste a route URL into the address bar. 

They should be able to click a link to navigate.


Add a <nav> element and, within that, 

an anchor element that, when clicked, triggers navigation to the HeroesComponent


The revised  AppComponent  template looks like this:

src/app/app.component.html (heroes RouterLink)
content_copy<h1>{{title}}</h1>
<nav>
  <a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>


routerLink attribute is set to "/heroes"

the string that the router matches to the route to HeroesComponent


The routerLink is the selector for the RouterLink directive that turns user clicks into router navigations.

 It's another of the public directives in the RouterModule.


The browser refreshes and displays the app title and heroes link, but not the heroes list.

1. If you click the link. 

2. The address bar updates to /heroes 

3. and the list of heroes appears.

效果如下:



Make this and future navigation links look better 

by adding private CSS styles to app.component.css 

as listed in the final code review below.

app.component.css样式如下:

h1 {
  color: #369;
  font-family: Arial, Helvetica, sans-serif;
  font-size: 250%;
  margin-top: 0;
  margin-bottom: 0;
}
h2, h3 {
  color: #444;
  font-family: Arial, Helvetica, sans-serif;
  font-weight: lighter;
}
body {
  margin: 2em;
}
body, input[text], button {
  color: #888;
  font-family: Cambria, Georgia;
}
/* everywhere else */
* {
  font-family: Arial, Helvetica, sans-serif;
}

/*=========================================*/

/* AppComponent's private CSS styles */
/*h1 {
  font-size: 1.2em;
  color: #369;
  margin-bottom: 0;
}*/
h2 {
  font-size: 2em;
  margin-top: 0;
  padding-top: 0;
}
nav a {
  padding: 5px 10px;
  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;
}


Add a dashboard view

Routing makes more sense when there are multiple views. 

So far there's only the heroes view.


Now, Add a DashboardComponent using the CLI:

ng generate component dashboard

如图所示:






The CLI generates the files for the DashboardComponent and declares it in AppModule.

1. Replace the default file content in these three files as follows 

2. and then return for a little discussion:

src/app/dashboard/dashboard.component.html中代码如下:

<h3>Top Girls</h3>
<div class="class_div_grid class_div_grid-pad">
	<a *ngFor="let hero of heroes" class="col-1-4">
		<div class="class_div_module hero">
			<h4>{{hero.name}}</h4>
		</div>
	</a>
</div>

src/app/dashboard/dashboard.component.ts中代码如下:

import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';


@Component({
  selector: 'app-dashboard',
  templateUrl: './dashboard.component.html',
  styleUrls: ['./dashboard.component.css']
})
// 重点来了
export class DashboardComponent implements OnInit {
  // 成员属性
  heroes: Hero[] = [];

  // 将接收的HeroService对象,变成私有属性
  constructor(private heroService: HeroService) { }

  // 定义一个方法, 来初始化
  getHeroes(): void {
  	this.heroService.getHeroes()
  		.subscribe(heroes => {
  			// 下标0 到 下标3 , 但不包括下标3 即元素 0 1 2; 即前3位
  			return this.heroes = heroes.slice(0,3);
  		})
  }

  // 生命周期函数中调用自定义的方法,完成初始化
  ngOnInit() {
  	this.getHeroes();
  }

}

src/app/dashboard/dashboard.component.css中代码如下:

/* DashboardComponent's private CSS styles */

[class*='col-'] {
  float: left;
  padding-right: 20px;
  padding-bottom: 20px;
}

[class*='col-']:last-of-type {
  padding-right: 0;
}

a {
  text-decoration: none;
}

*,
*:after,
*:before {
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
}

h3 {
  text-align: center;
  margin-bottom: 0;
}

h4 {
  position: relative;
}

.class_div_grid {
  margin: 0;
}

.col-1-4 {
  width: 25%;
}

.class_div_module {
  padding: 20px;
  text-align: center;
  color: #eee;
  max-height: 120px;
  min-width: 120px;
  background-color: #607D8B;
  border-radius: 2px;
}

.class_div_module:hover {
  background-color: #EEE;
  cursor: pointer;
  color: #607d8b;
}

.class_div_grid-pad {
  padding: 10px 0;
}

.class_div_grid-pad>[class*='col-']:last-of-type {
  padding-right: 20px;
}

@media (max-width: 600px) {
  .class_div_module {
    font-size: 10px;
    max-height: 75px;
  }
}

@media (max-width: 1024px) {
  .class_div_grid {
    margin: 0;
  }
  .class_div_module {
    min-width: 60px;
  }
}




The template presents a grid of hero name links.

  • The *ngFor repeater creates as many links as are in the component's heroes array.(有点拗口,但意思就是为数组中每个元素创建了链接)
  • The links are styled as colored blocks by the dashboard.component.css.
  • The links don't go anywhere yet but they will shortly(意思是link虽然现在还有指向,但下面马上就会有目标了).


The class is similar to the HeroesComponent class.

  • It defines a heroes array property.
  • The constructor expects Angular to inject the HeroService into a private heroService property.
  • The ngOnInit() lifecycle hook calls getHeroes.


This getHeroes reduces the number of heroes displayed to four (2nd, 3rd, 4th, and 5th).  

即下标1 2 3 4 5,但不包括5, 所以是4个元素

content_copygetHeroes(): void {
  this.heroService.getHeroes()
    .subscribe(heroes => this.heroes = heroes.slice(1, 5));
}


Add the dashboard route

To navigate to the dashboard, 

the router needs an appropriate route.


Import the DashboardComponent in the AppRoutingModule.

src/app/app-routing.module.ts (import DashboardComponent)
import { DashboardComponent }   from './dashboard/dashboard.component';


Add a route to the AppRoutingModule.routes array that matches a path to the DashboardComponent.

{ path: 'dashboard', component: DashboardComponent },


Add a default route

When the app starts, 

the browsers address bar points to the web site's root. 

That doesn't match any existing route

 so the router doesn't navigate anywhere. 

The space below the <router-outlet> is blank.


To make the app navigate to the dashboard automatically, 

add the following route to the AppRoutingModule.Routes array.

意思就是刚打开页面时,自动重定向到/dashboard路由下

{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },


This route redirects a URL (that fully matches the empty path)  

to the route whose path is '/dashboard'.


After the browser refreshes, the router loads the DashboardComponent 

and the browser address bar shows the /dashboard URL.


Add dashboard link to the shell

The user should be able to navigate back and forth 

between the DashboardComponent and the HeroesComponent

 by clicking links in the navigation area near the top of the page.


Add a dashboard navigation link to the AppComponent shell template, 

just above the Heroes link.

src/app/app.component.html
content_copy<h1>{{title}}</h1>
<nav>
  <a routerLink="/dashboard">Dashboard</a>
  <a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>

After the browser refreshes 

you can navigate freely between the two views by clicking the links.

效果如下:


Navigating to hero details

The HeroDetailsComponent displays details of a selected hero. 

At the moment the HeroDetailsComponent is only visible at the bottom of the HeroesComponent

The user should be able to get to these details in three ways.

  1. By clicking a hero in the dashboard.
  2. By clicking a hero in the heroes list.
  3. By pasting a "deep link" URL into the browser address bar that identifies the hero to display.


In this section, you'll enable navigation to the HeroDetailsComponent 

and liberate it from the HeroesComponent.


Delete hero details from HeroesComponent

When the user clicks a hero item in the HeroesComponent

the app should navigate to the HeroDetailComponent, replacing the heroes list view with the hero detail view. 


The heroes list view should no longer show hero details as it does now.


1. Open the HeroesComponent template (heroes/heroes.component.html

2. and delete the <app-hero-detail> element from the bottom.


Clicking a hero item now does nothing. 

You'll fix that shortly after you enable routing to the HeroDetailComponent.


Add a hero detail route

A URL like ~/detail/11 would be a good URL 

for navigating to the Hero Detail view of the hero whose id is 11.


Open AppRoutingModule and import HeroDetailComponent.

src/app/app-routing.module.ts (import HeroDetailComponent)
import { HeroDetailComponent }  from './hero-detail/hero-detail.component';


Then add a parameterized route to the AppRoutingModule.routes array 

that matches the path pattern to the hero detail view.

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

重点: The colon (:) in the path indicates that :id is a placeholder for a specific hero id.


At this point, all application routes are in place.

src/app/app-routing.module.ts (all routes)

const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'dashboard', component: DashboardComponent },
  { path: 'detail/:id', component: HeroDetailComponent },
  { path: 'heroes', component: HeroesComponent }
];


DashboardComponent hero links

The DashboardComponent hero links do nothing at the moment.


Now that (既然) the router has a route to HeroDetailComponent

fix the dashboard hero links to navigate via the parameterized dashboard route.

src/app/dashboard/dashboard.component.html (hero links)
<a *ngFor="let hero of heroes" class="col-1-4"
    routerLink="/detail/{{hero.id}}">

You're using Angular interpolation binding within the *ngFor repeater 

to insert the current iteration's hero.id into each routerLink.


HeroesComponent hero links

The hero items in the HeroesComponent are <li> elements 

whose click events are bound to the component's onSelectFunction()method.


src/app/heroes/heroes.component.html (list with onSelectFunction)

<ul class="heroes">
  <li *ngFor="let hero of heroes"
    [class.selected]="hero === selectedHero"
    (click)="onSelectFunction(hero)">
    <span class="badge">{{hero.id}}</span> {{hero.name}}
  </li>
</ul>


Strip the <li> back to just its *ngFor, wrap the badge and name in an anchor element (<a>), 

and add a routerLink attribute to the anchor 

that is the same as in the dashboard template


src/app/heroes/heroes.component.html (list with links)

<ul class="heroes">
  <li *ngFor="let hero of heroes">
    <a routerLink="/detail/{{hero.id}}">
      <span class="badge">{{hero.id}}</span> {{hero.name}}
    </a>
  </li>
</ul>

You'll have to fix the private stylesheet (heroes.component.css) to make the list look as it did before. 

Revised styles are in the final code review at the bottom of this guide.

heroes.component.css代码如下:

 





Remove dead code (optional)

While the HeroesComponent class still works, 

the onSelect() method and selectedHero property are no longer used.


It's nice to tidy up and you'll be grateful to yourself later. 

Here's the class after pruning away the dead code. (prune away 一个专八词汇)

src/app/heroes/heroes.component.ts (cleaned up)
export class HeroesComponent implements OnInit {
  heroes: Hero[];

  constructor(private heroService: HeroService) { }

  ngOnInit() {
    this.getHeroes();
  }

  getHeroes(): void {
    this.heroService.getHeroes()
    .subscribe(heroes => this.heroes = heroes);
  }
}


Routable HeroDetailComponent

Previously, the parent HeroesComponent set the HeroDetailComponent.hero property 

and the HeroDetailComponent displayed the hero.


HeroesComponent doesn't do that anymore. 

Now the router creates the HeroDetailComponent in response to a URL such as ~/detail/11.


The HeroDetailComponent needs a new way to obtain the hero-to-display.

  • 1. Get the route that created it,
  • 2. Extract the id from the route
  • 3. Acquire the hero with that id from the server via the HeroService


Add the following imports:

src/app/hero-detail/hero-detail.component.ts
import { ActivatedRoute } from '@angular/router';
import { Location } from '@angular/common';

import { HeroService }  from '../hero.service';


Inject the ActivatedRoute, HeroService, and Location services into the constructor, 

目的是转成私有成员,然后就可以为所欲为了:  saving their values in private fields:

constructor(
  private route: ActivatedRoute,
  private heroService: HeroService,
  private location: Location
) {}

The ActivatedRoute holds information about the route to this instance of the HeroDetailComponent

This component is interested in the route's bag of parameters extracted from the URL. 


The "id" parameter is the id of the hero to display.


The HeroService gets hero data from the remote server 

and this component will use it to get the hero-to-display.


The location is an Angular service for interacting with the browser. 

You'll use it later to navigate back (返回) to the view that navigated here.


Extract the id route parameter

1. In the ngOnInit() lifecycle hook call getHero() 

2. and define it as follows.  (+"11"  前置加号表示将字符串转成数字)

ngOnInit(): void {
  this.getHero();
}

getHero(): void {
  const id = +this.route.snapshot.paramMap.get('id');
  this.heroService.getHero(id)
    .subscribe(hero => this.hero = hero);
}

The route.snapshot is a static image of the route information 

shortly after the component was created.


The paramMap is a dictionary of route parameter values extracted from the URL. 

The "id" key returns the id of the hero to fetch.


Route parameters are always strings. 

The JavaScript (+) operator converts the string to a number, which is what a hero id should be.


The browser refreshes and the app crashes with a compiler error. HeroService doesn't have a getHero() method. 

Let's add it now.


Add HeroService.getHero()

Open HeroService and add this getHero() method

src/app/hero.service.ts (getHero)
getHero(id: number): Observable<Hero> {
  // TODO: send the message _after_ fetching the hero
  this.messageService.add(`HeroService: fetched hero id=${id}`);
  return of(HEROES.find(hero => hero.id === id));
}

Note the 反引号 backticks ( ` ) that define a JavaScript template literal for embedding the id.

ES6新语法: 强大的模板字符串


Like getHeroes()getHero() has an asynchronous signature

It returns a mock hero as an Observable, using the RxJS of() function.


You'll be able to re-implement getHero() as a real http request 

without having to change the HeroDetailComponent that calls it.


Try it

The browser refreshes and the app is working again. 

You can click a hero in the dashboard or in the heroes list and navigate to that hero's detail view.


If you paste localhost:4200/detail/1 in the browser address bar, 

the router navigates to the detail view for the hero with id: 1, "面码".

效果如下:


Find the way back

By clicking the browser's back button, 

you can go back to the hero list or dashboard view, 

depending upon which sent you to the detail view.


It would be nice to have a button on the HeroDetail view that can do that.


Add a go back button to the bottom of the hero-detail component template 

and bind it to the component's goBack() method.


src/app/hero-detail/hero-detail.component.html (back button)

<button (click)="goBack()">go back</button>


Add a goBack() method to the hero-detail component class 

that navigates backward one step in the browser's history stack 

using the Location service that you injected previously.


src/app/hero-detail/hero-detail.component.ts (goBack)

goBack(): void {
  this.location.back();
}



Refresh the browser and start clicking. 

Users can navigate around the app, 

1. from the dashboard to hero details and back, 

2. 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.

效果如下:

Final code review

Here are the code files discussed on this page and your app should look like this live example / download example.

代码太了, 放到了github上面

具体地址如下: asdf 待补充


Summary

  • You added the Angular router to navigate among different components.
  • You turned the AppComponent into a navigation shell with<a> links and a <router-outlet>.
  • You configured the router in an AppRoutingModule
  • You defined simple routes, a redirect route, and a parameterized route.
  • You used the routerLink directive in anchor elements.
  • You refactored a tightly-coupled master/detail view into a routed detail view.
  • You used router link parameters to navigate to the detail view of a user-selected hero.
  • You shared the HeroService among multiple components.



















未完待续,下一章节,つづく

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值