翻译自https://angular.io/tutorial,看每一节的完整代码似乎清晰一些
!!!注意
快写完了发现翻的是v2版本,现在都v14了还得重修一遍,导致这篇文章1-4主要是v2版本教程,只更新了一些函数,一开始把所有文件混在一起,后面逐步分开,很不合理,但这个代码复制到博客园比较清晰,再加上最终代码是v14的最新版,所以就没怎么改(才不是懒呢),呈现结果是个缝合怪,不影响学习,但不要跟着做,(如果真的有人看的话)
老是打hero好羞耻啊脑子里有一万个阿尔弗雷德在说话
0. 介绍
这个app将覆盖Angular核心技术。
功能:Dashboard展示top heroes信息,Heroes展示目前已有的hero,点击列表,显示详细信息,同时能编辑。
1. 制作资料卡,并添加输入
import { Component } from '@angular/core'; export class Hero { id: number; name: string; } //定义一个hero的类 @Component({ selector: 'my-app', template: ` <h1>{{title}}</h1> <h2>{{hero.name}} details!</h2> <div><label>id: </label>{{hero.id}}</div> <div> <label>name: </label> <input [(ngModel)]="hero.name" placeholder="name"> //注1 </div> ` }) //页面元素 export class AppComponent { title = 'Tour of Heroes'; hero: Hero = { id: 1, name: 'Windstorm' }; } //紧跟着的class为上面的Component服务,其中用到了hero类
注1:[(ngModel)]是hero.name和Input之间的双向绑定,使用时需要import { FormsModule } from '@angular/forms';
2. 展示多位heroes
import { Component } from '@angular/core'; export class Hero { id: number; name: string; } const HEROES: Hero[] = [ { id: 11, name: 'Mr. Nice' }, { id: 12, name: 'Narco' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, { id: 15, name: 'Magneta' }, { id: 16, name: 'RubberMan' }, { id: 17, name: 'Dynama' }, { id: 18, name: 'Dr IQ' }, { id: 19, name: 'Magma' }, { id: 20, name: 'Tornado' } ]; //用一个数组列举heroes @Component({ selector: 'my-app', template: ` <h1>{{title}}</h1> <h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes" //注2 [class.selected]="hero === selectedHero" //注4.2 (click)="onSelect(hero)"> //注3.1 <span class="badge">{{hero.id}}</span> {{hero.name}} </li> </ul> <div *ngIf="selectedHero"> //注4.1 <h2>{{selectedHero.name}} details!</h2> <div><label>id: </label>{{selectedHero.id}}</div> <div> <label>name: </label> <input [(ngModel)]="selectedHero.name" placeholder="name"/> </div> </div> `, //具体样式忽略 styles: [` .selected { background-color: #CFD8DC !important; color: white; } .heroes { ... } .heroes li { ... } .heroes li.selected:hover { ... } .heroes li:hover { ... } .heroes .text { ... } .heroes .badge { ... } `] }) export class AppComponent { title = 'Tour of Heroes'; heroes = HEROES; //使用上面的数组 selectedHero?: Hero; //注3.2 onSelect(hero: Hero): void { this.selectedHero = hero; } }
注2:*ngfor
ngfor代表着<li>和它的子元素构成了一个主模板,来展示hero的信息。let {item} of {vairable} ,hero是heroes的元素。
注3:选择一个hero
1. 增加一个点击事件,click后是对应的函数。
2.增加一个Hero变量,为selectedHero,当点击它的时候,为selectedHero赋值。? 表示变量不一定存在。
注4:*ngIf
1. 当没有选择hero的时候,当然不显示对应名称。当selectedHero不为空时,显示这块内容。
2. 将 [class.selected] 绑定给<li>上去,当hero===selectedHero时,它是selected的。
3. 代码复用
把上面的代码按功能分隔开来,以及多文件组合。文件名有一个style guide,但好像用不用都行,就不写了。
将上面一大坨代码拆成了四个文件,分别为hero-detail.component.ts、app.component.ts、hero.ts、app.module.ts
1. hero.ts
先把大家都要用的类拎(抽象)出来。
export class Hero {
id: number;
name: string;
}
2. app.component.ts
再拎出主体
import { Component } from '@angular/core'; //要用Component,先引用 import { Hero } from './hero'; //还有hero class const HEROES: Hero[] = [ { id: 11, name: 'Mr. Nice' }, { id: 12, name: 'Narco' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, { id: 15, name: 'Magneta' }, { id: 16, name: 'RubberMan' }, { id: 17, name: 'Dynama' }, { id: 18, name: 'Dr IQ' }, { id: 19, name: 'Magma' }, { id: 20, name: 'Tornado' } ]; @Component({ selector: 'my-app', template: ` <h1>{{title}}</h1> <h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes" [class.selected]="hero === selectedHero" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li> </ul> <hero-detail [hero]="selectedHero"></hero-detail> //由于把hero-detail当作子节点单独抽出来了,所以要给它传个参,把selectedHero传过去 `, styles: [` .selected { background-color: #CFD8DC !important; color: white; } .heroes { margin: 0 0 2em 0; list-style-type: none; padding: 0; width: 15em; } .heroes li { cursor: pointer; position: relative; left: 0; background-color: #EEE; margin: .5em; padding: .3em 0; height: 1.6em; border-radius: 4px; } .heroes li.selected:hover { background-color: #BBD8DC !important; color: white; } .heroes li:hover { color: #607D8B; background-color: #DDD; left: .1em; } .heroes .text { position: relative; top: -3px; } .heroes .badge { display: inline-block; font-size: small; color: white; padding: 0.8em 0.7em 0 0.7em; background-color: #607D8B; line-height: 1em; position: relative; left: -1px; top: -4px; height: 1.8em; margin-right: .8em; border-radius: 4px 0 0 4px; } `] }) export class AppComponent { title = 'Tour of Heroes'; heroes = HEROES; selectedHero: Hero; onSelect(hero: Hero): void { this.selectedHero = hero; } }
3. hero-detail.component.ts
上面提到的子节点
import { Component, Input } from '@angular/core'; //要用Component,先引用 import { Hero } from './hero'; //还有hero class @Component({ selector: 'hero-detail', //CSS选择器名字 template: ` <div *ngIf="hero"> <h2>{{hero.name}} details!</h2> <div><label>id: </label>{{hero.id}}</div> <div> <label>name: </label> <input [(ngModel)]="hero.name" placeholder="name"/> </div> </div> ` //这一块的html }) export class HeroDetailComponent { @Input() hero: Hero; //作为一个子节点,需要从父节点AppComponent那里继承一个Hero }
4. app.module.ts
类似于声明用的文件
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; //要用的 import { AppComponent } from './app.component'; import { HeroDetailComponent } from './hero-detail.component'; //涉及到的自己写的文件 @NgModule({ imports: [ BrowserModule, FormsModule ], //引用的Module declarations: [ AppComponent, HeroDetailComponent ], //包含了哪些Components,也可以是pipes或directives bootstrap: [ AppComponent ] }) export class AppModule { } //空着就行
4. Services
现在呢我们要在另一个地方展示编辑的heroes,但那里没有heroes的数据,所以要写一些services把heroes传过去。
先把那个数组拎出来:
mock-heroes.ts
import { Hero } from './hero'; export const HEROES: Hero[] = [ {id: 11, name: 'Mr. Nice'}, {id: 12, name: 'Narco'}, {id: 13, name: 'Bombasto'}, {id: 14, name: 'Celeritas'}, {id: 15, name: 'Magneta'}, {id: 16, name: 'RubberMan'}, {id: 17, name: 'Dynama'}, {id: 18, name: 'Dr IQ'}, {id: 19, name: 'Magma'}, {id: 20, name: 'Tornado'} ];
起一个新的文件,叫hero.service.ts。上面的component文件前面也是加 . 的,但其实改成下划线啥的也没问题,对编译没有影响。
hero.service.ts
import { Injectable } from '@angular/core'; //要把服务注入(inject)到组件中 import { Hero } from './hero'; import { HEROES } from './mock-heroes'; @Injectable() //有三种方法将服务注入到组件中,不要new HeroService //1. @injectable({providedIn: 'root'}) //意为将服务注入根中,这样全局都可以用,只要import一下,再在构造函数里写一句constructor(private heroService:HeroService)就好 //2. @injectable() //空着,什么都不写,但需要在NgMoudle加providers: [HeroService] //3. @injectable() //然后在@Component中加providers: [HeroService] export class HeroService { getHeroes(): Promise<Hero[]> { return Promise.resolve(HEROES); } //将调用服务改为异步,防止页面卡死,这时就要用到Promises。这个函数return一个promise,可以把它理解为一个容器,里面装着HEROES。用Promise.resolve()来代替new Promise() }
然后来看接收这个Promise的文件:
app.components.ts
import { Component, OnInit } from '@angular/core'; import { Hero } from './hero'; import { HeroService } from './hero.service'; @Component({ selector: 'my-app', template: ` <h1>{{title}}</h1> <h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes" [class.selected]="hero === selectedHero" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li> </ul> <hero-detail [hero]="selectedHero"></hero-detail> `, styles: [ ... ], providers: [HeroService] }) export class AppComponent implements OnInit { title = 'Tour of Heroes'; heroes: Hero[]; //注意Hero[]是HEROES的类型 selectedHero: Hero; constructor(private heroService: HeroService) { } getHeroes(): void { this.heroService.getHeroes().subscribe(heroes => this.heroes = heroes); //由于接受到的是一个Promise,所以改写成这样 //subscribe()里面是Promise的回调函数,意为调用成功后,执行这个函数 //执行的内容为赋值 } ngOnInit(): void { this.getHeroes(); } //不要在构造函数里调用方法,换到这里 onSelect(hero: Hero): void { this.selectedHero = hero; } }
5. 切换页面(Router)
现在有两个功能,编辑和展示。页面设计了一个类似按钮的东西,点一下就能切换过去,接下来要实现它。
实现主要是用路由(router),即URL后面加/,表示这个页面的不同部分,用路由导航过去。
介于两个功能现在在一个页面里,还要拆分下代码。
- 把app.component.ts改名为heroes.component.ts
- 把AppComponent改名为HeroesComponent
- 把my-app改名为my-heroes
1. Router
现在来加router,也就是页面跳转要用的。
首先先确定base页面,也就是源目录。确定index.html中有<base href="/">,也就是当前页面。
定义一个route给heroes component:
import { RouterModule } from '@angular/router'; RouterModule.forRoot([ { path: 'heroes', //给URL里加的 component: HeroesComponent //点了它应该创建的东西 } ])
然后给它新开一个文件,叫app-routing.module.ts
2. 加一个指示板(dashboard)
开始写对应的页面,建一个新的dashboard.component.ts
dashborad.component ---------------html---------------- <h2>Top Heroes</h2> <div class="heroes-menu"> <a *ngFor="let hero of heroes" routerLink="/detail/{{hero.id}}"> //点一下就会去对应页面 {{hero.name}} </a> </div> ------------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[] = []; constructor(private heroService: HeroService) { } ngOnInit(): void { this.getHeroes(); } getHeroes(): void { this.heroService.getHeroes() .subscribe(heroes => this.heroes = heroes.slice(1, 5)); //要第二个到第四个就够了 } }
//给它加个路由 .module.ts { path: 'dashboard', component: DashboardComponent },
现在打开页面,就会在/下,这没有内容,重定向到dashboard:
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
3. html和ts分离
@Component({ selector: 'my-dashboard', templateUrl: './dashboard.component.html', //用这个代替大段html })
4. 相应改动
把和router有关的都放到app-routing.module.ts
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { DashboardComponent } from './dashboard/dashboard.component'; import { HeroesComponent } from './heroes/heroes.component'; import { HeroDetailComponent } from './hero-detail/hero-detail.component'; const routes: Routes = [ { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, { path: 'dashboard', component: DashboardComponent }, { path: 'detail/:id', component: HeroDetailComponent }, { path: 'heroes', component: HeroesComponent } ]; @NgModule({ imports: [ RouterModule.forRoot(routes) ], exports: [ RouterModule ] }) export class AppRoutingModule {}
其中,herodetail自己占了一个页面,把herodetail也绑定一下:
{ path: 'detail/:id', //加个: ,表示后面跟hero的id component: HeroDetailComponent },
但这样一来我们就没法给它直接传参了,得再写个函数。直接看最终代码:
先看hero那边
hero.component ----------------html----------------------- <h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes" [class.selected]="hero === selectedHero" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li> </ul> <div *ngIf="selectedHero"> <h2> {{selectedHero.name | uppercase}} is my hero //注1 </h2> <button (click)="gotoDetail()">View Details</button> </div> -------------------ts--------------------------------- import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Hero } from './hero'; import { HeroService } from './hero.service'; @Component({ selector: 'my-heroes', templateUrl: './heroes.component.html', styleUrls: [ './heroes.component.css' ] }) export class HeroesComponent implements OnInit { heroes: Hero[]; selectedHero: Hero; constructor( private router: Router, //多用一个服务 private heroService: HeroService) { } getHeroes(): void { this.heroService.getHeroes().subscribe(heroes => this.heroes = heroes); } ngOnInit(): void { this.getHeroes(); } onSelect(hero: Hero): void { this.selectedHero = hero; } gotoDetail(): void { this.router.navigate(['/detail', this.selectedHero.id]); //导航到对应页面,传参数为hero的id } }
注1: | 是管道(pipe)的意思,意为将name转换为大写形式
hero-detail.component --------------html-------------- <div *ngIf="hero"> <h2>{{hero.name}} details!</h2> <div> <label>id: </label>{{hero.id}}</div> <div> <label>name: </label> <input [(ngModel)]="hero.name" placeholder="name" /> </div> <button (click)="goBack()">Back</button> //加一个返回按钮 </div> -------------ts----------------- import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Location } from '@angular/common'; import { Hero } from '../hero'; import { HeroService } from '../hero.service'; @Component({ selector: 'app-hero-detail', templateUrl: './hero-detail.component.html', styleUrls: [ './hero-detail.component.css' ] }) export class HeroDetailComponent implements OnInit { hero: Hero | undefined; constructor( private route: ActivatedRoute, private heroService: HeroService, private location: Location ) {} ngOnInit(): void { this.getHero(); } getHero(): void { const id = Number(this.route.snapshot.paramMap.get('id')); //route.snapshot是对应component创建后route的信息 //paraMap是从URL中获得的参数信息,也就是之前传的id,可以说是通过URL传参 //Number是把括号里的String强行转换为数字 this.heroService.getHero(id) .subscribe(hero => this.hero = hero); //通过id找hero,待会要说这个service } goBack(): void { this.location.back(); //利用location服务,往前退一格 } }
services里加用id找hero到函数:
getHero(id: number): Observable<Hero> { // For now, assume that a hero with the specified `id` always exists. // Error handling will be added in the next step of the tutorial. const hero = HEROES.find(h => h.id === id)!; //this.messageService.add(`HeroService: fetched hero id=${id}`); return of(hero); //用of()创建一个Observable }
5. MessageService
这是v14新写的一个东西,目的在于在页面底部显示操作,以及加上http后打印日志。
http部分就是把heroes名单包装一下,加上增删改查操作,最近用不到,也没有什么新的东西,就不再写下去了。
下班!