tc9011

Angular官方文档学习笔记之hero教程

18 min
2017011547655o_7 1.png
2017011547655o_7 1.png

ES6工具链

20161112402632016-11-12_13-40-24.png
20161112402632016-11-12_13-40-24.png
20161112965072016-11-12_13-56-31.png
20161112965072016-11-12_13-56-31.png
  • systemjs - 通用模块加载器,支持AMD、CommonJS、ES6等各种格式的JS模块加载
  • es6-module-loader - ES6模块加载器,systemjs会自动加载这个模块
  • traceur - ES6转码器,将ES6代码转换为当前浏览器支持的ES5代码。systemjs会自动加载 这个模块。

Hello World

20161113120092016-11-13_20-49-51.png
20161113120092016-11-13_20-49-51.png

根据官方文档的快速起步,按照自己的理解画了上面的流程图(有不对的地方还请指正)。

  • 包括Angular在内的Angular应用通过NgModules分成不同的代码块,需要哪个块,就import,这样可以减少文件体积。
  • 每一个Angular应用都至少有一个模块和组件,组件通过与它相关的模板来控制屏幕上的一小块(视图View),main.ts负责初始化应用平台,进行应用引导。
  • 为了方便测试组件,应用的引导应与组件或者模块分开,这样实现了MVM(model-view-whatever)

Hero教程

官网的Hero教程是建立在quickstart上的,所以可以把quickstart直接拿过来用。

英雄编辑器

class中可以定义一些变量、方法甚至是构造函数(用于初始化),export出来的class可以被外部使用。

在Angular中{% raw %}{{}}{% endraw %}用来取值。

一个ts文件中可以定义多个export class,但是官方不提倡。

id:numberhero:Hero = {}都是指定了变量的类型。

template中双引号改成反引号,可以写成多行形式。

表单输入双向绑定需要导入FormsModule模块,@NgModule中的import数组是应用中用到的外部模块列表。

用到Angular自带的模块,在import数组中声明;用到自定义的组件在declarations数组中声明。

主从结构

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' }
];

这段代码中的const是ES6中定义变量的关键字,在ES6中, const 代表一个值的常量索引,变量名字在内存中的指针不能够改变,但是指向这个变量的值 可能 改变。例如:

const names = [ ] ;
names . push ( “Jordan” ) ;
console . log ( names ) ;


const names = [ ] ;
names = [ ] ; // Error!

*ngFor="let hero of heroes"ngFor*前缀表示“及其子元素组成了一个主控模板。

ngFor指令在AppComponent.heroes属性返回的heroes数组上迭代,并输出此模板的实例。

引号中赋值给ngFor的那段文本表示“从heroes数组中取出每个英雄,存入一个局部的hero变量,并让它在相应的模板实例中可用”。

为一个组件指定样式时,它们的作用域将仅限于该组件。

ngIf指令为false则从 DOM 中移除整段 HTML。

[class.selected]="hero === selectedHero" 这个写法很有意思,.可以理解为css类选择器,在class上为selected类添加一个属性绑定(绑定了style中的.selected这个样式),当后面的表达式为true时,绑定这个样式,false时不绑定。而[]实现了从数据源(hero === selectedHero表达式)到class属性的单向数据流动。

多个组件

可以将需要多次引用的类单独写入一个ts文件中。

<my-hero-detail [hero]="selectedHero"></my-hero-detail>通过标签中的[hero]="selectedHero"可以在不同组件之间传递数据。在需要引用hero的地方,用@Input() hero: Hero;来引入hero

服务

当 TypeScript 看到@Injectable()装饰器时,就会记下本服务的元数据。 如果 Angular 需要往这个服务中注入其它依赖,就会使用这些元数据。

HeroServicemock-heroes.ts获取数据,并提供给其他组件使用。其他组件使用HeroService时,用构造函数定义一个私有属性,作为注入HeroService的靶点。

providers数组告诉 Angular,当它创建新的AppComponent组件时,也要创建一个HeroService的新实例。

可以把HeroService看做中转站,而providers数组告诉 Angular,这是中转站。中转站中记录着哪里的有元数据,并随时准备注入所需要的组件中。

OnInit 接口会在组件刚创建时、每次变化时,以及最终被销毁时被Angular调用。OnInit 接口中会有一个带有初始化逻辑的ngOnInit方法,可以用来初始化。

承诺,在有了结果时,它承诺会回调我们。 我们请求一个异步服务去做点什么,并且给它一个回调函数。 它会去做(在某个地方),一旦完成,它就会调用我们的回调函数,并通过参数把工作结果或者错误信息传给我们。

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

这个方法基于承诺的,并在承诺的事情被解决时再行动。 一旦承诺的事情被成功解决,就会显示英雄数据。then方法把回调函数作为参数传给承诺对象。

路由

路由告诉路由器,当用户点击链接或者把 URL 粘贴到浏览器地址栏时,应该显示哪个视图。

路由定义包括以下部分:

  • path: 路由器会用它来匹配浏览器地址栏中的地址,如heroes
  • component: 导航到此路由时,路由器需要创建的组件(HeroesComponent)。

路由有两种实现方式:

第一种是通过RouterLink指令,绑定到heroes的路由路径,在app.module.ts中指定了'/heroes'就是指向HeroesComponent的那个路由的路径,并且需要告诉路由把激活的组件显示在<router-outlet>里面。

template: `
   <h1>{{title}}</h1>
   <a routerLink="/heroes">Heroes</a>
   <router-outlet></router-outlet>
 `
@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    RouterModule.forRoot([
      {
        path: 'heroes',
        component: HeroesComponent
      }
    ])
  ],
  declarations: [
    AppComponent,
    HeroDetailComponent,
    HeroesComponent
  ],
  providers: [
    HeroService
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {
}

第二种是通过函数来实现,触发click事件后,调用gotoDetail函数,该函数将路由导航到detail的组件上。

gotoDetail(): void {
  this.router.navigate(['/detail', this.selectedHero.id]);
}
<button (click)="gotoDetail()">View Details</button>

浏览器启动时,在地址栏中使用的路径是/。如果要在应用启动的时候就显示仪表盘,而且希望在浏览器的地址栏看到一个好看的 URL,比如/dashboard,这就需要用到重定向:

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

设置moduleId属性到module.id后,才能使用templateUrl。

路径中的冒号 (:) 表示:id是一个占位符:

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

通过[routerLink]绑定了一个包含链接参数数组的表达式。 该数组有两个元素,目标路由和一个用来设置当前英雄的 id 值的路由参数。这两个元素与上面定义中的 path 和 

 对应。

<a *ngFor="let hero of heroes"  [routerLink]="['/detail', hero.id]"  class="col-1-4">

典型路由模块值得注意的有:

  • 将路由抽出到一个变量中。你将来可能会导出它,而且它让路由模块模式更加明确。
  • 添加RouterModule.forRoot(routes)imports
  • 添加RouterModuleexports,这样关联模块的组件可以访问路由的声明,比如RouterLinkRouterOutlet
  • declarations!声明是关联模块的任务。
  • 如果你有守卫服务,添加模块providers

Angular路由器提供了routerLinkActive指令,我们可以用它来为匹配了活动路由的 HTML 导航元素自动添加一个 CSS 类。

template: `
  <h1>{{title}}</h1>
  <nav>
    <a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
    <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
  </nav>
  <router-outlet></router-outlet>
`,
nav a.active {
  color: #039be5;
}

好不容易走到路由这步,我按照我的理解画了一个脑图:

2016122138842HeroApp.png
2016122138842HeroApp.png

HTTP

准备HTTP服务

@angular/http库中的HttpModule保存着这些 HTTP 相关服务提供商的全集。

模拟web API

// Imports for loading & configuring the in-memory web api
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService }  from './in-memory-data.service';

InMemoryWebApiModuleHttp客户端默认的后端服务替换成了内存 Web API服务。

InMemoryWebApiModule.forRoot(InMemoryDataService),

forRoot配置方法需要InMemoryDataService类实例,用来向内存数据库填充数据。

英雄与HTTP

  private heroesUrl = 'app/heroes';  // URL to web api

  constructor(private http: Http) { }

  getHeroes(): Promise<Hero[]> {
    return this.http.get(this.heroesUrl)
               .toPromise()
               .then(response => response.json().data as Hero[])
               .catch(this.handleError);
  }
HTTP承诺

Angular 的http.get返回一个RxJSObservable对象。 *Observable(可观察对象)*是一个管理异步数据流的强力方式。 

.toPromise()

利用toPromise操作符把Observable直接转换成Promise对象。Angular 的Observable并没有一个toPromise操作符,需要从 RxJS 库中导入它们。

import 'rxjs/add/operator/toPromise';
then 回调中提取出数据
.then(response => response.json().data as Hero[])

在 promise 的then回调中,调用 HTTP 的Reponse对象的json方法,以提取出其中的数据。

错误处理
.catch(this.handleError);
private handleError(error: any): Promise<any> {
  console.error('An error occurred', error); // for demo purposes only
  return Promise.reject(error.message || error);
}

catch了服务器的失败信息,并把它们传给了错误处理器

更新英雄详情

hero服务的update方法
private headers = new Headers({'Content-Type': 'application/json'});

update(hero: Hero): Promise<Hero> {
  const url = `${this.heroesUrl}/${hero.id}`;
  return this.http
    .put(url, JSON.stringify(hero), {headers: this.headers})
    .toPromise()
    .then(() => hero)
    .catch(this.handleError);
}

通过一个编码在 URL 中的英雄 id 来告诉服务器应该更新哪个英雄。put 的 body 是该英雄的 JSON 字符串,它是通过调用JSON.stringify得到的。 并且在请求头中标记出的 body 的内容类型(application/json)。

添加英雄

<div>
  <label>Hero name:</label> <input #heroName />
  <button (click)="add(heroName.value); heroName.value=''">
    Add
  </button>
</div>

  add(name: string): void {
  name = name.trim();
  if (!name) { return; }
  this.heroService.create(name)
    .then(hero => {
      this.heroes.push(hero);
      this.selectedHero = null;
    });
}

当指定的名字不为空的时候,点击处理器就会委托 hero 服务来创建一个具有此名字的英雄, 并把这个新的英雄添加到数组中。

删除一个英雄

  <li *ngFor="let hero of heroes" (click)="onSelect(hero)"
      [class.selected]="hero === selectedHero">
    <span class="badge">{{hero.id}}</span>
    <span>{{hero.name}}</span>
    <button class="delete"
      (click)="delete(hero); $event.stopPropagation()">x</button>
  </li>

delete按钮的点击处理器应该阻止点击事件向上冒泡 ,否则它会选中我们要删除的这位英雄。

可观察对象 (Observable)

一个可观察对象是一个事件流,我们可以用数组型操作符来处理它。

Observable转换成了Promise通常是更好地选择,我们通常会要求http.get获取单块数据。只要接收到数据,就算完成。 使用承诺这种形式的结果是让调用方更容易写,并且承诺已经在 JavaScript 程序员中被广泛接受了。

按名搜索
import { Injectable }     from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs';
import { Hero }           from './hero';
@Injectable()
export class HeroSearchService {
  constructor(private http: Http) {}
  search(term: string): Observable<Hero[]> {
    return this.http
               .get(`app/heroes/?name=${term}`)
               .map((r: Response) => r.json().data as Hero[]);
  }
}

不再调用toPromise,而是直接返回可观察对象

HeroSearchComponent
<div id="search-component">
  <h4>Hero Search</h4>
  <input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
  <div>
    <div *ngFor="let hero of heroes | async"
         (click)="gotoDetail(hero)" class="search-result" >
      {{hero.name}}
    </div>
  </div>
</div>

heroes属性现在是英雄列表的Observable对象,而不再只是英雄数组。 *ngFor不能用可观察对象做任何事,除非我们在它后面跟一个async pipe (AsyncPipe)。 这个async管道会订阅到这个可观察对象,并且为*ngFor生成一个英雄数组。

import { Component, OnInit } from '@angular/core';
import { Router }            from '@angular/router';
import { Observable }        from 'rxjs/Observable';
import { Subject }           from 'rxjs/Subject';
import { HeroSearchService } from './hero-search.service';
import { Hero } from './hero';
@Component({
  moduleId: module.id,
  selector: 'hero-search',
  templateUrl: 'hero-search.component.html',
  styleUrls: [ 'hero-search.component.css' ],
  providers: [HeroSearchService]
})
export class HeroSearchComponent implements OnInit {
  heroes: Observable<Hero[]>;
  private searchTerms = new Subject<string>();
  constructor(
    private heroSearchService: HeroSearchService,
    private router: Router) {}
  // Push a search term into the observable stream.
  search(term: string): void {
    this.searchTerms.next(term);
  }
  ngOnInit(): void {
    this.heroes = this.searchTerms
      .debounceTime(300)        // wait for 300ms pause in events
      .distinctUntilChanged()   // ignore if next search term is same as previous
      .switchMap(term => term   // switch to new observable each time
        // return the http search observable
        ? this.heroSearchService.search(term)
        // or the observable of empty heroes if no search term
        : Observable.of<Hero[]>([]))
      .catch(error => {
        // TODO: real error handling
        console.log(error);
        return Observable.of<Hero[]>([]);
      });
  }
  gotoDetail(hero: Hero): void {
    let link = ['/detail', hero.id];
    this.router.navigate(link);
  }
}
搜索词
private searchTerms = new Subject<string>();

// Push a search term into the observable stream.
search(term: string): void {
  this.searchTerms.next(term);
}

Subject(主题)是一个可观察的事件流中的生产者。 searchTerms生成一个产生字符串的Observable,用作按名称搜索时的过滤条件。

每当调用search时都会调用next来把新的字符串放进该主题的可观察流中。

初始化 HEROES 属性(NGONINIT)
heroes: Observable<Hero[]>;

ngOnInit(): void {
  this.heroes = this.searchTerms
    .debounceTime(300)        // wait for 300ms pause in events
    .distinctUntilChanged()   // ignore if next search term is same as previous
    .switchMap(term => term   // switch to new observable each time
      // return the http search observable
      ? this.heroSearchService.search(term)
      // or the observable of empty heroes if no search term
      : Observable.of<Hero[]>([]))
    .catch(error => {
      // TODO: real error handling
      console.log(error);
      return Observable.of<Hero[]>([]);
    });
}

Subject也是一个Observable对象。 要把搜索词的流转换成Hero数组的流,并把结果赋值给heroes属性。

如果我们直接把每一次用户按键都直接传给HeroSearchService,就会发起一场 HTTP 请求风暴。我们不希望占用服务器资源,也不想耗光蜂窝移动网络的流量。

幸运的是,我们可以在字符串的Observable后面串联一些Observable操作符,来归并这些请求。 我们将对HeroSearchService发起更少的调用,并且仍然获得足够及时的响应。做法如下:

  • 在传出最终字符串之前,debounceTime(300)将会等待,直到新增字符串的事件暂停了 300 毫秒。 我们实际发起请求的间隔永远不会小于 300ms。
  • distinctUntilChanged确保只在过滤条件变化时才发送请求, 这样就不会重复请求同一个搜索词了。
  • switchMap会为每个从debouncedistinctUntilChanged中通过的搜索词调用搜索服务。 它会取消并丢弃以前的搜索可观察对象,只保留最近的。

[switchMap操作符](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/flatmaplatest.md) (以前叫"flatMapLatest")是非常智能的。

每次符合条件的按键事件都会触发一次对http方法的调用。即使在发送每个请求前都有 300 毫秒的延迟, 我们仍然可能同时拥有多个在途的 HTTP 请求,并且它们返回的顺序未必就是发送时的顺序。

switchMap保留了原始的请求顺序,并且只返回最近一次 http 调用返回的可观察对象。 这是因为以前的调用都被取消或丢弃了。

如果搜索框为空,我们还可以短路掉这次http方法调用,并且直接返回一个包含空数组的可观察对象。

注意,取消HeroSearchService的可观察对象并不会实际中止 (abort) 一个未完成的 HTTP 请求, 除非服务支持这个特性,这个问题我们以后再讨论。 目前我们的做法只是丢弃不希望的结果。

导入RxJS操作符
// Observable class extensions
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/throw';

// Observable operators
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';

把整个应用中要用的那些 RxJS Observable扩展组合在一起,放在一个单独的 RxJS 导入文件中。我们在顶级的AppModule中导入rxjs-extensions就可以一次性加载它们。

import './rxjs-extensions';
  • 本文作者: tc9011
  • 本文链接: https://tc9011.com/posts/2017/angular官方文档学习笔记之hero教程/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!