[关闭]
@carlchen 2017-04-27T05:14:58.000000Z 字数 18416 阅读 675

前端Angular2教程

第八章:查缺补漏大合集(上)

这一章其实是我在前七章读者评论和私信交流时发现很多点我是要么漏掉了,要么自己理解有误。那这第八和第九章就来做一个小总结吧。本章我们讨论如何在Angular2中引入第三方JS库、惰性加载路由和子路由。

第三方JS类库的引入

这个是许多人的困惑,我们在Angular2中使用了TypeScript,但大量的类库是没有TypeScript怎么办?其实不用担心,非常简单。但在讲方法前,我们最好还是理解一下背景。

为什么JS不能直接使用

由于TypeScript是一个强类型语言,所以对于第三方类库,我们需要知道它们的JavaScript里面的暴露给外部使用的这些对象和方法的类型定义是什么。

这个类型定义文件长什么样呢?我们来看一看,你可以进入工程下的node_modules中的 @angular/common/src/directives/ng_class.d.ts:

  1. import { DoCheck, ElementRef, IterableDiffers, KeyValueDiffers, Renderer } from '@angular/core';
  2. /**
  3. * @ngModule CommonModule
  4. *
  5. * @whatItDoes Adds and removes CSS classes on an HTML element.
  6. *
  7. * @description
  8. *
  9. * The CSS classes are updated as follows, depending on the type of the expression evaluation:
  10. * - `string` - the CSS classes listed in the string (space delimited) are added,
  11. * - `Array` - the CSS classes declared as Array elements are added,
  12. * - `Object` - keys are CSS classes that get added when the expression given in the value
  13. * evaluates to a truthy value, otherwise they are removed.
  14. *
  15. * @stable
  16. */
  17. export declare class NgClass implements DoCheck {
  18. private _iterableDiffers;
  19. private _keyValueDiffers;
  20. private _ngEl;
  21. private _renderer;
  22. private _iterableDiffer;
  23. private _keyValueDiffer;
  24. private _initialClasses;
  25. private _rawClass;
  26. constructor(_iterableDiffers: IterableDiffers, _keyValueDiffers: KeyValueDiffers, _ngEl: ElementRef, _renderer: Renderer);
  27. klass: string;
  28. ngClass: string | string[] | Set<string> | {
  29. [klass: string]: any;
  30. };
  31. ngDoCheck(): void;
  32. private _cleanupClasses(rawClassVal);
  33. private _applyKeyValueChanges(changes);
  34. private _applyIterableChanges(changes);
  35. private _applyInitialClasses(isCleanup);
  36. private _applyClasses(rawClassVal, isCleanup);
  37. private _toggleClass(klass, enabled);
  38. }

可以看到这个文件其实就是用来做类型定义声明的,我们一般把这种以 .d.ts 后缀结尾的文件叫做类型定义文件(Type Definition)。有了这个声明定义,我们就可以在TypeScript中使用了。这个文件看起来也挺麻烦的,事实上真正需要你自己动手写的类库很少。我们来看一下一般的集成第三方类库的过程是什么样子的。

标准的JS库引入方法

我们拿百度的echarts (https://github.com/ecomfe/echarts)图表类库的集成来说明一下。我们先安装其npm包,在命令行窗口输入 npm install --save echarts ,然后我们安装其类型定义文件,在命令行窗口输入 npm install --save-dev @types/echarts 。然后。。就没有然后了。这么简单吗?是滴。

注意两件事,首先我们安装时使用了 --save-dev 开关,因为这个类型定义文件只对开发时有用,它并不是我们工程的依赖,只是为了编写时的方便。
第二件事我们使用了 @types/echarts 这样一个有点怪的名称,其实是这样的,微软维护了一个海量的类型定义数据中心,这个就是 @types。那么我们为了寻找echarts就会在 @types 这个目录下搜索它的二级目录。

这样安装之后,你可以在本地工程目录下的 node_modules/@types/echarts/index.d.ts 找到echarts的定义:

  1. // Type definitions for echarts
  2. // Project: http://echarts.baidu.com/
  3. // Definitions by: Xie Jingyang <https://github.com/xieisabug>
  4. // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
  5. declare namespace ECharts {
  6. function init(dom:HTMLDivElement|HTMLCanvasElement, theme?:Object|string, opts?:{
  7. devicePixelRatio?: number
  8. renderer?: string
  9. }):ECharts;
  10. //此处省略大部分声明,可以查阅本地文件
  11. }
  12. declare module "echarts" {
  13. export = ECharts;
  14. }

一般情况下,到这步就结束了,此时我们可以试验一下是否可以使用了,在一个组件文件中尝试引入echarts,如果你看到了智能提示中有你希望引入的类库中的方法或对象,那就一切顺利,接下来就可以正常使用这个类库了。

引入echarts看到智能提示

引入库的特殊情况

但有的时候,我们执行第二步 npm install --save-dev @types/echarts 时,会发现没有找到对应的类型定义文件。这个时候怎么办呢?
这时候要分两种情况看,首先应该去检查一下node_modules目录中的你要使用的类库子目录(本例中是echarts)中是否有类型定义文件,因为有的类库会把类型定义文件直接打包在npm的包中。比如我们前几章接触的angular-uuid,这个类库其实就是直接把类型定义文件打包在npm package中的。看下图,如果是这种情况,那么我们什么都不需要做,直接使用就好了。

有的类库直接将类型定义打包在npm中

当然还有一种情形就是,这样也找不到,或者这个类库是我们的团队已有的、自己写的等等情况。这时候就得自己写一下,也很简单,在 src/typings.d.ts 中加上一行:

  1. declare module 'echarts';

然后在要使用此类库的组件中引入:

  1. import * as echarts from 'echarts';

后面就可以正常使用了,当然这种添加方式是没有智能提示和自动完成的,你需要自己保证调用的正确性。如果觉得不爽,还是希望有提示、类型检查等等,那就得自己写一个类型定义文件了,可以参考 https://basarat.gitbooks.io/typescript/content/docs/types/ambient/d.ts.html 去编写自己的类型定义文件。

惰性路由和子路由

惰性路由

在需求和功能不断添加和修改之后,应用的尺寸将会变得更大。在某一个时间点,我们将达到一个顶点,应用 将会需要过多的时间来加载。这会带来一定的性能问题。
如何才能解决这个问题呢?Angular2引进了异步路由,我们可以惰性加载指定的模块或组件。这样给我们带来了下列好处:

还是我们一起打造一个例子说明一下,之后大家就可以清楚的理解这个概念了。我们新建一个叫Playground的module。打开一个命令行窗口,输入 ng g m playgorund ,这样Angular CLI非常聪明的帮我们建立了PlaygroundModule,不光如此,它还帮我们建立了一个PlaygroundComponent。因为一般来说,我们新建一个模块肯定会至少有一个组件的。
由于要做惰性加载,我们并不需要在根模块AppModule中引入这个模块,所以我们检查一下根模块 src/app/app.module.ts 中是否引入了PlaygroundModule,如果有,请去掉。
首先为PlaygroundModule建立自己模块的路由,我们如果遵守Google的代码风格建议的话,那么就应该为每个模块建立独立的路由文件。

  1. const routes: Routes = [
  2. { path: '', component: PlaygroundComponent },
  3. ];
  4. @NgModule({
  5. imports: [ RouterModule.forChild(routes) ],
  6. exports: [ RouterModule ],
  7. })
  8. export class PlaygroundRoutingModule { }

在src/app/app-routing.module.ts中我们要添加一个惰性路由指向PlaygroundModule

  1. import { NgModule } from '@angular/core';
  2. import { Routes, RouterModule } from '@angular/router';
  3. import { LoginComponent } from './login/login.component';
  4. import { AuthGuardService } from './core/auth-guard.service';
  5. const routes: Routes = [
  6. {
  7. path: '',
  8. redirectTo: 'login',
  9. pathMatch: 'full'
  10. },
  11. {
  12. path: 'playground',
  13. loadChildren: 'app/playground/playground.module#PlaygroundModule',
  14. }
  15. ];
  16. @NgModule({
  17. imports: [
  18. RouterModule.forRoot(routes)
  19. ],
  20. exports: [
  21. RouterModule
  22. ]
  23. })
  24. export class AppRoutingModule {}

在这段代码中我们看到一个新面孔,loadChildren 。路由器用 loadChildren 属性来映射我们希望惰性加载的模块文件,这里是 PlaygroundModule 。路由器将接收我们的 loadChildren 字符串,并把它动态加载进 PlaygroundModule ,它的路由被动态合并到我们的配置中,然后加载所请求的路由。但只有在首次加载该路由时才会这样做,后续的请求都会立即完成。
app/playground/playground.module#PlaygroundModule 这个表达式是这样的规则:模块的路径#模块名称
现在我们回顾一下,在应用启动时,我们并没有加载PlaygroundModule,因为在AppModule中没有它的引用。但是当你在浏览器中手动输入 http://localhost:4200/playground 时,系统在此时加载 PlaygroundModule

子路由

程序复杂了之后,一层的路由可能就不会够用了,在一个模块内部由于功能较复杂,需要再划分出二级甚至更多级别的路径。这种情况下我们就需要Angular2提供的一个内建功能叫做:子路由。
我们向来认为例子是最好的说明,所以还是来做一个小功能:现在我们需要对一个叫playground的路径下添加子路由,子路由有2个:one和two。其中one下面还有一层路径叫three。形象的表示一下,就像下面的结构一样。

  1. /playground---|
  2. |/one
  3. |--------|three
  4. |/two

那么我们还是先在项目工程目录输入 ng g c playground/one,然后再执行 ng g c playground/two ,还有一个three,所以再来:ng g c playground/three
现在我们有了三个组件,看看怎么处理路由吧,原有的模块路由文件如下:

  1. import { NgModule } from '@angular/core';
  2. import { Routes, RouterModule } from '@angular/router';
  3. import { PlaygroundComponent } from './playground.component';
  4. const routes: Routes = [
  5. {
  6. path: '',
  7. component: PlaygroundComponent
  8. },
  9. ];
  10. @NgModule({
  11. imports: [ RouterModule.forChild(routes) ],
  12. exports: [ RouterModule ],
  13. })
  14. export class PlaygroundRoutingModule { }

我们首先需要在模块的根路由下添加one和two,Angular2在路由定义数组中对于每个路由定义对象都有一个属性叫做children,这里就是指定子路由的地方了。所以在下面代码中我们把one和two都放入了children数组中。

  1. import { NgModule } from '@angular/core';
  2. import { Routes, RouterModule } from '@angular/router';
  3. import { PlaygroundComponent } from './playground.component';
  4. import { OneComponent } from './one/one.component';
  5. import { TwoComponent } from './two/two.component';
  6. const routes: Routes = [
  7. {
  8. path: '',
  9. component: PlaygroundComponent,
  10. children: [
  11. {
  12. path: 'one',
  13. component: OneComponent,
  14. },
  15. {
  16. path: 'two',
  17. component: TwoComponent
  18. }
  19. ]
  20. },
  21. ];
  22. @NgModule({
  23. imports: [ RouterModule.forChild(routes) ],
  24. exports: [ RouterModule ],
  25. })
  26. export class PlaygroundRoutingModule { }

这只是定义了路由数据,我们还需要在某个地方显示路由指向的组件,那么这里面我们还是在PlaygroundComponent的模版中把路由插座放入吧。

  1. <ul>
  2. <li><a routerLink="one">One</a></li>
  3. <li><a routerLink="two">Two</a></li>
  4. </ul>
  5. <router-outlet></router-outlet>

现在我们试验一下,打开浏览器输入 http://localhost:4200/playground 我们看到两个链接,你可以分别点一下,观察地址栏。应该可以看到,点击one时,地址变成 http://localhost:4200/playground/one 在我们放置路由插座的位置也会出现one works。当然点击two时也会有对应的改变。这说明我们的子路由配置好用了!

子路由的小例子

当然有的时候还需要更深的层级的子路由,其实也很简单。就是重复我们刚才做的就好,只不过要在对应的子路由节点上。下面我们还是演练一下,在点击one之后我们希望到达一个有子路由的页面(也就是子路由的子路由)。于是我们在OneComponent节点下又加了children,然后把ThreeComponent和对应的路径写入

  1. import { NgModule } from '@angular/core';
  2. import { Routes, RouterModule } from '@angular/router';
  3. import { PlaygroundComponent } from './playground.component';
  4. import { OneComponent } from './one/one.component';
  5. import { TwoComponent } from './two/two.component';
  6. import { ThreeComponent } from './three/three.component';
  7. const routes: Routes = [
  8. {
  9. path: '',
  10. component: PlaygroundComponent,
  11. children: [
  12. {
  13. path: 'one',
  14. component: OneComponent,
  15. children: [
  16. {
  17. path: 'three',
  18. component: ThreeComponent
  19. }
  20. ]
  21. },
  22. {
  23. path: 'two',
  24. component: TwoComponent
  25. }
  26. ]
  27. },
  28. ];
  29. @NgModule({
  30. imports: [ RouterModule.forChild(routes) ],
  31. exports: [ RouterModule ],
  32. })
  33. export class PlaygroundRoutingModule { }

当然,还是一样,我们需要改造一下OneComponent的模版以便于它可以显示子路由的内容。改动 src/app/playground/one/one.component.html 为如下内容

  1. <p>
  2. one works!
  3. </p>
  4. <ul>
  5. <li><a routerLink="three">Three</a></li>
  6. </ul>
  7. <router-outlet></router-outlet>

这回我们看到如果在浏览器中输入 http://localhost:4200/playground/one/three 会看到如图所示的结果:

更多层级的子路由

经过这个小练习,相信再复杂的路由你也可以搞定了。但是我要说一句,个人不是很推荐过于复杂的路由(复杂这里指层级嵌套太多)。层级多了之后意味着这个模块太大了,负责了过多它不应该负责的事情。也就是说当要使用子路由时,一定多问自己几遍,这样做是必须的吗?可以用别的方式解决吗?是不是我的模块改拆分了?

第九章:查缺补漏大合集(下)

Angular2 动画再体验

State和Transition

我写文章的习惯是先试验再理论,所以我们接下来梳理下Angular2提供的动画技能。还是从最简单的例子开始,一个非常简单的模版:

  1. <div class="traffic-light"></div>

同样非常简单的样式(其实就是画一个小黑块):

  1. .traffic-light{
  2. width: 100px;
  3. height: 100px;
  4. background-color: black;
  5. }

现在的效果就是这个样子,如图所示,一点都不酷啊,没关系,我们一点点来,越简单的越容易弄懂概念。

一点也不酷的小黑块

下面我们为组件添加一个animations的元数据描述:

  1. import {
  2. Component,
  3. trigger,
  4. state,
  5. style
  6. } from '@angular/core';
  7. @Component({
  8. selector: 'app-playground',
  9. templateUrl: './playground.component.html',
  10. styleUrls: ['./playground.component.css'],
  11. animations: [
  12. trigger('signal', [
  13. state('go', style({
  14. 'background-color': 'green'
  15. }))
  16. ])
  17. ]
  18. })
  19. export class PlaygroundComponent {
  20. constructor() { }
  21. }

我们注意到animations中接受的是一个数组,这个数组里面我们使用了一个叫trigger的函数,trigger接受的第一个参数是触发器的名字,第二个参数是一个数组。这个数组是由一种叫state的函数和叫transition的函数组成的。

那么什么是state?state表示一种状态,当这种状态激活时,state所附带的样式就会附着在应用trigger的那个控件上。transition又是什么呢?tranistion描述了一系列动画的步骤,在状态迁移时这些动画步骤就会执行。
我们现在的这个版本中暂时只有state而没有transition,让我们先来看看效果,当然在可以看到效果前我们先要把这个trigger应用到某个控件上。那在我们的例子里就是模版中的那个div了。

  1. <div
  2. [@signal]="'go'"
  3. class="traffic-light">
  4. </div>

返回浏览器,你会发现那个小黑块变成小绿块了,如图所示

小黑块变成小绿块

这说明什么?我们的state的样式附着在div上了。为什么呢?因为 [@signal]="'go'" 定义了trigger的状态是go。但这一点也不酷是吗?是的,暂时是这样,还是那句话,不要急。
接下来,我们再加一个状态 stop,在stop激活时我们要把小方块的背景色设为红色,那么我们需要把animations改成下面的样子:

  1. animations: [
  2. trigger('signal', [
  3. state('go', style({
  4. 'background-color': 'green'
  5. })),
  6. state('stop', style({
  7. 'background-color':'red'
  8. }))
  9. ])
  10. ]

同时我们需要给模板加两个按钮Go和Stop。现在的模版看起来是下面的样子

  1. <div
  2. [@signal]="signal"
  3. class="traffic-light">
  4. </div>
  5. <button (click)="onGo()">Go</button>
  6. <button (click)="onStop()">Stop</button>

当然你看得到,我们点击按钮时需要处理对应的点击事件。在这里我们希望点击Go时,方块变绿,点击Stop时方块变红。如果要达成这个目的,我们需要一个叫signal的成员变量,在点击的处理函数中更改相应的状态。

  1. export class PlaygroundComponent {
  2. signal: string;
  3. constructor() { }
  4. onGo(){
  5. this.signal = 'go';
  6. }
  7. onStop(){
  8. this.signal = 'stop';
  9. }
  10. }

现在打开浏览器,试验一下,我们会发现点击Go变绿,而点击Stop变红。但是还是没动起来啊,是的,这是因为我们还没加transition呢,我们只需把animations改写一下,你分别点Go和Stop就能看到动画效果了。为了让效果更明显一些,我们为两种状态指定一下高度。

  1. import {
  2. Component,
  3. OnDestroy,
  4. trigger,
  5. state,
  6. style,
  7. transition,
  8. animate
  9. } from '@angular/core';
  10. @Component({
  11. selector: 'app-playground',
  12. templateUrl: './playground.component.html',
  13. styleUrls: ['./playground.component.css'],
  14. animations: [
  15. trigger('signal', [
  16. state('void', style({
  17. 'transform':'translateY(-100%)'
  18. })),
  19. state('go', style({
  20. 'background-color': 'green',
  21. 'height':'100px'
  22. })),
  23. state('stop', style({
  24. 'background-color':'red',
  25. 'height':'50px'
  26. })),
  27. transition('void => *', animate(5000))
  28. ])
  29. ]
  30. })
  31. export class PlaygroundComponent {
  32. signal: string;
  33. constructor() { }
  34. onGo(){
  35. this.signal = 'go';
  36. }
  37. onStop(){
  38. this.signal = 'stop';
  39. }
  40. }

那么 transition('* => *', animate(500)) 这句什么意思呢?前面那个 '* => *' 是一个状态迁移表达式,* 表示任意状态,所以这个表达式告诉我们,只要有状态的变化就会激发后面的动画效果。后面的就是告诉Angular做500毫秒的动画,这个动画默认是从一个状态过渡到另一个状态。现在大家打开浏览器体验一下,分别点击Go和Stop,会发现我们的小方块从一个正方形变成一个长方形,红色变成绿色的过程。体验完之后再来看这句话:动画其实就是由若干个状态组成,由transition定义状态过渡的步骤。

有了形状和颜色变化的动画

那么下面我们介绍一个void 状态(空状态),为什么会有void状态呢?其实刚刚我们也体验了,只不过没有定义这个void 状态而已。我们在组件中并没有给signal赋初始值,这就意味着一开始trigger的状态就是void。我们往往在实现进场或离场动画时需要这个void状态。void状态就是描述没有状态值时的状态。

  1. animations: [
  2. trigger('signal', [
  3. state('void', style({
  4. 'transform':'translateY(-100%)'
  5. })),
  6. state('go', style({
  7. 'background-color': 'green',
  8. 'height':'100px'
  9. })),
  10. state('stop', style({
  11. 'background-color':'red',
  12. 'height':'50px'
  13. })),
  14. transition('* => *', animate(500))
  15. ])
  16. ]

上面代码定义了一个void状态,而且样式上有一个按Y轴做的-100%的位移,其实这就是一开始让小方块从场景外进入场景内,这样就是实现了一种进场动画,大家可以在浏览器中试验一下。

用void状态实现的进场动画

奇妙的animate函数

上面的我们的实验中,你会发现transition中有个animate函数,可能你认为它就是指定一个动画的时间的函数。它的身手可不止那么简单呢,我们来仔细挖掘一下。
首先呢,我们来对上面的代码做一个小改造,把animations数组改成下面的样子:

  1. animations: [
  2. trigger('signal', [
  3. state('void', style({
  4. 'transform':'translateY(-100%)'
  5. })),
  6. state('go', style({
  7. 'background-color': 'green',
  8. 'height':'100px'
  9. })),
  10. state('stop', style({
  11. 'background-color':'red',
  12. 'height':'50px'
  13. })),
  14. transition('* => *', animate('.5s 1s'))
  15. ])
  16. ]

我们其实只对animate中的参数做了一点小改动,就是把animate(500) 改成animate('.5s 1s')。那么.5s表示动画过渡时间为0.5秒(其实和上面设置的500毫秒是一样的),1s表示动画延迟1秒后播放。现在我们打开浏览器,看看效果如何吧。

当然还有更狠的大招,这个字符串表达式还可以变成 '.5s 1s ease-out',后面的这个ease-out是一种缓动函数,它是可以让动画效果更真实的一种方式。
现实世界中物体照着一定节奏移动,并不是一开始就移动很快的,也不可能是一直匀速运动的。怎么理解呢?当皮球往下掉时,首先是越掉越快,撞到地上后回弹,最终才又碰触地板。而缓动函数可以使动画的过渡效果按照这样的真实场景抽象出的对应函数来进行绘制。ease-out只是众多的缓动函数的其中一种,我们当然可以指定其他函数。
另外需要说明的一点是诸如ease-out只是真实函数的一个友好名称,我们当然可以直接指定背后的函数:cubic-bezier(0, 0, 0.58, 1) 。我们下个小例子不用这个ease-out,因为效果可能不是特别明显,我们找一个明显的,使用 cubic-bezier(0.175, 0.885, 0.32, 1.275) 。现在我们打开浏览器,你仔细观察一下是否看到了小方块回弹的效果

  1. animations: [
  2. trigger('signal', [
  3. state('void', style({
  4. 'transform':'translateY(-100%)'
  5. })),
  6. state('go', style({
  7. 'background-color': 'green',
  8. 'height':'100px'
  9. })),
  10. state('stop', style({
  11. 'background-color':'red',
  12. 'height':'50px'
  13. })),
  14. transition('* => *', animate('.5s 1s cubic-bezier(0.175, 0.885, 0.32, 1.275)'))
  15. ])
  16. ]

加上了缓动函数的进场动画

关于缓动函数的更多资料可以访问 http://easings.net/zh-cn 在这里可以看到各种函数的曲线和效果,以及cubic-bezier函数的各种参数

easing.net上列出了各种缓动函数的曲线和效果

需要注意的一点是Angular2实现动画的机制其实是基于W3C的Web Animation标准,这个标准暂时无法支持所有的cubic-bezier函数,只有部分函数被支持。这样的话我们如果要实现某些不被支持的函数怎么办呢?那就得有请我们的关键帧出场了。

关键帧

何谓关键帧?首先需要知道什么是帧?百度百科给了定义:
帧——就是动画中最小单位的单幅影像画面,相当于电影胶片上的每一格镜头。在动画软件的时间轴上帧表现为一格或一个标记。
关键帧——相当于二维动画中的原画。指角色或者物体运动或变化中的关键动作所处的那一帧。关键帧与关键帧之间的动画可以由软件来创建,叫做过渡帧或者中间帧。
先来做一个小实验,我们把入场动画改造成关键帧形式。

  1. import {
  2. Component,
  3. OnDestroy,
  4. trigger,
  5. state,
  6. style,
  7. transition,
  8. animate,
  9. keyframes
  10. } from '@angular/core';
  11. @Component({
  12. selector: 'app-playground',
  13. templateUrl: './playground.component.html',
  14. styleUrls: ['./playground.component.css'],
  15. animations: [
  16. trigger('signal', [
  17. state('void', style({
  18. 'transform':'translateY(-100%)'
  19. })),
  20. state('go', style({
  21. 'background-color': 'green',
  22. 'height':'100px'
  23. })),
  24. state('stop', style({
  25. 'background-color':'red',
  26. 'height':'50px'
  27. })),
  28. transition('void => *', animate(5000, keyframes([
  29. style({'transform': 'scale(0)'}),
  30. style({'transform': 'scale(0.1)'}),
  31. style({'transform': 'scale(0.5)'}),
  32. style({'transform': 'scale(0.9)'}),
  33. style({'transform': 'scale(0.95)'}),
  34. style({'transform': 'scale(1)'})
  35. ]))),
  36. transition('* => *', animate('.5s 1s cubic-bezier(0.175, 0.885, 0.32, 1.275)'))
  37. ])
  38. ]
  39. })
  40. export class PlaygroundComponent {
  41. // clock = Observable.interval(1000).do(_=>console.log('observable created'));
  42. signal: string;
  43. constructor() { }
  44. onGo(){
  45. this.signal = 'go';
  46. }
  47. onStop(){
  48. this.signal = 'stop';
  49. }
  50. }

保存后返回浏览器,你应该可以看到一个正方形由小变大的进场动画。

关键帧实现的入场动画

现在我们来分析一下代码,这个入场动画是5秒的时间,我们给出6个关键帧,也就是0s,1s,2s,3s,4s和5s这几个。对于每个关键帧,我们给出的样式都是放缩,而放缩的比例逐渐加大,而且是先快后慢,也就是说我们可以模拟出缓动函数的效果。

如果我们不光做放缩,而且在style中还指定位置的话,这个动画就会出现边移动边变大的效果了。把入场动画改成下面的样子试试看吧。

  1. transition('void => *', animate(5000, keyframes([
  2. style({'transform': 'scale(0)', 'padding': '0px'}),
  3. style({'transform': 'scale(0.1)', 'padding': '50px'}),
  4. style({'transform': 'scale(0.5)', 'padding': '100px'}),
  5. style({'transform': 'scale(0.9)', 'padding': '120px'}),
  6. style({'transform': 'scale(0.95)', 'padding': '135px'}),
  7. style({'transform': 'scale(1)', 'padding': '140px'})
  8. ]))),

加上位移的效果

最后的结果可能还是不酷,但是这样的话利用关键帧我们如果结合好CSS样式,就会做出比较复杂的动画了。

方便的管道--PIPE

我们一直没有提到的一点就是管道,虽然我们的例子中没有用到,但其实这是Angular 2中提供非常方便的一个特性。这个特性可以让我们很快的将数据在界面上以我们想要的格式输出出来。还是拿例子说话,比如我们在页面上显示一个日期,先建立一个简单的模版:

  1. <p> Without Pipe: Today is {{ birthday }} </p>
  2. <p> With Pipe: Today is {{ birthday | date:"MM/dd/yy" }} </p>

再来建立对应的组件文件:

  1. import { Component, OnDestroy } from '@angular/core';
  2. @Component({
  3. selector: 'app-playground',
  4. templateUrl: './playground.component.html',
  5. styleUrls: ['./playground.component.css']
  6. })
  7. export class PlaygroundComponent {
  8. birthday = new Date();
  9. constructor() { }
  10. }

无管道和有管道的日期输出

上面的例子可能还没太明显,我们 进一步改造一下模板:

  1. <p> Without Pipe: Today is {{ birthday }} </p>
  2. <p> With Pipe: Today is {{ birthday | date:"MM/dd/yy" }} </p>
  3. <p>The time is {{ birthday | date:'shortTime' }}</p>
  4. <p>The time is {{ birthday | date:'medium' }}</p>

同一数据可以显示成不同样子

而且更牛的是多个Pipes可以串起来使用,比如说上图中最下面那个日期我们希望把Dec大写,就可以这样使用:

  1. <p>The time is {{ birthday | date:'medium' | uppercase }}</p>

多个Pipe连用

自定义一个Pipe

那么自己写一个Pipe是怎样的体验呢?创建一个Pipe非常简单,我们来体会一下。首先创建一个 src/app/playground/trim-space.pipe.ts 的文件:

  1. import { Pipe, PipeTransform } from '@angular/core';
  2. @Pipe({
  3. name: 'trimSpace'
  4. })
  5. export class TrimSpacePipe implements PipeTransform {
  6. transform(value: any, args: any[]): any {
  7. return value.replace(/ /g, '');
  8. }
  9. }

在Module文件中声明这个Pipe:declarations: [PlaygroundComponent, TrimSpacePipe] 以便于其他控件可以使用这个Pipe:

  1. import { NgModule } from '@angular/core';
  2. import { SharedModule } from '../shared/shared.module';
  3. import { PlaygroundRoutingModule } from './playground-routing.module';
  4. import { PlaygroundComponent } from './playground.component';
  5. import { PlaygroundService } from './playground.service';
  6. import { TrimSpacePipe } from './trim-space.pipe';
  7. @NgModule({
  8. imports: [
  9. SharedModule,
  10. PlaygroundRoutingModule
  11. ],
  12. providers:[
  13. PlaygroundService
  14. ],
  15. declarations: [PlaygroundComponent, TrimSpacePipe]
  16. })
  17. export class PlaygroundModule { }

然后在组件的模板文件中使用即可 {{ birthday | date:'medium' | trimSpace}}

  1. <p> Without Pipe: Today is {{ birthday }} </p>
  2. <p> With Pipe: Today is {{ birthday | date:"MM/dd/yy" }} </p>
  3. <p>The time is {{ birthday | date:'shortTime' }}</p>
  4. <p>The time is {{ birthday | date:'medium' | trimSpace}} with trim space pipe applied</p>
  5. <p>The time is {{ birthday | date:'medium' | uppercase }}</p>

打开浏览器看一下效果,我们看到应用了trimSpace管道的日期的空格被移除了,如图所示:

自定义一个移除空格的Pipe

内建的Pipe

Decimal Pipe

DatePipe和UpperCase Pipe我们刚刚已经见识过了,现在我们看一看内建的其他Pipe。首先是用于数字格式化的DecimalPipe。DecimalPipe的参数是以 {minIntegerDigits}.{minFractionDigits}-{maxFractionDigits} 的表达式形式体现的。其中:

  1. minIntegerDigits 是最小的整数位数,默认是1。
  2. minFractionDigits 表示最小的小数位数,默认是0。
  3. maxFractionDigits 表示最大的小数位数,默认是3。
  1. <p>pi (no formatting): {{pi}}</p>
  2. <p>pi (.5-5): {{pi | number:'.5-5'}}</p>
  3. <p>pi (2.10-10): {{pi | number:'2.10-10'}}</p>
  4. <p>pi (.3-3): {{pi | number:'.3-3'}}</p>

如果我们在组件中定义 pi: number = 3.1415927; 的话,上面的数字会被格式化成下图的样子

Decimal Pipe用于数字的格式化

Currency Pipe

顾名思义,这个Pipe是格式化货币的,这个Pipe的表达式形式是这样的: currency[:currencyCode[:symbolDisplay[:digitInfo]]],也就是说在currency管道后用分号分隔不同的属性设置:

  1. <p>A in USD: {{a | currency:'USD':true}}</p>
  2. <p>B in CNY: {{b | currency:'CNY':false:'4.2-2'}}</p>

上面的代码中 USDCNY 表面货币代码,truefalse 表明是否使用该货币的默认符号,后面如果再有一个表达式就是规定货币的位数限制。这个限制的具体规则和上面Decimal Pipe的类似,如下图所示。

Currecy Pipe用于格式化货币

Percent Pipe

这个管道当然就是用来格式化百分数的,百分数的整数位和小数位的规则也和上面提到的Decimal Pipe和Currency Pipe一致。如果在组件中定义 myNum: number = 0.1415927; 下面的代码会输出成下图的样子:

  1. <p>myNum : {{myNum | percent}}</p>
  2. <p>myNum (3.2-2) : {{myNum | percent:'3.2-2'}}</p>

Percent Pipe用来格式化百分数

Json Pipe

这个管道个人感觉更适合在调试中使用,它可以把任何对象格式化成JSON格式输出。如果我们在组件中定义了一个对象:

  1. object: Object = {
  2. foo: 'bar',
  3. baz: 'qux',
  4. nested: {
  5. xyz: 3,
  6. numbers: [1, 2, 3, 4, 5]
  7. }
  8. };

那么下面的模板会输出下图的样子,在调试阶段,这个特性很好帮助你输出可读性很强的对象格式。当然如果你使用了现代化的IDE,这么使用的意义就不是很大了:

  1. <div>
  2. <p>Without JSON pipe:</p>
  3. <pre>{{object}}</pre>
  4. <p>With JSON pipe:</p>
  5. <pre>{{object | json}}</pre>
  6. </div>

Json Pipe用于以Json形式格式化对象

指令——Directive

另一个我们一直没有提到的重要概念就是指令了,但这个虽然我们没提到,却已经用过了。比如 *ngFor*ngIf 等,这些都叫做结构性指令,而像 *ngModel 等属于属性型指令。
Angular 2中的指令分成三种:结构型(Structural)指令和属性型(Attribute)指令,还有一种是什么呢?就是Component,组件本身就是一个带模板的指令。
结构型指令可以通过添加、删除DOM元素来更改DOM树的布局,比如我们前面使用 *ngFor在todo-list的模板中添加了多个todo-item。而属性型指令可以改变一个DOM元素的外观或行为,比如我们利用 *ngModel 进行双向绑定,改变了该组件的默认行为(我们在组件中改变某个变量值,这种改变会直接反应到组件上,这并不是组件自身定义的行为,而是我们通过 *ngModel 来改变的)。
Angular 2中给出的内建结构型指令如下表所示:

名称 用法 说明
ngIf <div*ngIf="canShow"> 基于canShow表达式的值移除或重新创建部分DOM树。
ngFor <li *ngFor="let todo of todos"> 把li元素及其内容转化成一个模板,并用它来为列表中的每个条目初始化视图。
ngSwitch, ngSwitchCase, ngSwitchDefault <div [ngSwitch]="someCondition"></div> 基于someCondition的当前值,从内嵌模板中选取一个,有条件的切换div的内容。

自定义一个指令也很简单,我们动手做一个。这个指令非常简单就是使任何控件加上这个指令后,其点击动作都会在console中输出 “I am clicked”。由于我们要监视其宿主的click事件,所以我们引入了 HostListener,在onClick方法上用 @HostListen(‘click’) ,表明在检测到宿主发生click事件时调用这个方法。

  1. import {
  2. Directive,
  3. HostListener
  4. } from '@angular/core';
  5. @Directive({
  6. selector: "[log-on-click]",
  7. })
  8. export class LogOnClickDirective {
  9. constructor() {}
  10. @HostListener('click')
  11. onClick() { console.log('I am clicked!'); }
  12. }

在模板中简单写一句就可以看效果了

<button log-on-click>Click Me</button>

自定义指令使得点击按钮会log一条消息

代码: https://github.com/wpcfan/awesome-tutorials/tree/master/angular2/ng2-tut

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注