Angular2 官网文章阅读指南

本系列是根据angular.io的Docs栏目按顺序翻译。首先目的并不是为了翻译工作,主要是针对自己Angular2学习并到过程做纪录;其次每一篇并非按官方直译,都是结合自身理解做叙述。

基础

开发指南

如有您对Angular2.x感兴趣,也可加入QQ群:485843913。

angular-cli如何启用HMR

webpack有一个非常好玩的插件叫:模块换替换(HMR),即添加或删除模块时应用持续运行,不需要页面刷新。虽然angular cli至问世以来就以webpack为核心,可直到【1.0.0-beta.22】以上版本才开始支持。

那么,接下来我们来玩一玩Angular的热替换。

先决条件

  • angular cli 至少 1.0.0-beta.22 以上版本

创建项目

使用Angular CLI创建一个空项目。

ng new ng-hmr
cd ng-hmr

添加HMR环境配置文件

HMR需要一定代码嵌入,而这些模块我们希望只在开发过程中使用,所以这里创建一个环境配置文件,用于区分我们什么时候需要引入 hmr 模块,而不会影响我们生产环境。

首先,创建 src/environments/environment.hmr.ts 文件,内容为:

export const environment = {
  production: false,
  hmr: true
};

这里相比较默认两个环境,增加了 hmr: true 变量。那么,另外两个,也一并设置相应 hmr 变量,来确保什么情况下才允许开启 HMR。

environment.prod.ts 生产环境肯定不会开启它:

export const environment = {
  production: true,
  hmr: false
};

environment.ts 默认环境,如果有必要也可以设置为 true,为了后面更好的演示,这里我设置 false

export const environment = {
  production: false,
  hmr: false
};

新环境配置还需要更新 angular-cli.jsonenvironments 节点,以确保angular cli 能认识:

      "environments": {
        "source": "environments/environment.ts",
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts",
        // 新增的节点
        "hmr": "environments/environment.hmr.ts"
      }

到此,与配置相关已经完成,你可以尝试使用 ng serve --hmr -e=hmr 来启用HMR。而且和之前相比会有这样一段提醒:

NOTICE Hot Module Replacement (HMR) is enabled for the dev server.
The project will still live reload when HMR is enabled,
but to take advantage of HMR additional application code is required
(not included in an Angular CLI project by default).
See https://webpack.github.io/docs/hot-module-replacement.html
for information on working with HMR for Webpack.

添加angular-hmr模块依赖与配置

正如提醒信息中说的,Angular CLI默认并不提供HMR所需要的额外应用代码。但Angular已经有了一个现成HMR模块即【Angular 2 Hot Module Replacement】,安装它:

npm install --save-dev @angularclass/hmr

接下来,创建 src/hmr.ts 文件,配置模块内容:

import { NgModuleRef, ApplicationRef } from '@angular/core';
import { createNewHosts } from '@angularclass/hmr';

export const hmrBootstrap = (module: any, bootstrap: () => Promise<NgModuleRef<any>>) => {
    let ngModule: NgModuleRef<any>;
    module.hot.accept();
    bootstrap().then(mod => {
        ngModule = mod;
    });
    module.hot.dispose(() => {
        let appRef: ApplicationRef = ngModule.injector.get(ApplicationRef);
        let elements = appRef.components.map(c => c.location.nativeElement);
        let makeVisible = createNewHosts(elements);
        ngModule.destroy();
        makeVisible();
    });
};

最后,修改 src/main.ts 内容:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode, ApplicationRef } from '@angular/core';
import { environment } from './environments/environment';
import { AppModule } from './app/app.module';

import { hmrBootstrap } from './hmr';

if (environment.production) {
  enableProdMode();
}

const bootstrap = () => {
  return platformBrowserDynamic().bootstrapModule(AppModule);
}

if (environment.hmr) {
  if (module['hot']) {
    hmrBootstrap(module, bootstrap);
  } else {
    // 未加上 --hmr 时,控制台会有错误提醒
    console.error('HMR没有启用,确保 ng server 命令加上 --hmr 标记');
  }
} else {
  bootstrap();
}

运行

现在,我们可以使用以下命令来启动HMR:

ng serve --hmr -e=hmr

尝试修改一个 app.component.tstitle 属性值,保存后,无须刷新页面,立即生效:

效果图

快点,享受更快乐的编码方式吧!

Angular2 动态创建组件

在ng1.x时我写过Angular 动态创建 directive;而在Angular2应用是由一个庞大的组件树来构建业务,一般而言我们是通过 template 模板来动态加载组件。

一、原因

我依然以1.x所遇到的项目实现为原型。

我们所认知的编辑器是为创建一个富HTML,而为了让用户更好的体验(总不能让客户写HTML语言吧)所产生的按表单填写数据最终生成有效的HTML。那么自然我们不可能只放一种像百度编辑器就完事,你让用创建一个轮播广告几乎是不可能;所以需要有很多组件,比如:商品列表、富文本、图片广告、轮播广告等等,而每一种组件又不可能是统一界面。

二、创建组件

首先需要先创建了两个组件 richconfig

src/components/rich.ts

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

@Component({
  selector: 'rich',
  template: `
    <p>Rich Content</p>
  `
})

export class RichComponent { }

src/components/config.ts

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

@Component({
  selector: 'config',
  template: `
    <p>Config Content</p>
  `
})

export class ConfigComponent { }

三、利用模板动态加载组件

template 模板里面加载组件是最常用加载方式,而且非常简单,只需要存放相应的 selector 模板引擎会自己查找并编译。

//our root app component
import {Component} from '@angular/core'

import {ConfigComponent} from 'src/components/config'
import {RichComponent} from 'src/components/rich'

@Component({
  selector: 'template-load',
  providers: [],
  template: `
      <h1>方式一:通过 template 动态加载</h1>
      <config></config>
      <rich></rich>
  `,
  directives: [ ConfigComponent, RichComponent ]
})
export class TemplateLoadComponent {
  constructor() {

  }
}

这样的方式很明显的一点是,template的结构将决定组件在组件树当中所处的位置。假如我们要把 configrich 组件对换位置,不得不对 template 重新编写,这样更别说让用户来拖拽来调整组件显示位置这么高级的办法了。

四、利用 DynamicComponentLoader 实现动态加载

DynamicComponentLoader 支持两种不同的加载方式来动态创建组件;但在这之前我们需要先做点什么。

首先,我示例里是根据按钮点击事件创建相应的组件,所以我们需要先创建一个组件映射表。

import {ConfigComponent} from 'src/components/config'
import {RichComponent} from 'src/components/rich'

export class DynamicLoadComponent {
  compMaps: {[key: string]: any} = {},
  constructor() {
    this.compMaps.config = ConfigComponent;
    this.compMaps.rich = RichComponent;
  }
}

这样我们可以模板里面定义添加事件,并传递相应的key值,下面是分别定义两个按钮用来创建 configrich 组件,以及 container1 元素用于放置组件位置。

<button type="button" (click)="add('config')">Add Config</button>
<button type="button" (click)="add('rich')">Add Rich</button>
<div id="container1"></div>

准备工作完成后,我们分别来看一下二种方式的差异:

1、loadAsRoot

loadAsRoot(type: Type, overrideSelectorOrNode: string|any, injector: Injector, onDispose?: () => void, projectableNodes?: any[][]) : Promise<ComponentRef<any>>

loadAsRoot 方法会创建一个 type 组件实例,然后放到 overrideSelectorOrNode 放处位置的第一个元素下面,不管你动态加载几次永远都只有一份组件实例

constructor(public dcl:DynamicComponentLoader, public _injector:Injector) {
}

add(comp) {
  this.dcl.loadAsRoot(this.compMaps[comp], "#container1", this._injector);
}

#container1 位置可以在整个文档的任意位置,只要该ID是存在。

最终的DOM结构像这样:

<div id="container1">
   <p>Config Content</p>
</div>

loadAsRoot 方法很好理解,而且很霸道,始终只会创建一个组件实例。

2、loadNextToLocation

loadNextToLocation(type: Type, location: ViewContainerRef, providers?: ResolvedReflectiveProvider[], projectableNodes?: any[][]) : Promise<ComponentRef<any>>

同样也是创建一个 type 组件实例,并插入到指定 location 位置的后面,ViewContainerRef 是一个视图容器,所以通过 loadNextToLocation 方法加载的组件,都会放入容器里面。

首先需要在模板中定义一个本地模板变量 container2,并在组件类中获取该变量。

@Component({
   template: `
    <div #container2></div>
   `
})

export class DynamicLoadComponent {
  @ViewChild('container2', {read: ViewContainerRef}) target;
}

add 方法也非常简单:

  add(type) {
    this.dcl.loadNextToLocation(this.compMaps[comp], this.target)
  }

最终的DOM结构像这样:

<config>
  <p>Config Content</p>
</config>
<rich>
  <p>Rich Content</p>
</rich>

3、传递数据

两种方法返回的结果都是一个 Promise 类型,并传递一个 ComponentRef 的新创建组件对象。


this.dcl.loadNextToLocation(this.compMaps[comp], this.target).then(function(cmpRef) { // doing })

可以通过 cmpRef 参数进行数据传递,比如:

cmpRef.instance.someProperty = someData;

五、总结

文章中只是做个抛砖引玉,实际项目应该做更深一层的抽象,Angular2是以组件为出发点,不能有太多了依赖。但是,文章至少已经解决最核心的问题了。文章并没有给出完整代码,如您感兴趣见Plunker

DynamicComponentLoader 从变更纪录来看非常频繁,在前两个BETA版本中都对其做大量的变动;但不管怎么变,是越来越精简。

Angular2 路由

Angular组件路由使一个视图至另一个视图的导航任务组件。

一、概要

浏览器是一个最熟悉的导航模型应用了,输入一个URL地址后浏览器导航相应URL页面,点击网页上的链接导航至另一个新的页面,浏览器的“后退”或“前近”可以导航至我们浏览器的历史页面。

Angular组件路由(也叫路由)就是借用这个模型,可以理解为通过浏览器的网址来生成客户端视图,并通过一些可选参数指定当前显示哪些视图。当然也可以把路由绑定至一个视图组件里,当点击链接时,导航至适合的视图。并且记录在浏览器历史列表,以便浏览器的前进或后退正常工作。

二、基础知识

先从组件路由基本概念开始,慢慢深入。

1、<base href>

大部分带有路由应用程序都会在 index.html<head> 加上 <base> 元素,用来设置基准URL(也就是应用程序的根路径)。

<base href="/">

2、导入 Router

Angular组件路由是个可选组件,所以并不包括在 core 里,需要我们单独引用 @angular/router 模块。

import { ROUTER_PROVIDERS } from '@angular/router';

3、配置

当浏览器URL改变时,路由会查找相应的 RouteDefinition 来确定组件的显示。

一般路由配置都在宿主组件内,(在Angular1.x经常会看到一些项目所有路由都在一个js文件当中)而且非常简单,通过 @Routers 装饰注册路由。

@Component({ ... })
@Routes([
  {path: '/crisis-center', component: CrisisListComponent},
  {path: '/heroes',        component: HeroListComponent},
  {path: '/hero/:id',      component: HeroDetailComponent}
])
export class AppComponent  implements OnInit {
  constructor(private router: Router) {}

  ngOnInit() {
    this.router.navigate(['/crisis-center']);
  }
}

有好几种 RouteDefinition 路由定义方式,大部分都是用URL地址来映射组件。

其中第三条路由 :id 是一个路由参数,比如URL地址 /hero/42 中 “42” 值就是 id 参数值。

4、Router Outlet

现在我们已经知道如何配置路由,当浏览器请求URL地址 /heroes 时,路由会查找 RouteDefinition 匹配到 HeroListComponent,那么匹配到的组件放在哪呢?我们就需要在当前组件模板里加入 RouterOutlet

<router-outlet></router-outlet>

我把它叫成点位符更贴切一点。

5、路由链接

知道如何配置和显示,那接下来就是如何导航,正常我们是通过浏览器的地址栏输入网站,可大部分是用户去触发某个链接进行导航的。

可以在一个锚标签添加一个 RouterLink 指令,模板表达式绑定的是一个字符串数组类型的网址。第一眼一定会很奇怪为什么是数组?这是因为还可能需要URL可选参数,虽然还是很奇怪。

template: `
  <h1>Component Router</h1>
  <nav>
    <a [routerLink]="['/crisis-center']">Crisis Center</a>
    <a [routerLink]="['/heroes']">Heroes</a>
  </nav>
  <router-outlet></router-outlet>
`,

6、总结一下

先通过 @RouterConfig 配置组件 AppComponent 的路由,再利用 RouterOutlet 来指定命中路由在哪显示,最后使用 RouterLinks 来处理用户点击链接的导航。

现在 AppComoponent 已经变成了一个路由组件。

路由 意思
Router 类定义了当前活动URL地址环境,可以用它在组件类里进行导航管理。
@RouteConfig 路由装饰器,接收一个 RouteDefinitions 路由定义对象数组。
RouteDefinition 路由定义对象。
RouterOutlet 匹配到组件的显示位置。
RouterLink 用于模板的链接导航。
Link Parameters Array 链接参数数组,可以用在模板的 RouterLink 或 Router.navigate 方法。
Routing Component 配置路由的组件。

三、一个示例

官网用了蛮长的来说明这个示例是怎么回事,大概是这样的:有两个列表分别 “Crisis Center”(记A) 和 “Heroes”(记B),点击列表的项时会进入编辑状态。这在之前二者都是一样的,不同在于编辑页面。

对于A有保存和取消按钮,不管点哪个按钮最后都会回到A列表,而且如果你改动 Name 后点击浏览器后退时会有一个对话框告诉你是不是要放弃编辑;反之B编辑时就很简单只有一个返回按钮,因为你每一次变动都会自动保存。

一个很长的动画,有耐心的可以先看一看效果。

router-anim

那么好,接下来会按里程碑式一步步说说是怎么实现的。

四、里程碑一:开始Router入门

很简单,通过路由来控制两个视图显示。

router-1-anim

1、设置 <base href>

组件路由使用浏览器 history.pushState 来导航,这样的URL地址看起来就像跟直接从服务端URL一样,例如:localhost:3000/crisis-center

现代浏览器都支持HTML5 pushState,为了让 pushState 更好工作,还需要在 index.html 添加一个 <base href="/"> 元素,这样同样可以确保所有请求的资源前缀。

HTML5模式导航是组件路由默认风格,我们也可以用老式的哈希(#)风格,后面会谈到。

2、导入组件路由模块

前面就说过路由模块是独立模块,需要从 angular/router 模块中获取。

import { Routes, Router, ROUTER_DIRECTIVES } from '@angular/router';

3、引导程序中注册路由服务

需要在 main.ts 文件里添加启动时注册路由服务。

import { bootstrap }        from '@angular/platform-browser-dynamic';
import { ROUTER_PROVIDERS } from '@angular/router';

import { AppComponent }     from './app.component';
bootstrap(AppComponent, [
  ROUTER_PROVIDERS
]);

从路由模块中导入 ROUTER_PROVIDERS,它实现一个默认配置的项,并注入到 provider 中。

4、AppComponent组件

和往常一样 AppComponent 组件是我们根组件,模板里包括两个导航链接和一个Router Outlet,通过点击导航链接显示不同的组件。

shell-and-outlet

对应的模板:

template: `
  <h1>Component Router</h1>
  <nav>
    <a [routerLink]="['/crisis-center']">Crisis Center</a>
    <a [routerLink]="['/heroes']">Heroes</a>
  </nav>
  <router-outlet></router-outlet>
`,

RouterOutlet

组件模板中 <router-outlet> 标签就是点位符,告诉命中路由的组件放在哪。

RouterLink属性绑定

RouterLink 属性指令,模板表达式的右边是一个数组对象来表示路由器导航所需要的数据:

  • 下标0是固定的URL部分。
  • 下标1以后放置的是可选参数数据。
<a [routerLink]="['/heroes']">Heroes</a>

除此之外,我们还可以在组件类里面使用 Route 导航管理器进行跳转。

this.router.navigate(['/heroes']);

总结

对于组件而言是一个带有层级结构的,那么当一个组件被放置时就已经被确立其URL地址结构了;所以组件自身理应包括路由的定义。如何更好在代码里面编写路由注册代码,应该在构建项目时就要加以考虑。

路由这一课题Angular2.x一直在变动,但整体的使用上并没有太多不同。

Angular2 结构指令

一个单页应用程序(SPA)最大特征就是对DOM树的操作,是根据程序状态对整段DOM的显示和消失。

一、什么是结构指令

Angular有三种指令类型:

  1. 组件
  2. 属性指令
  3. 结构指令

组件实际是一个带有模板的指令,这是最常见的三种指令,我们的应用程序也是基于指令来构建的。

属性指令改变的是元素的外观或行为,例如内置 NgStyle 指令,可以根据绑定组件的属性值同时改变多个元素的样式。

而结构指令是对DOM的添加或移除,这会改变DOM的布局。在模板语法章节就介绍过 ngIfngSwitchngFor

<div *ngIf="hero">{{hero}}</div>
<div *ngFor="let hero of heroes">{{hero}}</div>
<div [ngSwitch]="status">
  <template [ngSwitchWhen]="'in-mission'">In Mission</template>
  <template [ngSwitchWhen]="'ready'">Ready</template>
  <template ngSwitchDefault>Unknown</template>
</div>

二、NgIf 案例研究

ngIf 指令通过一个布尔类型变量来改变DOM的显示或消失。

<p *ngIf="condition">
  condition is true and ngIf is true.
</p>
<p *ngIf="!condition">
  condition is false and ngIf is false.
</p>

ngIf 指令并不是隐藏元素,使用浏览器开发工具我们可以看到,当条件为 true 时显示第一段DOM,反之显示第二段。

为什么要移除而不是隐藏?

我们可以通过设置CSS display 样式为 none 来隐藏元素,元素保留在DOM中只是看不见而已。相反,就用 ngIf

当我们隐藏一个元素,组件的行为还是继续着,像事件监听、变化检测、子组件的事件监听等依然还会在执手,哪怕我们压根看不见,很明显隐藏这一点来说最大的可能是性能问题;当然他也有好处,那就是当元素想要再一次被显示时,会非常快。

ngIf 就不同,设置为 false 时,会释放组件及其子组件的所有资源,并从DOM里移除元素;当然坏处就是,当你很快又需要该元素时,就要重新构建了。

虽然两种方法都有利弊,如果我们确认不使用的元素在短时间内不会再用那么 ngIf 最合理,反之。

接下来,我们用一个示例来说明二者的区别,通过设置css样式和 ngIf 对一个组件 heavy-loader (假设组件非常复杂)的显示或隐藏。

<div><!-- Visibility -->
  <button (click)="isVisible = !isVisible">show | hide</button>
  <heavy-loader [style.display]="isVisible ? 'inline' : 'none'" [logs]="logs"></heavy-loader>
</div>
<div><!-- NgIf -->
  <button (click)="condition = !condition">if | !if</button>
  <heavy-loader *ngIf="condition" [logs]="logs"></heavy-loader>
</div>
<h4>heavy-loader log:</h4>
<div *ngFor="let message of logs">{{message}}</div>

同时还记录组件的创建和销毁日志,这是利用内置 ngOnInitngOnDestroy 生命周期钩子来实现,效果如下:

heavy-loader-toggle

一开始在DOM都显示这两个组件,首先我们反复切换可见性,利用CSS样式由于每一次的可见性都是同一个实例,所以日志没有任何改变。

相反,当我们反复切换第二 ngIf 的可见性时,每一次都会出现创建新实例和销毁日志。

三、<template> 标签

结构指令像 ngIf,使用HTML template标签。对于非Angular应用而言 <template> 标签 display 样式值为 none,内容是不可见的文档片断;而Angular应用中,会移除所有 <template> 标签,并存储起来。

比如以下代码片断,中间的 Hip! 是用一个 <template> 包裹着。

<p>
  Hip!
</p>
<template>
  <p>
    Hip!
  </p>
</template>
<p>
  Hooray!
</p>

左边是非Angular应用,右边是Angular应用,二者的DOM区别。

template-in-out-of-a2

显然,默认情况下Angular会把 <template> 标签替换成一个空内容的 <script> 标签(虽然我本地的测试里并不会有这个空的script标签),不过呢,他可以做更多的事,比如 ngSwitch 指令在 <template> 标签中的应用:

<div [ngSwitch]="status">
  <template [ngSwitchWhen]="'in-mission'">In Mission</template>
  <template [ngSwitchWhen]="'ready'">Ready</template>
  <template ngSwitchDefault>Unknown</template>
</div>

当其中一个 ngSwitch 条件为 true 时,Angular会把标签里模板内容插入DOM当中。

四、关于指令中的 * 号

看看下面代码有什么特别的?

<div *ngIf="hero">{{hero}}</div>
<div *ngFor="let hero of heroes">{{hero}}</div>

上面指令名称有前缀星号(*),星号是一个语法糖,简化了 ngIfngFor 二者的读写,在模板引擎里最后还是会转换成 <template> 模式。

以下是 ngIf 语法糖与 <template> 的写法:

<!-- Examples (A) and (B) are the same -->
<!-- (A) *ngIf paragraph -->
<p *ngIf="condition">
  Our heroes are true!
</p>

<!-- (B) [ngIf] with template -->
<template [ngIf]="condition">
  <p>
    Our heroes are true!
  </p>
</template>

只要不傻肯定用第一种写法。值得注意的是从A扩展到B,先将段落及其内容移到 <template> 标签里面,属性绑定部分作为 <template> 标签的指令。

接下来看看 *ngFor 是如何转换:

<!-- Examples (A) and (B) are the same -->

<!-- (A) *ngFor div -->
<div *ngFor="let hero of heroes">{{ hero }}</div>

<!-- (B) ngFor with template -->
<template ngFor let-hero [ngForOf]="heroes">
  <div>{{ hero }}</div>
</template>

基本上和 ngIf 如出一辙;额外的细微是多了 ngForOf 属性绑定和 hero 本地模板变量。

五、创建结构指令

让我们创建一个 Unless 指令,类似 ngIf,只不过条件是反着的,当为 false 时才会显示内容。创建指令类似于创建组件:

  • 导入 Directive 装饰器。
  • 添加指令的CSS属性选择器标识符。
  • 指定一个公共的 Input 属性属性用于绑定(通常是指令的名字)。
  • 指令类实现。

先从以下代码开始:

import { Directive, Input } from '@angular/core';

@Directive({ selector: '[myUnless]' })
export class UnlessDirective {
}

指令的选择器用的 [myUnless],了解CSS选择器的就很清楚,用中括号来表示某个HTML元素属性。同时命名也尽可能避免 ng 开头,这一点说过很多次,无非就是害怕冲突。

现在还需要分别定义一个 TemplateRef 用来访问模板内容,以及 ViewContainerRef 用来访问视图容器。

constructor(
  private templateRef: TemplateRef<any>,
  private viewContainer: ViewContainerRef
  ) { }

重点来了,给指令定义一个输入属性 myUnless (名称和 selector 一样),并且接收一个布尔类型,用于条件判断添加或移除模板内容。

@Input() set myUnless(condition: boolean) {
  if (!condition) {
    this.viewContainer.createEmbeddedView(this.templateRef);
  } else {
    this.viewContainer.clear();
  }
}

整体的逻辑非常简单,指令所有代码如下:

import { Directive, Input } from '@angular/core';
import { TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({ selector: '[myUnless]' })
export class UnlessDirective {
  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
    ) { }
  @Input() set myUnless(condition: boolean) {
    if (!condition) {
      this.viewContainer.createEmbeddedView(this.templateRef);
    } else {
      this.viewContainer.clear();
    }
  }
}

现在来试试怎么运用这个指令,同 *ngIf 一样,只不过改成 *myUnless

<p *myUnless="condition">
  condition is false and myUnless is true.
</p>

<p *myUnless="!condition">
  condition is true and myUnless is false.
</p>

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解决一个路径深度的问题。

© 2017 卡片机色彩

Theme by cipchk

to top