剖析 Angular 2

By

汤桂川 @广发证券

About me

汤桂川

爱Coding,也爱Traveling

@ 腾讯 - AlloyTeam 前端工程师

  • WebQQ and Smart QQ
  • PC QQ and QQ群
  • 手机QQ:吃喝玩乐,兴趣部落

@ 广发证券 - 前端技术专家

  • 广发微店 - 易淘金
  • 广发大数据日志平台
  • 金钥匙 - 金融界的嘀嘀打车

Agenda - 大纲

① 概述

  • Angular2 前世今生
  • ES6
  • TypeScript

② 核心部分

  • Component
  • DI
  • Template
  • Tools
  • ngUpgrade

③  延伸

  • Change Detection     - Zone.js

  • Reactive Programming - Rx.js
  • Web worker
  • server-side Render

Part 1 - 概述

👐

  • Angular2 前世今生
  • ES6
  • TypeScript

Angular 2 前世今生

  • 2014年宣布开发2.0
  • 2015.02   Alpha.1  -  55
  • 2015.12   Beta.0
  • 2016.03.19 Beta.11 & Build
  • SPEED(change detection,启动时间)

  • CROSS PLATFORM(worker,server,ionic)

  • Mobile (Android 4.1,  iOS 7.1,IE9+)

  • Flexible (TypeScript/ES6, ES5,dart)

New Feature:

将ng2的Hello World例子的负载减少到 <10KB min+compressed

Angular 1 版本:

<p>Today is {{todayDate | date}}</p>

Angular Dart 版本:

<p>Today is {{todayDate | date}}</p>

Angular 2 版本:

<p>Today is {{todayDate | date}}</p>

语法 -  (eg: 按一定格式输出今天的日期)

import { bootstrap } from 'angular2/platform/browser';
import { Component, Output, AfterViewInit } from 'angular2/core';
import { LoggerService }  from './logger.service';
import { RouteConfig, ROUTER_DIRECTIVES, ROUTER_PROVIDERS } from 'angular2/router';

@Component({
  selector: 'my-app',
  directives: [ROUTER_DIRECTIVES, HomeComponent],
  providers: [LoggerService, ROUTER_PROVIDERS],
  template: `
    <a [routerLink]="['Home']">Home</a>
    <router-outlet></router-outlet>
    <!--<form (submit)="addName()" *ngIf="canAddName">
       <input [(ngModel)]="newName" [style.color] = "isSpecial ? 'red' : 'green'">
       <img [src]="addImageUrl" >
    </form>
    <p *ngFor="#name of personNames">{{name}}</p> -->
  `,
  styles: ['p{padding: 8px;}']
})
@RouteConfig([{ path: '/home', name: 'Home',  component: HomeComponent }])
export class AppComponent implements AfterViewInit{
    private _logger:LoggerService;
    static addImageUrl = '';
    @Output('everyFiveSeconds') five5Secs = new EventEmitter();
    constructor(logger:LoggerService){
        this._logger = logger;
        setInterval(() => this.five5Secs.emit("event"), 5000);
    }
    ngAfterViewInit() {
        this._logger.log(`After View Init.`);
    }
}
bootstrap(AppComponent).catch(err => console.error(err));
import { Directive, ElementRef, Renderer } from 'angular2/core';

@Directive({
    selector: '[myHighlight]'
})
export class HighlightDirective { 

  constructor(el: ElementRef, renderer: Renderer) {
        
       // renderer.setElementStyle(el, 'backgroundColor', 'yellow');
       // renderer接口更改了
       renderer.setElementStyle(el.nativeElement, 'backgroundColor', 'yellow');
  }

}

SO 暂不关注太多细节。。。

ECMAScript 6

-  2015. 06. 17 

ECMAScript 6 在浏览器的实现情况

Modules (1/4)

// 模块导入
import { lightRed } from './colors';
import { Component } from 'angular2/core';
import * as UserModule from './user.module';

let userCtrl = UserModule.ctrl;

// 模块导出
export { userCtrl };
export class App{
  constructor() {}
};
export default lightRed;
  • ng2 is modular
  • vs CommonJS
  • 循环依赖?
  • Native, HTTP 2
  • System.js
var webpackOptions = {
  resolve: {
     root: [
         path.resolve(__dirname, "../src/app"),//根路径
         path.resolve(__dirname, "../../public-modules")
     ],
     extensions: ['', '.js'],
     alias: {
        functions:  "utils/functions.js" // 配置需要的文件别名
     }
  }
};

//import {resolveInject} from '../../../../public-modules/utils/functions';
import {resolveInject} from 'functions';

Classes (2/4)

class Animal {
  constructor(name) {
    this.name = name;
  }
  say(msg){
    alert(`${this.name} says ${msg}`);
  }
  static staticMethod() {
    return '不能被实例继承,子类可继承父类';
  }
}
class Panda extends Animal {
  say(msg = 'hi') {
    super(msg); // 调用父类say方法
  }
}
let panda = new Panda('Tuan');
panda.say(); // 'Tuan says hi'
panda.say('Ooooh'); // 'Tuan says Ooooh'
  • constructor
  • this
  • default
  • extends
  • super
  • 静态方法

IN TypeScript 

Template String (3/4)

Notice?

// 多行字符串,引号
var multiStr = `In 'JavaScript' this is
                not legal.`;


// 字符串插值
var name = "ITA1024", time = "today";
console.log(`Hello ${name},
how are you ${time}?`);

Arrows (4/4)

const name = 'object name';
let obj = {
  name,
  init() {
    // Lexical this
    $.on('click', () => this.name);
  }
};
// 以上代码等同于 ES5
var name = 'object name';
var obj = {
  name: name,
  init: function init() {
    var _this = this;
    
    $.on('click', function () {
      return _this.name;
    });
  }
};
  • C# 3.0 lambda 
  • Lexical this
  • 单行隐性return value
  • No arguments object

** 严格模式下,

模块里顶层的this

是undefined

ES6Features

查看详情:  ES6features(中文版)

TypeScript

Type  +  Java  Script

interface ClockInterface {
    currentTime: Date;
    color?: string;
}

class Clock implements ClockInterface  {
    private  name: string;
    currentTime: Date;
    static origin = {x: 0, y: 0};

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

Decorator - 修饰器

@SomeDecorator
class Test {}


// 等同于
class Test {}
Test = SomeDecorator(Test) || Test;
  • compile,no runtime
  • class ,  class method
  • vs Annotation 
  • @ AtScript ?
function someDecorator(target) {
  // 对目标函数进行处理
  target.isTestable = true;
}

@Directive

@Animation

@Inject

@InjectLazy

@Optional

@Host

@Parent

@Pipe

@Property

@Event

@RouteConfig

@HostBinding

@HostEvent

@ContentChildren

@ViewChild

@ViewChildren

@Input

@Output

@Attribute

@CanActivate

学习资源:

Part 2 - 核心部分

component 组件

  • Web Component
  • 组件化开发
  • Tree - 分治
  • 相互独立

HTML

CSS

JS

import { Component } from 'angular2/core';
import {AppHeadComponent} from './app-head.component';
import { LogService } from './log.service';

@Component({
  selector: 'my-app',
  providers: [ LogService ],
  directives: [ AppHeadComponent ],
  pipes: [], 
  styles: [`
    .main-body { width: 60px; }    
  `], 
  template: `
    <my-app-head [title]="titleStr"></my-app-head>
    <div class="main-body">
        Test..
    </div>
  `
});
export class MyAppComponent {
    titleStr = 'My App';
    constructor(private _log: LogService){ }
}
  • @Component
  • metaData
  • 多组件依赖

组件树工作机制

[property]="expression"

@Input()

(event)="update()"

@Output()

根组件

子组件

子组件

孙组件

<cmp [property]="someExp" (someEvent)="update()"></cmp>

class Cmp {
  @Input(name) property: any;
  @Output() someEvent: EventEmitter;
  //...
}

组件的生命周期

Hooks:

  • OnInit
  • OnDestroy
  • DoCheck 
  • OnChanges 
  • AfterViewInit
  • AfterViewChecked
  • AfterContentInit
  • AfterContentChecked
  • 调用方法:ng+..   (eg: ngOnInit)
  • ngOnChanges 搭配SimpleChange拿前后值
  • DoCheck 自定义变化检测,不与changes同时存在
import {Component, OnInit} from 'angular2/core';

@Component({
  selector: 'my-test'
})
export class MyTest implements OnInit{

   ngOnInit() {
    console.log(`onInit`);
  }
}
import { Component } from 'angular2/core';
import {AppHeadComponent} from './app-head.component';
import { LogService } from './log.service';

@Component({
  selector: 'my-app',
  providers: [ LogService ],
  directives: [ AppHeadComponent ],
  pipes: [], 
  styles: [`
    .main-body { width: 60px; }    
  `], 
  template: `
    <my-app-head [title]="titleStr"></my-app-head>
    <div class="main-body">
        Test..
    </div>
  `
});
export class MyAppComponent {
    titleStr = 'My App';
    constructor(private _log: LogService){ }
}

Dependency Injection 依赖注入

interfaces, no implement

组件

服务接口

服务实现

实现

提供给

// log.service.ts
import {Injectable} from 'angular2/core';

@Injectable()
export class LogService{

  log(msg:string){
    console.log(msg)
  }
}

Services

-

服务

// 使用服务
import {Component} from 'angular2/core';
import {LogService} from './log.service';

@Component({
    selector: 'my-app',
    providers: [LogService]
})
export class AppComponent {
  constructor(private _log: LogService){ }
  doLog(msg) {
      this._log.log(msg);  
  }
}

全局注入(不推荐)

bootstrap(AppComponent, [LogService]); 

可能(可选)注入

import {Optional} from 'angular2/core';

constructor(@Optional() private _logger:Logger) {  }

Child Injectors - 非单例

var injector = Injector.resolveAndCreate([Engine]);
var childInjector = injector.resolveAndCreateChild([Engine]);

injector.get(Engine) !== childInjector.get(Engine);
  • 子注入器可继承父类注入器中声明的依赖项
  • 声明某依赖创建的实例与父注入器相同token的实例不一致

延伸:

指令注入指令:

@ContentChildren,@ViewChildren

@Component({selector: 'tab', template: 'tab.html'})
class Tab {
  @ContentChildren(Pane) panes: QueryList<pane>;
   
  ngAfterContentInit() {
    // contentChildren is set
  }
}

@Component({selector: 'pane', template: 'pane.html'})
class Pane {
}
export provide(
    token: any, 
    {useClass, useValue, useExisting, useFactory, deps, multi}: {
      useClass?: Type,
      useValue?: any,
      useExisting?: any,
      useFactory?: Function,
      deps?: Object[],
      multi?: boolean
    }
) : Provider

可提供的服务 Provider:类,值,工厂

  • useExisting:新旧共存,起别名
  • multi: 多依赖,[ ]
var injector = Injector.resolveAndCreate([
  new Provider("message", { useValue: 'Hello' })
]);
expect(injector.get("message")).toEqual('Hello');

// 可用provide简写new Provider,metadata
providers:[provide('tokenStr', {useValue: 'CONFIG'})]
import { Component } from 'angular2/core';
import {AppHeadComponent} from './app-head.component';
import { LogService } from './log.service';

@Component({
  selector: 'my-app',
  providers: [ LogService ],
  directives: [ AppHeadComponent ],
  pipes: [], 
  styles: [`
    .main-body { width: 60px; }    
  `], 
  template: `
    <my-app-head [title]="titleStr"></my-app-head>
    <div class="main-body">
        Test..
    </div>
  `
});
export class MyAppComponent {
    titleStr = 'My App';
    constructor(private _log: LogService){ }
}
  • templateUrl
  • Component = Directives + template

Template 模板

  • PROPERTY BINDINGS - 属性绑定
  • EVENT BINDINGS - 事件绑定

[] vs {{}}

<img src={{picUrl}}>

默认行为

主动权?

  • INTERPOLATION - 插值

<div [textContent]="interpolate(['My name is'], [name])"></div>

// 语法糖
<div>My name is {{name}}</div>
<cmp bind-prop="someExp"></cmp>

// 语法糖
<cmp [prop]="someExp"></cmp>
<cmp on-event="someEvent()"></cmp>

// 语法糖
<cmp (event)="someEvent()"></cmp>
<todo-cmp [model]="todo" (complete)="onComplete(todo)"></todo-cmp>
@Component({
    // inputs: ['model'],
    // outputs: ['complete'],
    selector: 'todo-cmp'
})
class TodoCmp {
  @Input() model;
  @Output() complete = new EventEmitter(); // emit() subscibe

  constructor(){ }
}

inputs vs @Input ?

TodoCmp 内部:

[],() vs 数据流动机制  (~~组件树)

input

output

双向绑定 - In Forms

@Directive({
  selector: '[ngModel]',
  host: {
    "[value]": 'ngModel',
    "(input)": "ngModelChange.next($event.target.value)"
  }
})
class NgModelDirective {
  @Input() ngModel:any; // 存储数据
  @Output() ngModelChange:EventEmitter = new EventEmitter() // 事件触发
}

实现原理:

<input [ngModel]="todo.text" (ngModelChange)="todo.text=$event"></input>
// 语法糖
<input [(ngModel)]="todo.text"></input>
ngControl in forms:跟踪状态变化和有效性校验

Pipe  - 过滤加工器

<p>Your birthday is {{ birthday | date:'shortDate'}} /p>

以上将输出 'Your birthday is 9/3/1999'
  • 内置的Pipes

      DateUpperCaseLowerCaseCurrency, Percent

  • 链式调用Pipes
<p> Your birthday is{{ birthday | date | uppercase}} </p>
MAR 9, 1999
  • 自定义Pipes
import {Pipe, PipeTransform} from 'angular2/core';
//求某数的指数倍
@Pipe({name: 'exponentialStrength'})
export class ExponentialStrengthPipe implements PipeTransform {
  transform(value:number, args:string[]) : any {
    return Math.pow(value, parseInt(args[0] || '1', 10));
  }
}

模板内置指令

ng-show ?

<p  [hidden]="expression">
NgClass, NgStyle, NgIf, NgSwitch, NgFor
<my-cmp *ngIf="isShow" [personName]="myName"></my-cmp>

eg:深入剖析NgIf

<my-cmp template="ngIf:isShow" [personName]="myName"></my-cmp>
<template [ngIf]="isShow">
    <my-cmp  [personName]="myName"></my-cmp>
</template>

Tools

工具

Angular 2 Style Guide

代码质量检查工具 ng2lint 

- 基于TSLint

// 不好
<input bindon-ngModel="person.firstName">

// 好
<input [(ngModel)]="person.firstName">

注:避免ElementRef nativeElement尽量使用顶层的方法封装

等等

Debug

Angular 2.0 Batarangle 

- chrome extension

Scaffold - 脚手架

ngUpgrade

升级

import {UpgradeAdapter} from 'angular2/upgrade';

var adapter = new UpgradeAdapter();
var app = angular.module('myApp', []);

adapter.bootstrap(document.body, ['myApp']);

混合NG1,NG2代码

资源:

Part 3 - 延伸

  • Change Detection - Zone.js
  • Reactive Programming - Rx.js
  • Web Worker
  • server-side Render

Change Detection

Zone.js

造成变化的因素:

  • Events - click,  change …
  • XHR - Ajax远程获取数据
  • Timers - setTimeout(), setInterval()

异 步!

怎么通知变化了?

Zone.js

  • zone? (Dart, 异步时序, hooks)
dirty:scope.$apply()
foo();
setTimeout(doSth, 0);
bar();
zone.run(() => {
  foo();
  setTimeout(doSth, 0);
  bar();
});

monkey-patched (内置对象扩展) in zone

  • Zone.setInterval()
  • Zone.alert()
  • Zone.prompt()
  • Zone.requestAnimationFrame()
  • Zone.addEventListener()
  • Zone.removeEventListener()
zone.fork({
  beforeTask: function () {...},
  afterTask: function () {...},
  ...
})
.run(() => {
  foo();
  setTimeout(doSth, 0);
  bar();
});

NgZone

  • onTurnStart() 
  • onTurnDone() 
  • onEventDone()
  • based on Observables
this.zone.onTurnDone
  .subscribe(() => {
    this.zone.run(() => this.tick() })
  })
tick() {
  this.changeDetectorsRefs
    .forEach((ref) => ref.detectChanges())
}

如何变化

根组件

子组件

子组件

孙组件

孙组件

孙组件

孙组件

发生变化

孙组件

  • 自上而下
  • 几毫秒做成百上千次检查
  • VM优化

根组件

子组件

子组件

孙组件

孙组件

孙组件

孙组件

发生变化

孙组件

Immutable Objects

  • 来源?
  • 可变数据的问题?(引用)
  • 只依赖于input属性

智能的变化检测

var someData = someAPIForImmutables.create({
              name: 'test'
            });

var someData2 = someData.set('name', 'test2');

someData === someData2 // false
@Component({
  template: `
    <h2>{{someData.name}}</h2>
    <span>{{someData.email}}</span>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
class CardCmp {
  @Input() someData;
}

还可以避免 input不变的 子组件树 的更新

根组件

子组件

子组件

孙组件

孙组件

孙组件

孙组件

发生变化

孙组件

onPush

Observables

订阅模式 (emit, subscribe)

@Component({
  template: '{{counter}}',
  changeDetection: ChangeDetectionStrategy.OnPush
})
class CartBadgeCmp {

  @Input() addItemStream:Observable<any>;
  counter = 0;

  constructor(private cd: ChangeDetectorRef) {}

  ngOnInit() {
    this.addItemStream.subscribe(() => {
      this.counter++; // application state changed
      this.cd.markForCheck(); // marks path
    })
  }
}

markForCheck() : 标示变化的路径

根组件

子组件

子组件

孙组件

孙组件

孙组件

孙组件

发生变化

  • Big O
  • 组合
  • 使用场景

孙组件

Reactive Programming

Rx.js

Gulp  ?

订阅,观察者

HTTP Client

好处:vs promise   - 大一统(events),加工

  • 自定义触发时机
  • 缓存数据
  • 多、无序的请求:switchMap(自动退订)
@Component({
  selector: 'my-app',
  template: `
    <div>
      <h2>Wikipedia Search</h2>
      <input type="text" [ngFormControl]="term"/>
      <ul>
        <li *ngFor="#item of items | async">{{item}}</li>
      </ul>
    </div>
  `
})
export class App {
  items: Observable<Array<string>>;
  term = new Control();
  constructor(private wikipediaService: WikipediaService) {
    this.items = this.term.valueChanges
                 .debounceTime(400)
                 .distinctUntilChanged()
                 .switchMap(term => this.wikipediaService.search(term));
  }
}

More Detail:

Webworkers

Note:

  • 不能访问DOM,window, Document
  • 浏览器API
  • 不能与UI共享内存
  • 并发的问题

使用场景?

<script src="angular2/web_worker/ui.js"></script>


// index.js
import {WORKER_RENDER_PLATFORM, WORKER_RENDER_APPLICATION, 
WORKER_SCRIPT} from "angular2/platform/worker_render";

import {platform} from "angular2/core";

platform([WORKER_RENDER_PLATFORM])
.application([WORKER_RENDER_APPLICATION, provider(WORKER_SCRIPT, {useValue: "loader.js"});


// loader.js
importScripts("./es6-shim.js", "./system@0.16.js", "angular2/web_worker/worker.js");
System.import("app");


// app.ts
import {Component, View, platform} from "angular2/core";
import {WORKER_APP_PLATFORM, WORKER_APP_APPLICATION} from "angular2/platform/worker_app";

@Component({
  selector: "hello-world"
  template: "<h1>Hello {{name}}</h1>
})
export class HelloWorld {
  name: string = "Jane";
}

platform([WORKER_APP_PLATFORM])
.application([WORKER_APP_APPLICATION])
.then((ref) => ref.bootstrap(RootComponent));

server-side Render

服务端渲染

preboot - control

注意事项:

  • 不要直接操作nativeElement,用Render

        -确保多平台能正确更新view

constructor(element: ElementRef, renderer: Renderer) {
  renderer.setElementStyle(element.nativeElement, 'font-size', 'x-large');
}
  • 尽量让指令无状态化,减少跟随属性的变化<img url="">
  • 从"angular2/src/platform/dom/dom_adapter"

      使用支持的DOM操作

Summary - 总结

① 概述

  • Angular2 前世今生
  • ES6
  • TypeScript

② 核心部分

  • Component
  • DI
  • Template
  • Tools
  • ngUpgrade

③  延伸

  • Change Detection     - Zone.js

  • Reactive Programming - Rx.js
  • Web worker
  • server-side Render

更多资源:

<thank-you  author="汤桂川" [date]="'2016.03'">

</thank-you>

tangguichuan@gmail.com

加入广发证券,

一起做前沿好玩的事儿:

Dissect Angular 2

By Tang Guichuan

Dissect Angular 2

Dissect Angular2 (ng2) core ideas

  • 7,743