Angular2 管道(pipes)

每一个应用程序都是一些非常简单的任务开始:获取数据、转换数据,并把它们显示给用户。

获取数据可以是像创建一个变量那样简单,也可以像从Websocket获取数据流那么复杂。

一旦数据达到,我们可以调用 toString 直接在屏幕上显示原始的数据。但这样可能用户体验很差,几乎所有人都需要一个简单的生日日期(1988-4-15)而非原始字符串格式(Fri Apr 15 1988 00:00:00 GMT-0700 (Pacific Daylight Time))。

我们发现,应用程序中重复着相同的转换非常多,事实上,我们想将这些转换直接在HTML模板里应用。这一过程叫做管道(pipe)。

一、使用管道

管道是将数据输入并将其转换后输出,比如把生日日期转换成更友好的日期:

import { Component } from '@angular/core'

@Component({
  selector: 'hero-birthday',
  template: `<p>The hero's birthday is {{ birthday | date }}</p>`
})
export class HeroBirthday {
  birthday = new Date(1988,3,15); // April 15, 1988
}

重点看组件模板:

<p>The hero's birthday is {{ birthday | date }}</p>

插值表达式绑定组件 birthday 属性,然后通过管道操作符(|),而操作符的右边是一个日期管道(Date pipe)函数。这便是管道的调用方式。

二、内置管道

Angular自带一堆管道,比如:DatePipeUpperCasePipeLowerCasePipeCurrencyPipePercentPipe,他们都可以在任何模板中使用;另外还可以通过API Reference 查看所有内置的管道。

三、管道参数化

一个管理可以接受任意数量的可选参数来微调输出,参数之间用冒号(:)隔开,比如: currency:'EUR'slice:1:5

现在给生日模板增加一个日期格式参数。

<p>The hero's birthday is {{ birthday | date:"yyyy-MM-dd" }} </p>

参数值可以是任何有效的模板表达式,如:字符串文本、组件属性。换句话说,我们可通过一个绑定的方式来控制我们的格式,比如通过组件类属性值来控制我们的生日日期格式。

template: `
  <p>The hero's birthday is {{ birthday | date:format }}</p>
  <button (click)="toggleFormat()">Toggle Format</button>
`

按钮点击事件是调用 toggleFormat 方法,方法是用来切换短日期和长日期。

export class HeroBirthday2 {
  birthday = new Date(1988,3,15); // April 15, 1988
  toggle = true; // start with true == shortDate

  get format()   { return this.toggle ? 'shortDate' : 'fullDate'}
  toggleFormat() { this.toggle = !this.toggle; }
}

结果如下:

date-format-toggle-anim

四、链式管道

我们可以通过若干有用的管道依次转移输出,这便叫链式管道。下面示例,我们先通过 DatePipe 管道和 UpperCasePipe 管道的组合,将生日日期先转换成短日期,然后再把结果大写化。最终显示的是:APR 15, 1988。

The chained hero's birthday is
{{ birthday | date | uppercase}}

如果我们需要给日期指定一个日期格式,可以利用括号来改变管道的执行顺序,比如:

The chained hero's birthday is
{{ ( birthday | date:'fullDate' ) | uppercase}}

五、自定义管道

ExponentialStrengthPipe 是一个计算次幂的自定义管道:

import { Pipe, PipeTransform } from '@angular/core';
/*
 * Raise the value exponentially
 * Takes an exponent argument that defaults to 1.
 * Usage:
 *   value | exponentialStrength:exponent
 * Example:
 *   {{ 2 |  exponentialStrength:10}}
 *   formats to: 1024
*/
@Pipe({name: 'exponentialStrength'})
export class ExponentialStrengthPipe implements PipeTransform {
  transform(value: number, exponent: string): number {
    let exp = parseFloat(exponent);
    return Math.pow(value, isNaN(exp) ? 1 : exp);
  }
}

这个管道有几个关键点:

  • 管道是一个使用管道元数据装饰的类。
  • 类必须实现 PipeTransform 接口 transform 方法,方法接收一个输入参数,和若干可选参数,最后返回转换后的值。
  • 管道的 transform 方法有一个可选参数 exponent
  • 我们通过 @Pipe 装饰器来告诉Angular这是一个管道类,所以还需要从Angular库中加载它。
  • @Pipe 装饰器有一个 name 属性,用来指定管道名称。名称必须是有效的JavaScript标识符,这里叫 exponentialStrength

现在演示如何在组件中调用自定义管道。

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

import { ExponentialStrengthPipe } from './exponential-strength.pipe';

@Component({
  selector: 'power-booster',
  template: `
    <h2>Power Booster</h2>
    <p>
      Super power boost: {{2 | exponentialStrength: 10}}
    </p>
  `,
  pipes: [ExponentialStrengthPipe]
})
export class PowerBooster { }

两个注意点:

  1. 自定义管道和内置管道使用方式完全相同。
  2. 必须在 @Component 装饰器的 pipes 数组里列出我们的管道。

六、Power Boost计算器

目前自定义的管道计算都是很死,现在我把他升级成一个利用 ngModel 双向绑定管道的输入和可选参数,这样我们只就可以在文本框输入数值。

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

import { ExponentialStrengthPipe } from './exponential-strength.pipe';

@Component({
  selector: 'power-boost-calculator',
  template: `
    <h2>Power Boost Calculator</h2>
    <div>Normal power: <input [(ngModel)]="power"></div>
    <div>Boost factor: <input [(ngModel)]="factor"></div>
    <p>
      Super Hero Power: {{power | exponentialStrength: factor}}
    </p>
  `,
  pipes: [ExponentialStrengthPipe]
})
export class PowerBoostCalculator {
  power = 5;
  factor = 1;
}

power-boost-calculator-anim

七、管道和变化检测

Angular每次JavaScript事件(如:按键、移动鼠标、定时器触发、服务器响应)后会执行数据绑定值变化检测,这可能会带来性能问题,Angular会尽可能在适当的时候提高性能。

如果我们使用管道,Angular会选择使用更简单、更快速的变化检测算法。现在来看看有无管道时Angular是如何优化性能的。

1、无管道

示例是一个显示 heroes 数组列表,并在每一变动时都会重新更新显示,这是一种比较积极变化检测策略。

组件模板内容:

New hero:
  <input type="text" #box
          (keyup.enter)="addHero(box.value); box.value=''"
          placeholder="hero name">
  <button (click)="reset()">Reset</button>
  <div *ngFor="let hero of heroes">
    {{hero.name}}
  </div>

组件类提供 heroes 属性,将新 hero 推入数组中,以及可以重置数据。

export class FlyingHeroesComponent {
  heroes:any[] = [];
  canFly = true;
  constructor() { this.reset(); }

  addHero(name:string) {
    name = name.trim();
    if (!name) { return; }
    let hero = {name, canFly: this.canFly};
    this.heroes.push(hero)
  }

  reset() { this.heroes = HEROES.slice(); }
}

任何对 heroes 数组进行操作都会重新被刷新显示。

2、Flying Heroes 管道

让我们来添加一个 FlyingHeroesPipe 管道用于过滤出那只能飞的 hero。先在组件模板里使用自定义的管道。

<div *ngFor="let hero of (heroes | flyingHeroes)">
  {{hero.name}}
</div>

FlyingHeroesPipe 自定义管道如下:

import { Pipe, PipeTransform } from '@angular/core';

import { Flyer } from './heroes';

@Pipe({ name: 'flyingHeroes' })
export class FlyingHeroesPipe implements PipeTransform {
  transform(allHeroes:Flyer[]) {
    return allHeroes.filter(hero => hero.canFly);
  }
}

当我们运行示例会发现很奇怪,添加的 hero 并不会显示。虽然没有得到我们想要的效果,但是Angular一切都是正常的,只不过使用了一个忽略变更列表的变化检测算法。

看看是如何新增一个 hero 的:

this.heroes.push(hero)

我们是将新 hero 对象推入到数组里面,数组的对象引并没有发生改变,它还是同一个数组,所以从Angular角度来说,相同数组、没有变化、不需要更新。

我们可以解决这个问题,就是使用 concat 将老数据和新数据连接成一些新的副本,并重新赋值给 heroes,这样数组的对象引用地址就变成新的,Angular也能检测到新的变化。

这里通过新增一个 Mutate Array 选择框来表示我们是采用 push (理解为:变改数组) 或 concat(理解为:替换数组) 来插入新 hero的。

flying-heroes-anim

通过替换数组这种方式比较变异的写法,而且很难控制,我们对数组的操作都要重新产生一个新副本,这肯定会让人崩溃。

因此,Angular提供另一种管道,用来解决检测变化时对对象引用依然有效的办法。

八、Pure和Impure管道

管道有两种类型:pureimpure,其中pure是默认类型,我们可以在管道元数据里指定 pure:false 表明管道是 impure 类型。

@Pipe({
  name: 'flyingHeroes',
  pure: false
})

现在我们来看看 pure 和 impure 两者的区别。

1、Pure管道

Angular只会检测到输入值变更才会执行pure管道。pure变更指的是原始输入值的变化(StringNumberBooleanSymbol)或对象引用的变化(DateArrayFunctionObject`)。

Angular会忽略对象内部的变化,一个对象引用检查要比一个深差异检查要快得多,这样Angular才能快速判断它是否可以跳过管道或屏幕更新。正因为有对象引用的受限,所以我们需要另一种管道的变化检测策略 impure 管道。

2、Impure管道

Angular每次组件变化检测周期时都会执行 impure 管道,每个按钮或鼠标移动时都会执行管道。所以实施一个 impure 管道要很注意了,因为管道执行不足够快,有可能会让用户体验变得很差。

3、impure 管道示例

现在我们把 FlyingHeroesPipe 用 impure 管道实现一个 FlyingHeroesImpurePipe 管道。

@Pipe({
  name: 'flyingHeroes',
  pure: false
})
export class FlyingHeroesImpurePipe extends FlyingHeroesPipe {}

唯一不同的是,只是在pipe元数据增加一个 pure:false在线示例

4、impure异步管道

Angular里有一个非常有趣的 AsyncPipe 管道,授收的是 PromiseObservable 输入值,并自己订阅它,最终返回产生的值。

它也有状态,管道维护 Observable 的订阅并保持从 Observable 传递值。

下面示例,演示 async 管道与一个 Observable 消息字符串的使用方法。

import { Component } from '@angular/core';
import { Observable } from 'rxjs/Rx';
// Initial view: "Message: "
// After 500ms: Message: You are my Hero!"
@Component({
  selector: 'hero-message',
  template: `
    <h2>Async Hero Message and AsyncPipe</h2>
    <p>Message: {{ message$ | async }}</p>
    <button (click)="resend()">Resend</button>`,
})
export class HeroAsyncMessageComponent {
  message$:Observable<string>;
  constructor() { this.resend(); }
  resend() {
    this.message$ = Observable.interval(500)
      .map(i => this.messages[i])
      .take(this.messages.length);
  }
  private messages = [
    'You are my hero!',
    'You are the best hero!',
    'Will you be my hero?'
  ];
}

异步管道保存样本是放在组件代码里,组件里面并没有订阅异步数据源。

5、impure 缓存管道

让我们再写一个 impure 管道,通过 HTTP 请求远程服务端数据。

如果请求的网址已经改变,管道会重新发起服务器请求;如果请求的是已经缓存的网址,则直接返回缓存的结果。

import { Pipe, PipeTransform } from '@angular/core';
import { Http }                from '@angular/http';
@Pipe({
  name: 'fetch',
  pure: false
})
export class FetchJsonPipe  implements PipeTransform{
  private fetched:any = null;
  private prevUrl = '';
  constructor(private _http: Http) { }
  transform(url: string): any {
    if (url !== this.prevUrl) {
      this.prevUrl = url;
      this.fetched = null;
      this._http.get(url)
        .map( result => result.json() )
        .subscribe( result => this.fetched = result );
    }
    return this.fetched;
  }
}

然后我们为了证明只调用一次请求,在模板里使用了两次管道。

template: `
  <h2>Heroes from JSON File</h2>
  <div *ngFor="let hero of ('heroes.json' | fetch) ">
    {{hero.name}}
  </div>
  <p>Heroes as JSON:
  {{'heroes.json' | fetch | json}}
  </p>
`,

尽管我们多次调用管道,但是通过浏览器开发者工具,确实只有一个请求。

hero-list

6、JsonPipe

Angular内建的 JsonPipe,将输入对象转换成JSON字符串输出,用来打印一次测试信息倒是很方便哈。

九、在类里使用管道

Angular2.x的管道其实就是1.x的过滤器,而管道有时候也需要在类里面调用,比如:DatePipe 是Angular内置管道,存放在 angular/common 库里面。

前面我们示例中所有的管道实际都是一个类,因此,如果我们想在某个组件类调用管道,可以先加载相应模块,然后按类实例调用 transform 方法即可。

import { DatePipe } from '@angular/common';

export class HeroBirthday {
  year = new DatePipe().transform(new Date(1988,3,15), 'y');
}

Angular2 NPM包

Angular应用和Angular自身依赖者很多第三方包,这些软件包的维护和安装都通过NPM来管理。Angualr建议利用快速入门的package.json做为基础。

{
  "dependencies": {
    "@angular/common": "2.0.0-rc.0",
    "@angular/compiler": "2.0.0-rc.0",
    "@angular/core": "2.0.0-rc.0",
    "@angular/http": "2.0.0-rc.0",
    "@angular/platform-browser": "2.0.0-rc.0",
    "@angular/platform-browser-dynamic": "2.0.0-rc.0",
    "@angular/router-deprecated": "2.0.0-rc.0",
    "@angular/upgrade": "2.0.0-rc.0",
    "systemjs": "0.19.27",
    "es6-shim": "^0.35.0",
    "reflect-metadata": "^0.1.3",
    "rxjs": "5.0.0-beta.6",
    "zone.js": "^0.6.12",
    "angular2-in-memory-web-api": "0.0.5",
    "bootstrap": "^3.3.6"
  },
  "devDependencies": {
    "concurrently": "^2.0.0",
    "lite-server": "^2.2.0",
    "typescript": "^1.8.10",
    "typings": "^0.8.1"
  }
}

npm有非常丰富的第三方包可以选择,我们可以增加和减少来满足应该需求。接下来解释这些包是什么意思、干什么用的。

一、dependencies和devDependencies节点

package.json 将依赖分成dependencies和devDependencies。前者是运行应用程序所需要包;后者是开发应用所需要包,可以在部署生产环境时除掉。

npm install my-application --production

二、依赖

package.jsondependencies 节点的依赖包分为三种类型:

  • 功能 功能包提供我们的应用框架和实用功能。
  • Polyfills Polyfills 插件消除JavaScript在各浏览器上的差异。
  • 其它 其它第三方库,比如:bootstrap

1、功能包

@angular/core – 应用程序所需要的关键运行库,包括所有元数据装饰器、ComponentDirective、依赖注入以及组件生命周期钩子。

@angular/common – Angular提供常用Service、管道和指令(表单)。

@angular/compiler – Angular模板编译器。

@angular/platform-browser – 有关DOM与浏览器相关的一切,尤其是帮助DOM渲染。包还包括引导应用启动方法,以及预编译模板方法。

@angular/platform-browser-dynamic – 动态编译?

**@angular/http ** – Angular Http客户端。

@angular/router – 组件路由。

@angular/upgrade – 用于升级1.x至2的实用工具集。

system.js – 兼容ES2015模块规范的动态模块加载器,也可以选择其他库代替,例如Webpack。SystemJS恰好是我们所有系列中的示例样本。

2、Polyfills 插件

Angular应用环境需要一些polyfills,这些polyfills列表都放在 dependencies 节点当中。

es6-shim – ES6引擎,用于解决低浏览器版本的一些ES6特性,不过一般只是在开发时会用它来做API转换,实现部署时这些东西应该都是移除的。

reflect-metadata – 一个依赖在Angular和TypeScript中间的元数据编译器。

rxjs – 一种利用 Observables 流的异步编程接口。

zone.js – 用于解决异步任务之间的持久性传递,2.x的事件绑定诸如 (click) 而无须像 1.x 中还要定义一个 ngClick 指令,这里面 zone 功不可没。

3、其它辅助库

bootstrap – 不用多话,最常用的HTML和CSS框架。

angular2-in-memory-web-api – 之前示例有出现过,用于模拟一个远程WEB API服务接口,用在测试上蛮方便的。

三、devDependencies

package.jsondevDependencies 节点列出的包是帮助我们开发应用,不需要被部署到生产环境中,

concurrently 一个同时运行多个 npm 命令的实用工具(支持os/x,Windows,Linus)

lite-server 一个轻型的静态文件服务器。

typescript TypeScript编译器。

typings TypeScript定义文件的管理器。

当然还有一些像测试和构建等工具也是放在这里。

四、总结

其实Angular2利用了在大量第三方库,好好的认知这些库对学习Angualr2非常有帮助。对于devDependencies节点,如何去构建一套便于团队开发的项目样本,就需要对各种测试、构建工具非常熟悉。

官网文章还有一小节是介绍 peerDependencies 节点,它是为了解决一些库的兼容性依赖问题,在我们目前接触的示例中从未体现过,但是在Angular源代码中有该节点。可在我写文章时,目前最新的源代码已经没有这个节点,我猜想可能是因为 npm3.x 的关系,因为npm3解决一个路径深度的问题。

Angualr2 生命周期钩子

Angular会管理一个组件的生命周期,包括组件的创建、渲染、子组件创建与渲染、当数据绑定属性变化时的校验、DOM移除之前毁销。

Angular提供组件生命周期钩子便于我们在某些关键点发生时添加操作。

一、组件生命周期钩子

指令和组件实例有个生命周期用于创建、更新和销毁,开发者可通过 angular2/core 库实现一个或多个这些生命周期钩子接口,每个接口都有一个单独的钩子方法,都是以接口名称加上 ng 前缀,比如:OnInit 接口有个钩子方法名为 ngOnInit,我们可以组件类实现它,比如:

import { OnInit } from 'angular2/core';

export class PeekABoo implements OnInit {
  constructor(private _logger:LoggerService) { }

  // implement OnInit's `ngOnInit` method
  ngOnInit() { this._logIt(`OnInit`); }

  protected _logIt(msg:string){
    this._logger.log(`#${nextId++} ${msg}`);
  }
}

不是所有指令或组件都能实现所有钩子方法,有的只能用在组件上。

Angular只能调用已经在指令/组件定义过钩子方法。

可选接口

从技术角度而言,对于JavaScript和TypeScript开发者来说接口是可选的,JavaScript语言并没有接口概念,Angular在运行过程中也没TypeScript接口说法,因为当TypeScript编译以后接口就消失了。

进一步来讲,其实对于Angular而言用不用钩子接口并不关心的,只要组件类存在钩子的命令规范,比如组件类有 onOnInit(),Angular自然就会调用。

尽管如此,大家还是按照接口的写法吧。

那么,刚才说过钩子接口只对指令或组件有效,而部分只对组件有效。

1、指令和组件都有效的钩子

钩子 目的
ngOnInit 发生于构造函数之后,用于初始化指令/组件,主要用于数据绑定的输入属性处理。
ngOnChanges 在数据绑定属性检查后被调用,在view和content children检查后其中之一发生改变之前。换个简单的说法,数据绑定属性的值发生变化前。
ngDoCheck 发生在每次变化检测时,主要用于在进行自定义的检查前扩展变化的检查或强制忽略检查。
ngOnDestroy 在Angular销毁组件/指令之前的清理工作,取消订阅监控对象和事件处理函数,以避免内存泄漏,仅会被调用一次。

2、组件有效的钩子

钩子 目的
ngAfterContentInit 在Angular将外部内容放到视图内之后。
ngAfterContentChecked 在Angular检测放到视图内的外部内容的绑定后。
ngAfterViewInit 在Angular创建了组件视图之后。
ngAfterViewChecked 在Angular检测了组件视图的绑定之后。

二、生命周期顺序

在Angular通过构造函数创建组件/指令后,就会按以下顺序调用这些生命周期钩子方法:

钩子 时间点
ngOnChanges 在 `ngOnInit` 之前,当数据绑定输入属性的值发生变化时。
ngOnInit 在第一次 `ngOnChanges` 之后。
ngDoCheck 每次Angular变化检测时。
ngAfterContentInit 在外部内容放到组件内之后。
ngAfterContentChecked 在放到组件内的外部内容每次检查之后。
ngAfterViewInit 在初始化组件视图和子视图之后。
ngAfterViewChecked 在组件视图和子视图检查之后。
ngOnDestroy 在Angular销毁组件/指令之前。

三、其他生命周期钩子

除上面列出的钩子外,Angular子系统也可能有自个生命周期钩子。例如,路由器,也有自己的钩子,使我们能够在路由导航的特定时刻做一些操作。

ngOnInitrouterOnActivate 都是属于钩子,为了避免发生冲突,二者前缀规范不同。当然第三方库也可能是会定义一个钩子,总之呀多注意点命令方面的问题。

四、生命周期示例简要说明

下面会通过一系列的示例,演示生命周期钩子的使用方法。所有示例都以 AppComponent 为父组件,每个子组件都会特定对某个生命周期的钩子方法进行演示。

所有示例的在线DEMO,最好是和在线DEMO一起阅读,因为后面并不会完整的去展示代码。以下是每个子组件的简要说明:

组件 说明
Peek-a-boo 演示每个生命周期钩子,每个钩子被调用时都会在屏幕上打印日志。
Spy 指令生命周期钩子,利用 `SpyDirective` 指令的 `ngOnInit` 和 `ngOnDestroy` 钩子,演示一个指令是如何被创建或销毁的。
OnChanges 看看Angular调用 `ngOnChanges` 钩子在 `changes` 对象每一次变化时的调用情况。
DoCheck 实现一个自定义变化检测 `ngDoCheck` 钩子方法,看看Angular如果调用钩子及它更改会日志的变化。
AfterView 演示Angular如何显示一个视图,包括 `ngAfterViewInit` 和 `ngAfterViewChecked` 钩子。
AfterContent 展示如何将项目外部内容放进组件内,且如何把项目内容和组件子视图区分开来,演示的钩子包括 `ngAfterContentInit` 和 `ngAfterContentChecked`。
Counter 演示一个自定义钩子示例。

五、Peek-a-boo

利用 PeekABooComponent 演示所有钩子。在实际项目中,我们很少需要实现所有钩子接口,这个示例为的是看看Angular调用钩子的执行顺序。这里有个快照,点击创建按钮再一次点击销毁的钩子调用过程。

peek-a-boo

根据日志消息的显示,钩子调用的顺序:OnChangesOnInitDoCheck(3次)、AfterContentInitAfterContentChecked(3次)、AfterViewInitAfterViewChecked(3次)、OnDestroy

如果我们点击 Update Hero 按钮,会看到另一个 OnChanges 和两对 DoCheckAfterContentCheckedAfterViewChecked

六、Spying OnInit and OnDestroy

现在来揭开这两个钩子到底是如何初始化和销毁。

创建一个 spy 指令,并实现 ngOnInitngOnDestroy 钩子,并注入 LoggerService,当钩子被调用时能在屏幕上打印日志。

// Spy on any element to which it is applied.
// Usage: <div mySpy>...</div>
@Directive({selector: '[mySpy]'})
export class Spy implements OnInit, OnDestroy {

  constructor(private _logger: LoggerService) { }

  ngOnInit()    { this._logIt(`onInit`); }

  ngOnDestroy() { this._logIt(`onDestroy`); }

  private _logIt(msg: string) {
    this._logger.log(`Spy #${nextId++} ${msg}`);
  }
}

我们可以把指令用到任何本地模板或组件元素当中,这样当这些元素初始化时或销毁时指令也会跟着初始化或销毁。

`<div *ngFor="let hero of heroes"  mySpy  class="heroes">
   {{hero}}
 </div>`

这里 spy 指令(mySpy)依附在 <div> 元素,意味者元素的初始化或销毁也会发生在指令身上。当添加一个新 hero 时,会多出一个初始化指令的动作,如果点击 reset,所有的指令也会跟着元素一同被销毁。

spy-directive

ngOnInitngOnDestroy 在实际项目应用中发挥很重要的作用,接着看看为什么我们需要他们?

1、ngOnInit

使用 ngOnInit 有两个主要理由:

  • 在构造函数之后执行复杂初始化。
  • 在设置组件之后设置输入属性。

ngOnInit 经常用来获取数据,比如之前我们介绍的 HTTP 客户端的文章。

为什么我们不把获取数据放在构造函数里呢?之前HTTP文章也谈过,试想如果在构造函数里调用一个远程服务时有异常,那实例到底应该是成功还是失败呢?一个类的构造函数应该是简单且安全的。

当一个组件必须很快的启用并开始工作时,就应该让Angular去调用 ngOnInit 方法来启用这些复杂的业务逻辑。

记住一个指令数据绑定输入属性是必须等构造函数完成以后才会开始设置,如果需要基于这些属性来初始化指令,也必须在 ngOnInit 里运行它。

2、ngOnDestroy

ngOnDestroy 主要用来清除一些逻辑,以及那些无法被GC自动回收释放资源,比如:退订observable、DOM事件、停止interval计时器等。

七、OnChanges

下面是一个监测 OnChanges 钩子的示例,每当输入属性发生变化时,Angular会调用 ngOnChanges 方法。

OnChangesComponent 组件

ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
  for (let propName in changes) {
    let prop = changes[propName];
    let cur  = JSON.stringify(prop.currentValue);
    let prev = JSON.stringify(prop.previousValue);
    this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`);
  }
}

ngOnChanges 钩子接收一个类型为 SimpleChange 的映射对象,包括新值和旧值。

组件里还指定两个输入属性:

@Input() hero: Hero;
@Input() power: string;

父组件绑定示例:

<on-changes [hero]="hero" [power]="power"></on-changes>

最终效果

on-changes-anim

我们看到 power 的每一次输入变化都会打印出日志,而为什么 hero 没有什么任何检测日志呢?

这是因为Angular只会对那些值会发生变化的输入属性调用 ngOnChanages,而 hero 属性是一个引用 hero 对象,Angular无法知道 hero 的 name 属性是不是已经发生变化了。所以Angular的 onChanage 钩子对于引用对象类型是无效的。

八、DoCheck

我们可以使用 DoCheck 钩子来改变变化检测结果。DoCheck 示例是在 OnChanages 示例代码基础上扩展并实现 DoCheck:

ngDoCheck() {

  if (this.hero.name !== this.oldHeroName) {
    this.changeDetected = true;
    this.changeLog.push(`DoCheck: Hero name changed to "${this.hero.name}" from "${this.oldHeroName}"`);
    this.oldHeroName = this.hero.name;
  }

  if (this.power !== this.oldPower) {
    this.changeDetected = true;
    this.changeLog.push(`DoCheck: Power changed to "${this.power}" from "${this.oldPower}"`);
    this.oldPower = this.power;
  }

  if (this.changeDetected) {
      this.noChangeCount = 0;
  } else {
      // log that hook was called when there was no relevant change.
      let count = this.noChangeCount += 1;
      let noChangeMsg = `DoCheck called ${count}x when no change to hero or power`;
      if (count === 1) {
        // add new "no change" message
        this.changeLog.push(noChangeMsg);
      } else {
        // update last "no change" message
        this.changeLog[this.changeLog.length - 1] = noChangeMsg;
      }
  }

  this.changeDetected = false;
}

我们手动检测并比较新值与旧值,当发生变化时打印日志。

do-check-anim

现在我们可以检测 hero.name 的变化了,但一定要小心。ngDoCheck 钩子调用频率非常高,不管你有没有值发生变化,每个变化都会进行一次检测。

就拿上面的示例来说,哪怕你什么值也没有改变,离开焦点时也会进行一次检测,所以 ngDoCheck 最好不要做一些很复杂的事情,这样会影响用户体验。

九、AfterView

AfterView示例探讨 AfterViewInitAfterViewChecked,Angular会在创建组件的子视图(视图可以理解为指令或组件)后调用它们。

子视图是一个文本框里显示 hero 的 name。

@Component({
  selector: 'my-child',
  template: '<input [(ngModel)]="hero">'
})
export class ChildViewComponent {
  hero = 'Magneta';
}

AfterViewComponent 组件的模板内显示子视图:

template: `
  <div>-- child view begins --</div>
    <my-child></my-child>
  <div>-- child view ends --</div>`

为了能让钩子能够检测值变化,还需要使用 @ViewChild 查询类型为 ChildViewComponent 的组件,并绑定到 viewChild 属性。

export class AfterViewComponent implements  AfterViewChecked, AfterViewInit {
  private _prevHero = '';

  // Query for a VIEW child of type `ChildViewComponent`
  @ViewChild(ChildViewComponent) viewChild: ChildViewComponent;

  ngAfterViewInit() {
    // viewChild is set after the view has been initialized
    this._logIt('AfterViewInit');
    this._doSomething();
  }

  ngAfterViewChecked() {
    // viewChild is updated after the view has been checked
    if (this._prevHero === this.viewChild.hero) {
      this._logIt('AfterViewChecked (no change)');
    } else {
      this._prevHero = this.viewChild.hero;
      this._logIt('AfterViewChecked');
      this._doSomething();
    }
  }
  // ...
}

遵守单向数据流规则

_doSomething 方法用来检测当 name 超过10个字符时显示 “That’s a long name”。

// This surrogate for real business logic sets the `comment`
private _doSomething() {
  let c = this.viewChild.hero.length > 10 ? "That's a long name" : '';
  if (c !== this.comment) {
    // Wait a tick because the component's view has already been checked
    setTimeout(() => this.comment = c, 0);
  }
}

为什么 _doSomething 方法需要使用 setTimeout 来延迟更新 comment 呢?

我们必须坚持Angular单向数据流规则,换句话说,如果我们立即更新组件数据绑定 comment 属性,而此时正在检测变化Angular会抛出异常,这一点很容易发生因为当 ngAfterViewChecked 触发前会先进行 DoCheck 这之前我们也讨论过,所以这里才用 setTimeout 推迟浏览器JavaScript循环周期。

after-view-anim

十、AfterContent

AfterContent示例探讨 AfterContentInitAfterContentChecked,Angular会在项目外部内容被放进组件内后调用它们。

1、内容投影

内容投影是将外部组件的HTML内容插入到组件模板的指定位置;跟Angular1.x的transclusion类似。

本示例同 AfterView 的示例一样,只不过这一次,我们父组件模板多套一层 AfterContentComponent 组件:

`<after-content>
   <my-child></my-child>
 </after-content>`

注意, <after-content> 标签包裹着 <my-child> 标签,除非我们打算把内容也插入到组件里,否则永远不要把其他元素放到这中间。

现在看看组件模板:

template: `
  <div>-- projected content begins --</div>
    <ng-content></ng-content>
  <div>-- projected content ends --</div>`

<ngContent> 标签是外部内容的占位符,告诉Angular要在哪插入内容。

2、AfterContent 钩子

AfterContent钩子类似于AfterView钩子,关键的区别在于如何查找子组件。

  • AfterView钩子涉及 ViewChildren,子组件通过组件模板插入。
  • AfterContent钩子涉及 ContentChildren,子组件通过映射内容插入。

接下来 AfterContent 钩子只能通过属性装饰 @ContentChild 查询组件。

export class AfterContentComponent implements AfterContentChecked, AfterContentInit {
  private prevHero = '';
  comment = '';

  // Query for a CONTENT child of type `ChildComponent`
  @ContentChild(ChildComponent) contentChild: ChildComponent;

  ngAfterContentInit() {
    // viewChild is set after the view has been initialized
    this.logIt('AfterContentInit');
    this.doSomething();
  }

  ngAfterContentChecked() {
    // viewChild is updated after the view has been checked
    if (this.prevHero === this.contentChild.hero) {
      this.logIt('AfterContentChecked (no change)');
    } else {
      this.prevHero = this.contentChild.hero;
      this.logIt('AfterContentChecked');
      this.doSomething();
    }
  }
  // ...
}

[基础系列] 如何更好的组织 Angular2 文件结构

当我的文章写到最后时,我发现了在Github里面已经有个非常专业的Angular2样板项目angular2-webpack-starter,他完全包括我想要的东西甚至很多我并未考虑的,比如:单元测试、热替换等。所以我删除我的文章,只保留一些我认为有必要声明的理论知识;转而对angular2-webpack-starter的简单翻译事宜。

一、写在前面

angular2-webpack-starter已经有完整的解决方案,而且非常优秀。但在此之前我会解释一些理论问题。

1、模块规范

模块是一个极为重要的概念,别人写的功能代码你可以直接拿过来用;但,如果你的代码没有按指定的规范来写,那怎么可能做到代码相互使用呢?虽然 ES6 已经有模块的概念,但目前整体Javascript发展来看还需要一段时间。

目前最常用的有CommonJS、AMD、CMD。这三者的主要区别在于,从加载模块的角度出发:

CommonJS
比较适合服务端,因为当我们使用 require('math') 来加载一个模块时,由于模块文件在服务端都是存在本地硬盘上,所以非常快的加载进来。而如果我们把这一行代码放在客户端里面,我们就会遇到一个问题 异步加载,因此才会有AMD规范这一说。

AMD
AMD为的就是解决CommonJS在前端异步加载的不足,当然国产的CMD也是如此,只不过二者存在加载顺序上的区别,不过现在已经没有区别了,因为AMD也改为跟CMD相同原理的加载模式。

SystemJS
万能的加载器,她的定位包括ES6代码,当然既然是万能就少不了:CSS、图片。

三者的规范都是为了解决一些模块加载问题,那么选用什么规范更合理呢?这取决于你的需求,合理需求选用合理的规范。

2、我的选择?

我个人比较偏爱CommonJS,因为我们大部分项目都是单页应用,换句话说CommonJS先天性不足,并不会对我们有什么影响。当然相比较其它规范,有着更多的优势:

  • CommonJS更容易被流行的打包工具所接受,比如:Webpack、Browserify
  • CommonJS比SystemJS更容易
  • CommonJS是node.js格式,允许组件更容易在服务端渲染
  • CommonJS服务端和前端共用代码
  • CommonJS生成的文档可以省20%的空间

二、细说angular2-webpack-starter

angular2-webpack-starter是使用Webpack来构建针对Angular2和TypeScript的样板项目,还包括E2E和Karma单元测试,他列举几项使用样板项目的理由:

  • 它是构建Angular2文件组织的最佳实践。
  • 基于Webpack编译TypeScript。
  • 自动化单元测试。
  • 热替换。
  • 使用Material Design。

1、快速启动

首先要确保 node版本4.0 以上。

# 从git克隆一下最新版本
git clone --depth 1 https://github.com/angularclass/angular2-webpack-starter.git

# 切换至项目根目录
cd angular2-webpack-starter

# 先添加一些全局库,这样下次使用时不需要再重新获取 
# 在安装webpack时,我出现了 AttributeError: ‘MSVSProject’ object has no attribute ‘iteritems’ 异常
# 后来我更改为: npm install webpack -g 既然成功了。
# 如有其它异常见:http://asdfblog.com/npm-install-errors.html(欢迎提交更多异常和解决方案)
cnpm install typings webpack-dev-server rimraf webpack -g

# 使用cnpm(淘宝NPM 镜像)安装组件
cnpm install

# 启动
# 如果收到 `Cannot find name 'Promise'.` 异常时,表明未正确安装ts定义
# 重新运行:npm run typings -- install
npm start

# 获取启动一个带有热替换的服务
npm run server:dev:hmr

最后在浏览器访问:http://0.0.0.0:3000 或 http://localhost:3000,效果如下:

test

2、文件结构

angular2-webpack-starter使用组件风格来管理文件,这很符合Angular2的发展新标准,这样有利于代码可维性和行为逻辑封装。一个组件的基本组成包括:组件类、样式、模板文件、单元测试等。具体结构如下:

angular2-webpack-starter/
 ├──config/                    * 配置文件夹
 |   ├──helpers.js             * 配置辅助类
 |   ├──spec-bundle.js         * 测试文件所需要的额外配置
 |   ├──karma.conf.js          * karma 配置文件(单元测试)
 |   ├──protractor.conf.js     * protractor 配置文件(端对端测试)
 │   ├──webpack.common.js      * webpack 开发、产品、测试模式通用配置文件 
 │   ├──webpack.dev.js         * webpack 开发模式配置文件 
 │   ├──webpack.prod.js        * webpack 产品模式配置文件 
 │   └──webpack.test.js        * webpack 测试模式配置文件
 │
 ├──src/                       * 程序源代码
 |   ├──main.browser.ts        * webpack的entry入口
 |   ├──index.html             * index.html模板文件 
 |   ├──polyfills.ts           * polyfills 文件(提供支持ES6特性)
 |   ├──vendor.ts              * 第三方组件(目的为webpack能够正常打包)
 |   ├──custom-typings.d.ts    * 自定义ts定义,ts编译时非常严格
 │   ├──app/                   * APP应用源文件
 │   │   ├──app.spec.ts        * 组件app.ts的单元测试
 │   │   ├──app.e2e.ts         * 端对端的测试文件 
 │   │   └──app.ts             * App.ts 组件入口
 │   │
 │   └──assets/                * 静态文件
 │       ├──icon/              * icon文件 
 │       ├──service-worker.js  * 忽略
 │       ├──robots.txt         * 相关搜索引擎
 │       └──human.txt          * 见:humanstxt.org
 │
 ├──nitrous.json               * 一个逆天的构建在线测试站(需要费用)
 ├──tslint.json                * typescript lint 配置文件
 ├──typedoc.json               * typescript 文档生成配置
 ├──tsconfig.json              * typescript 编译配置文件
 ├──typings.json               * ts定义配置文件
 └──package.json               * npm配置文件

app/home/ 相当于一个页面的组件都包括在该文件夹下,同时文件夹又嵌套着页面所需要的其他组件,构建一个完整的ng2网站是由若干组件组合而成,一个页面可以被细分出不同子组件,整体组件的结构图跟一个树结构一样。

3、入门指南

在【1、快速启动】已经做了很多注释以及可能发生的QA,不再赘述。重点来看angular2-webpack-starter提供的一些运行指令。

其它运行指令

编译文件

# 编译开发版本
npm run build:dev
# 编译生产环境版本
npm run build:prod

编译成功后会在根目录下生成一个 dist 文件夹。

热替换模式

npm run server:dev:hmr

意味者进入开发模式后,你可以打开 http://localhost:3000,当你修改ts文件时会自动重新编译,并以热替换的形式变更页面效果。(非常赞喔,特别是安装ssd硬盘的,响应更佳!)

监听和编译

npm run watch

意味者进入开发模式后,你可以打开 http://localhost:3000,当你修改ts文件时会自动重新编译。

运行测试

npm run test

运行并监听测试

npm run watch:test

运行端对端测试

npm run watch:test

4、自定义类型定义(TS)

当使用第三方模块时还需要包含模块的类型定义,可以尝试使用 typings install node --save 来安装TS定义。但是并不是所有模块都已经在TS中已经有注册相应的类型定义文件。那么,我们只能自己来写这些TS定义信息(这也是没有办法TS编译器在编译过程中严格规定所有类型都需要有明确的定义),例如:

declare module "my-module" {
  export function doesSomething(value: string): string;
}

或者,可以定义一个全局的 any 类型变量,例:

declare var assert: any;

总结

angular2-webpack-starter是一个非常棒的样板项目,你可以直接在此基础上做产品开发,即便自己建也未必能够这么周到;同时项目已经被Star:2415可见有多么热门。

angular2-webpack-starter常见问题清单已经有很多可能发生的问题并已经给出了具体解决方案,但凡在使用过程有问题也可以去提交issues。

之前一直在群里(QQ群:485843913)经常会提出Angular2环境搭建问题,特别是拿官网的快速入门做为敲门砖,但是官网的示例在现实生产环境中一点也不合格;我相信angular2-webpack-starter是最简单解决我们生产环境下所有问题的最佳样板项目了。读懂她,才算真正才能开始入门Angualr2吧。

03.TypeScript 类

JavaScript语言基于函数与原型链继承的方式来构建可重用组件。对于OO编程来说会很奇怪,在下一代JavaScript标准(ES6)已经为我们提供OO设计方式。而TypeScript已经实现这种方式,无须等待ES6就可以放心的使用。

一、类

class Greeter {
    greeting: string; // public 属性
    constructor(message: string) { // 类构造函数
        this.greeting = message;
    }
    greet() { // public 方法
        return "Hello, " + this.greeting;
    }
}

// 使用new实例一个Greeter对象
var greeter = new Greeter("world");

和其他语言一样,我们可以在类方法里面使用 this 来代表当前实例的对象。

二、继承

OO编程最基本就是继承,先来看一个示例:

class Animal {
    name: string;
    constructor(theName: string) {
        this.name = theName;
    }
    move(meters: number = 0) {
        alert(this.name + ' moved ' + meters + 'm.');
    }
}

class Snake extends Animal {
    constructor(name: string) {
        super(name);
    }
    move(meters = 5) {
        alert('Slithering...');
        super.move(meters);
    }
}

class Horse extends Animal {
    constructor(name: string) {
        super(name);
    }
    move(meters = 45) { // 重写父类方法
        alert('Galloping...');
        super.move(meters); // 调用父类方法
    }
}

var sam = new Snake('Sammy the Python');
var tom: Animal = new Horse('Tommy the Palomino');

sam.move();
tom.move(34);

案例中和普通类只不过多了一个 extend 来表示类的继承关系,这和接口不同,只允许单一继承。

三、Private/Public 访问限制

在TypeScript中默认的访问限制为:public,这也就是为什么上面的案例都没有出现 public 字眼。如果想定义私有方法只须在方法前加:private。

class Animal {
    private name: string;
    constructor(theName: string) {
        this.name = theName;
    }
    say() {
        alert(this.name);
    }
}

四、参数属性

参数属性是访问限制另一种简单的写法,比如我们将上面案例改写成:

class Animal {
    constructor(private name: string) {
    }
    say() {
        alert(this.name);
    }
}

当我们在构造函数里声明一个带有 private 属性时,同时会自动将这个属性初始化成一个类私有属性。

五、属性访问器

在C#里,可以对某个属性的读和写(即:public string name { get; set; } )操作时执行语句。同样,TypeScript也有类似的实现方式。

class Employee {
    private _fullname: string;
    get fullname(): string {
        return this._fullname;
    }
    set fullname(newName: string) {
        // 做点其它的
        this._fullname = newName;
    }
}

var employee = new Employee();
employee.fullname = "asdf";
alert(employee.fullname);

六、静态

通过 static 标记某个属性或方法,这和我其他语言的使用方法一样,其可见性都是为类级访问。

class Grid {
    constructor(public scale: number) { }
    static origin = { x: 0, y: 0 } // 静态属性
    static show() { // 静态方法
        alert('sho');
    }
    cal(point: { x: number; y: number; }) {
        var xDist = (point.x - Grid.origin.x);
        var yDist = (point.y - Grid.origin.y);
        return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    }
}

var grid1 = new Grid(1.0);  // 1x scale
var grid2 = new Grid(5.0);  // 5x scale

alert(grid1.cal({ x: 10, y: 10 }));
alert(grid2.cal({ x: 10, y: 10 }));

02.TypeScript 接口

TypeScript核心设计原则之一就是类型检查,而接口实际是为了这种类型检查而产生的,实际他并不会在生成的JavaScript里有半点体现。

一、一个简单的示例

// 定义IUser接口
interface IUser {
    name: string; // 姓名:字符串类型
    age: number; // 年龄:数值类型
}

function getUser(user: IUser) : string {
    return user.name + ':' + user.age;
}

var userInfo: string = getUser({ age: 25, name: 'Kase' });
alert(userInfo);

getUser 接收的参数是一个 IUser 接口,所以在传参时ts会要求里必须要出现 age 和 name。

IUser 目的单纯只是为了编写TypeScript时更有效的开发,而且这种接口并不会在生成后的JavaScript有任何体现,我们来看看生成后的JavaScript:

function getUser(user) {
    return user.name + ':' + user.age;
}

var userInfo = getUser({ age: 25, name: 'Kase' });
alert(userInfo);

二、可选参数

如上文的示例,必须是 age 和 name 存在,这样ts才会检查通过。但有时我们又不希望接口所定义的属性都是必须的,我们可以在属性名加上 ?,来表示一个属性是可选项。

interface IUser {
    name: string; // 姓名:字符串类型
    age: number; // 年龄:数值类型
    sex?: boolean // 性别:布尔型,但是可选项
}

function getUser(user: IUser) : string {
    return user.name + ':' + user.age + ':' + (user.sex ? 'm' : 'f');
}

var userInfo: string = getUser({ age: 25, name: 'Kase' });
alert(userInfo);

三、函数类型

有时我们需要明确规定一个函数的参数和返回值时,也可以定义一个接口来实现约定。这种接口对于把某个函数做为一个第三方库被使用时,非常有用,因为他可以明确在IDE中告诉你参数类型。虽然有点遗憾ts在vs下在智能提醒时并不会包括强力的注释。

// 健康状态
interface Healthy {
    (height: number, weight: number): boolean
}

var myHealthy: Healthy;
myHealthy = function (height: number, weight: number) {
    return true;
}

console.log(myHealthy(170, 120));

对于调用者而言 myHealthy 必须传递两个数值类型的数据。

四、Class 类型

1、实现Interface

在TypeScript中也能实现类似C#中interface,用来强制实现类的结构。

interface IClock {
    currentTime: Date;
    setTime(d: Date);
}

class Clock implements IClock {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }

    constructor(h: number, m: number) { }
}

Clock 是 IClock 的实现类,明确约定必须要实现 currentTime 属性和 setTime 函数。

2、类static和instance区别

当使用类和接口时,类有两种类型:static(静态)部分和instance(实例)部分;由于ts只会对instance部分进行类型检查,而构造函数是属于静态部分,所以当我们使用一个带有构造签名的接口时,就会出错。

interface IClock {
    new (h: number, m: number);
}

class Clock implements IClock {
    constructor(h: number, m: number) { }
}

当然我们需要明确规定某个类在调用构造函数时参数时,我们可以这样子。。。但我不想说,因为我自己使用下来完全在违背ts的设计原则。在c#的接口中是无法约定接口的构造方法的,所以这种想法就需要提及。

3、interface继承

interface也能被继承,还可以组合继承多个接口。

interface Shape {
    color: string;
}

interface PenStroke {
    penWidth: number;
}

interface Square extends Shape, PenStroke {
    sideLength: number;
}

var square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

console.log(square);

© 2017 卡片机色彩 沪ICP备13032872号-3

Theme by cipchk

to top