剖析 Angular 2
By
汤桂川 @广发证券
爱Coding,也爱Traveling
@ 腾讯 - AlloyTeam 前端工程师
@ 广发证券 - 前端技术专家
① 概述
② 核心部分
③ 延伸
Change Detection - Zone.js
Part 1 - 概述
👐
Angular 2 前世今生
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 在浏览器的实现情况
// 模块导入
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;
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';
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'
IN TypeScript
Notice?
// 多行字符串,引号
var multiStr = `In 'JavaScript' this is
not legal.`;
// 字符串插值
var name = "ITA1024", time = "today";
console.log(`Hello ${name},
how are you ${time}?`);
More detail in:
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;
});
}
};
查看详情: ES6features(中文版)
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;
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 - 核心部分
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){ }
}
组件树工作机制
[property]="expression"
@Input()
(event)="update()"
@Output()
根组件
子组件
子组件
孙组件
<cmp [property]="someExp" (someEvent)="update()"></cmp>
class Cmp {
@Input(name) property: any;
@Output() someEvent: EventEmitter;
//...
}
组件的生命周期
Hooks:
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);
延伸:
指令注入指令:
@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:类,值,工厂
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){ }
}
[] vs {{}}
<img src={{picUrl}}>
默认行为
主动权?
<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'
Date, UpperCase, LowerCase, Currency, Percent
<p> Your birthday is{{ birthday | date | uppercase}} </p>
MAR 9, 1999
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>
// 不好
<input bindon-ngModel="person.firstName">
// 好
<input [(ngModel)]="person.firstName">
注:避免ElementRef nativeElement尽量使用顶层的方法封装
等等
Debug
Scaffold - 脚手架
准备工作
使用更新适配器进行更新
How The Upgrade Adapter Works
Bootstrapping Hybrid Angular 1+2 Applications
Using Angular 2 Components from Angular 1 Code
Using Angular 1 Component Directives from Angular 2 Code
Projecting Angular 1 Content into Angular 2 Components
Transcluding Angular 2 Content into Angular 1 Component Directives
Making Angular 1 Dependencies Injectable to Angular 2
Making Angular 2 Dependencies Injectable to Angular 1
import {UpgradeAdapter} from 'angular2/upgrade';
var adapter = new UpgradeAdapter();
var app = angular.module('myApp', []);
adapter.bootstrap(document.body, ['myApp']);
混合NG1,NG2代码
资源:
Part 3 - 延伸
造成变化的因素:
异 步!
怎么通知变化了?
Zone.js
dirty:scope.$apply()
foo();
setTimeout(doSth, 0);
bar();
zone.run(() => {
foo();
setTimeout(doSth, 0);
bar();
});
monkey-patched (内置对象扩展) in zone
zone.fork({
beforeTask: function () {...},
afterTask: function () {...},
...
})
.run(() => {
foo();
setTimeout(doSth, 0);
bar();
});
NgZone
this.zone.onTurnDone
.subscribe(() => {
this.zone.run(() => this.tick() })
})
tick() {
this.changeDetectorsRefs
.forEach((ref) => ref.detectChanges())
}
如何变化
根组件
子组件
子组件
孙组件
孙组件
孙组件
孙组件
发生变化
孙组件
根组件
子组件
子组件
孙组件
孙组件
孙组件
孙组件
发生变化
孙组件
Immutable Objects
智能的变化检测
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
订阅模式 (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() : 标示变化的路径
根组件
子组件
子组件
孙组件
孙组件
孙组件
孙组件
发生变化
孙组件
Gulp ?
订阅,观察者
HTTP Client
好处:vs promise - 大一统(events),加工
@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:
Note:
使用场景?
<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
注意事项:
-确保多平台能正确更新view
constructor(element: ElementRef, renderer: Renderer) {
renderer.setElementStyle(element.nativeElement, 'font-size', 'x-large');
}
使用支持的DOM操作
Summary - 总结
① 概述
② 核心部分
③ 延伸
Change Detection - Zone.js
更多资源:
<thank-you author="汤桂川" [date]="'2016.03'">
</thank-you>
tangguichuan@gmail.com
加入广发证券,
一起做前沿好玩的事儿: