Giter Club home page Giter Club logo

angular2-ionic2's Introduction

Hi,我是阿宝哥。17 年开始在思否写 Angular 修仙之路专栏,已输出 152 篇原创文章。曾获得思否年度优秀文章作者及两季 Top Writer,今年开始写 “重学TS”(60几篇)、玩转前端、你不知道的和优质前端项目 源码分析 系列专题。观看 《轻松学 TypeScript》 系列教程,已有十几期。

平常活跃在各个社区,这里分享主要的社区地址:


📚 我的 PDF 电子书

2020 年是不平凡的一年,今年阿宝哥也迈出了一大步,写了三本 PDF 电子书,这里墙裂推荐其中的两本:

阿宝哥的《前端进阶篇》- 170页 ⭐⭐⭐

“玩转前端专题” 包含玩转 Video 播放器、玩转图片处理、玩转 Word 文档、玩转混合加密和玩转网络五大章节的内容。而 “你不知道的 XXX 专题” 包含你不知道的 Web Workers、你不知道的 WebSocket、你不知道的 Blob 及你不知道的 WeakMap 四大章节的内容。

阿宝哥的《重学TS》- 228页 ⭐⭐⭐⭐

“重学TS” 包含了 TypeScript 快速入门篇(1.8W字)、九种常见的 TypeScript 设计模式、TypeScript 进阶之插件化架构、控制反转与依赖注入及编写高效 TypeScript 代码的一些建议。


😊 一起交流

想进一步阅读 50 几篇的 TypeScript 文章或前端架构技术干货,邀请你关注 全栈修仙之路,另外,如果你在学习、成长过程中遇到什么问题,也可以添加我的微信一起交流。

angular2-ionic2's People

Contributors

semlinker avatar yanhaijing avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

angular2-ionic2's Issues

Angular 2 属性指令 vs 结构指令

Angular 2 的指令有以下三种:

  • 组件(Component directive):用于构建UI组件,继承于 Directive 类
  • 属性指令(Attribute directive): 用于改变组件的外观或行为
  • 结构指令(Structural directive): 用于动态添加或删除DOM元素来改变DOM布局

组件

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

@Component({
  selector: 'my-app', // 定义组件在HTML代码中匹配的标签
  template: `<h1>Hello {{name}}</h1>`, // 指定组件关联的内联模板
})
export class AppComponent  {
  name = 'Angular'; 
}

Angular 2 内置属性指令

1.ngStyle指令: 用于设定给定 DOM 元素的 style 属性

使用常量

<div [ngStyle]="{'background-color': 'green'}"></div>

使用变量

<div [ngStyle]="{'background-color': person.country === 'UK' ? 'green' : 'red'}">

具体示例:

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

@Component({
    selector: 'ngstyle-example',
    template: `<h4>NgStyle</h4>
    <ul *ngFor="let person of people">
        <li [ngStyle]="{'color': getColor(person.country)}">
			{{ person.name }} ({{person.country}})
	    </li>
    </ul>
    `
})
export class NgStyleExampleComponent {

    getColor(country: string) {
        switch (country) {
            case 'CN':
                return 'red';
            case 'USA':
                return 'blue';
            case 'UK':
                return 'green';
        }
    }

    people: any[] = [
        {
            name: "Semlinker",
            country: 'CN'
        },
        {
            name: "Donald John Trump",
            country: 'USA'
        },
        {
            name: "Daniel Manson",
            country: 'UK'
        }
    ];
}

上面的例子,除了使用 ngStyle 指令,我们还可以使用 [style.] 的语法:

 <ul *ngFor="let person of people">
     <li [style.color]="getColor(person.country)">
		{{ person.name }} ({{person.country}})
	 </li>
</ul>

2.ngClass指令:用于动态的设定 DOM 元素的 CSS class

使用常量

<div [ngClass]="{'text-success': true }"></div>

使用变量

<div [ngClass]="{'text-success': person.country === 'CN'}"></div>

具体示例:

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

@Component({
    selector: 'ngclass-example',
    template: `
    <style>
      .text-success {
         color: green 
      }
      .text-primary {
          color: red
      }
      .text-secondary {
          color: blue
      } 
    </style>
    <h4>NgClass</h4>
    <ul *ngFor="let person of people">
        <li [ngClass]="{
          'text-success': person.country === 'UK',
          'text-primary': person.country === 'CN',
          'text-secondary': person.country === 'USA'  
        }">{{ person.name }} ({{person.country}})</li>
    </ul>
    `,
   
})
export class NgClassExampleComponent {

    people: any[] = [
        {
            name: "Semlinker",
            country: 'CN'
        },
        {
            name: "Donald John Trump",
            country: 'USA'
        },
        {
            name: "Daniel Manson",
            country: 'UK'
        }
    ];
}

Angular 2 内置结构指令

1.ngIf指令:根据表达式的值,显示或移除元素

<div *ngIf="person.country === 'CN'">{{ person.name }} ({{person.country}})</div>

2.ngFor指令:使用可迭代的每个项作为模板的上下文来重复模板,类似于 Ng 1.x 中的 ng-repeat 指令

<div *ngFor="let person of people">{{person.name}}</div>

3.ngSwitch指令:它包括两个指令,一个属性指令和一个结构指令。它类似于 JavaScript 中的 switch 语句

<ul [ngSwitch]='person.country'>
  <li *ngSwitchCase="'UK'" class='text-success'>
  	{{ person.name }} ({{person.country}})
  </li>
   <li *ngSwitchCase="'USA'" class='text-secondary'>
  	{{ person.name }} ({{person.country}})
  </li>
  <li *ngSwitchDefault class='text-primary'>
    {{ person.name }} ({{person.country}})
  </li>
</ul>

通过上面的例子,可以看出结构指令和属性指令的区别。结构指令是以 * 作为前缀,这个星号其实是一个语法糖。它是 ngIf 和 ngFor 语法的一种简写形式。Angular 引擎在解析时会自动转换成 标准语法。

Angular 2 内置结构指令标准形式

1.ngIf指令:

<template [ngIf]='condition'>
   <p>I am the content to show</p>
</template>

2.ngFor指令:

<template ngFor [ngForOf]="people" let-person>
   <div> {{ person.name }} ({{person.country}}) </div>
</template>

3.ngSwitch指令:

<ul [ngSwitch]='person.country'>
  <template [ngSwitchCase]="'UK'">
      <li class='text-success'>
  		{{ person.name }} ({{person.country}})
  	  </li>
  </template>
  <template [ngSwitchCase]="'USA'">
      <li class='text-secondary'>
  		{{ person.name }} ({{person.country}})
  	  </li>
  </template>
  <template [ngSwitchDefault]>
      <li class='text-primary'>
  		{{ person.name }} ({{person.country}})
  	  </li>
  </template>
</ul>

Angular 2 内置结构指令定义

1.ngIf指令定义:

@Directive({selector: '[ngIf]'})
export class NgIf {}

2.ngFor指令定义:

@Directive({selector: '[ngFor][ngForOf]'})
export class NgForOf<T> implements DoCheck, OnChanges {}

3.ngSwitch指令定义:

@Directive({selector: '[ngSwitch]'})
export class NgSwitch {}

@Directive({selector: '[ngSwitchCase]'})
export class NgSwitchCase implements DoCheck {}

@Directive({selector: '[ngSwitchDefault]'})
export class NgSwitchDefault {}

自定义属性指令

指令功能描述:该指令用于在用户点击宿主元素时,根据输入的背景颜色,更新宿主元素的背景颜色。宿主元素的默认颜色是黄色。

  1. 指令实现

    import {Directive, Input, ElementRef, HostListener} from "@angular/core";
    
    @Directive({
      selector: '[exeBackground]'
    })
    export class BeautifulBackgroundDirective {
      private _defaultColor = 'yellow';
      private el: HTMLElement;
    
      @Input('exeBackground')
      backgroundColor: string;
    
      constructor(el: ElementRef) {
        this.el = el.nativeElement;
        this.setStyle(this._defaultColor);
      }
    
      @HostListener('click')
      onClick() {
        this.setStyle(this.backgroundColor || this._defaultColor);
      }
    
      private setStyle(color: string) {
        this.el.style.backgroundColor = color;
      }
    }
    
    

2.指令应用:

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

@Component({
  selector: 'my-app', 
  template: `<h1 [exeBackground]="'red'">Hello {{name}}</h1>`,
})
export class AppComponent  {
  name = 'Angular'; 
}

自定义结构指令

指令功能描述:该指令实现 ngIf 指令相反的效果,当指令的输入条件为 Falsy 值时,显示DOM元素。

1.指令实现

@Directive({
  selector: '[exeUnless]'
})
export class UnlessDirective {
  @Input('exeUnless')
  set condition(newCondition: boolean) {
    if (!newCondition) {
      this.viewContainer.createEmbeddedView(this.templateRef);
    } else {
      this.viewContainer.clear();
    }
  }

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

2.指令应用

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

@Component({
  selector: 'my-app',
  template: `<h1 [exeBackground]="'red'" *exeUnless="condition">Hello {{name}}</h1>`, 
})
export class AppComponent  {
  name = 'Angular'; 
  condition: boolean = false;
}

总结

本文主要介绍了 Angular 2 中的属性指令和结构指令,通过具体示例介绍了 Angular 2 常见内建指令的使用方式和区别。最终,我们通过自定义属性指令和自定义结构指令两个示例,展示了如何开发自定义指令。

Angular 2 Pipe

Angular 2 中 Pipe(管道) 与 Angular 1.x 中的 filter(过滤器) 的作用的是一样的。它们都是用来对输入的数据进行处理,如大小写转换、数值和日期格式化等。

angular2-pipe

Angular 2 内建管道

AsyncPipe、CurrencyPipe、DatePipe、DecimalPipe、I18nPluralPipe、I18nSelectPipe、

JsonPipe、LowerCasePipe、PercentPipe、SlicePipe、TitleCasePipe、UpperCasePipe

Angular 2 内建管道使用示例

1.大写转换

<div>
  <p ngNonBindable>{{ 'Angular' | uppercase }}</p>
  <p>{{ 'Angular' | uppercase }}</p> <!-- Output: ANGULAR -->
</div>

2.小写转换

<div>
  <p ngNonBindable>{{ 'Angular' | lowercase }}</p>
  <p>{{ 'Angular' | lowercase }}</p> <!-- Output: angular -->
</div>

3.数值格式化

<div>
  <p ngNonBindable>{{ 3.14159265 | number: '1.4-4' }}</p>
  <p>{{ 3.14159265 | number: '1.4-4' }}</p> <!-- Output: 3.1416 -->
</div>

4.日期格式化

<div>
  <p ngNonBindable>{{ today | date: 'shortTime' }}</p>
  <p>{{ today | date: 'shortTime' }}</p> <!-- Output: 10:40 AM -->
</div>

5.JavaScript 对象序列化

<div>
  <p ngNonBindable>{{ { name: 'semlinker' } | json }}</p>
  <p>{{ { name: 'semlinker' } | json }}</p> <!-- Output: { "name": "semlinker" } -->
</div>

管道参数

管道可以接收任意数量的参数,使用方式是在管道名称后面添加 : 和参数值。如 number: '1.4-4' ,若需要传递多个参数则参数之间用冒号隔开,具体示例如下:

<div>
  <p ngNonBindable>{{ 'semlinker' | slice:0:3 }}</p>
  <p>{{ 'semlinker' | slice:0:3 }}</p> <!-- Output: sem -->
</div>

管道链

我们可以将多个管道连接在一起,组成管道链对数据进行处理。

<div>
  <p ngNonBindable>{{ 'semlinker' | slice:0:3 | uppercase }}</p>
  <p>{{ 'semlinker' | slice:0:3 | uppercase }}</p> <!-- Output: SEM -->
</div>

完整示例

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

@Component({
  selector: 'my-app',
  template: `
    <div>
      <p ngNonBindable>{{ 'Angular' | uppercase }}</p>
      <p>{{ 'Angular' | uppercase }}</p>
    </div>
    <div>
      <p ngNonBindable>{{ 'Angular' | lowercase }}</p>
      <p>{{ 'Angular' | lowercase }}</p>
    </div>
    <div>
      <p ngNonBindable>{{ 3.14159265 | number: '1.4-4' }}</p>
      <p>{{ 3.14159265 | number: '1.4-4' }}</p>
    </div>
    <div>
      <p ngNonBindable>{{ today | date: 'shortTime' }}</p>
      <p>{{ today | date: 'shortTime' }}</p>
    </div>
    <div>
      <p ngNonBindable>{{ { name: 'semlinker' } | json }}</p>
      <p>{{ { name: 'semlinker' } | json }}</p>
    </div>
    <div>
      <p ngNonBindable>{{ 'semlinker' | slice:0:3 }}</p>
      <p>{{ 'semlinker' | slice:0:3 }}</p>
    </div>
    <div>
      <p ngNonBindable>{{ 'semlinker' | slice:0:3 | uppercase }}</p>
      <p>{{ 'semlinker' | slice:0:3 | uppercase }}</p>
    </div>
  `,
})
export class AppComponent {
  today = new Date();
}

自定义管道

自定义管道的步骤:

  • 使用 @pipe 装饰器定义 Pipe 的 metadata 信息,如 Pipe 的名称 - 即 name 属性
  • 实现 PipeTransform 接口中定义的 transform 方法

1.1 WelcomePipe 定义

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

@Pipe({ name: 'welcome' })
export class WelcomePipe implements PipeTransform {
  transform(value: string): string {
    if(!value) return value;
    if(typeof value !== 'string') {
      throw new Error('Invalid pipe argument for WelcomePipe');
    }
    return "Welcome to " + value;
  }
} 

1.2 WelcomePipe 使用

<div>
   <p ngNonBindable>{{ 'semlinker' | welcome }}</p>
   <p>{{ 'semlinker' | welcome }}</p> <!-- Output: Welcome to semlinker -->
</div>

当 WelcomePipe 的输入参数,即 value 值为非字符串时,如使用 123,则控制台将会抛出以下异常:

EXCEPTION: Error in ./AppComponent class AppComponent - inline template:23:9 caused by: Invalid pipe argument for WelcomePipe

2.1 RepeatPipe 定义

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

@Pipe({name: 'repeat'})
export class RepeatPipe implements PipeTransform {
	transform(value: any, times: number) {
	    return value.repeat(times);
	}
}

2.2 RepeatPipe 使用

<div>
   <p ngNonBindable>{{ 'lo' | repeat:3 }}</p>
   <p>{{ 'lo' | repeat:3 }}</p> <!-- Output: lololo -->
</div>

管道分类

  • pure 管道:仅当管道输入值变化的时候,才执行转换操作,默认的类型是 pure 类型。(备注:输入值变化是指原始数据类型如:string、number、boolean 等的数值或对象的引用值发生变化)
  • impure 管道:在每次变化检测期间都会执行,如鼠标点击或移动都会执行 impure 管道

管道探秘

1.Pipe 相关接口与 PipeDecorator

Pipe 接口定义

export interface Pipe {
  name: string;
  pure?: boolean;
}

PipeDecorator

export const Pipe: PipeDecorator = <PipeDecorator>makeDecorator('Pipe', {
  name: undefined,
  pure: true, // 默认是pure
});

PipeTransform 接口定义

export interface PipeTransform {
  transform(value: any, ...args: any[]): any;
}

2.RepeatPipe 详解

2.1 RepeatPipe 定义

@Pipe({name: 'repeat'})
export class RepeatPipe implements PipeTransform {
	transform(value: any, times: number) {
	    return value.repeat(times);
	}
}

2.2 RepeatPipe 转换为 ES 5 代码片段

__decorate = (this && this.__decorate) || function (decorators, target, key, desc) {...};
                                                                                                  
var core_1 = require('@angular/core');
var RepeatPipe = (function () {
    function RepeatPipe() { }
    RepeatPipe.prototype.transform = function (value, times) {
        if (!value) return;
        return value.repeat(times);
    };
    RepeatPipe = __decorate([
        core_1.Pipe({ name: 'repeat' }), // 调用PipeDecorator返回TypeDecorator函数
        __metadata('design:paramtypes', [])
    ], RepeatPipe);
    return RepeatPipe;
}());

2.3 通过 Reflect API 保存后的对象信息

angular2-pipe-1

2.4 管道解析 - PipeResolver 源码片段

// @angular/compiler/src/pipe_resolver.ts
@CompilerInjectable()
export class PipeResolver {
  constructor(private _reflector: ɵReflectorReader = ɵreflector) {}

  // 通过内部的ɵReflectorReader对象提供的API读取metadata信息
  resolve(type: Type<any>, throwIfNotFound = true): Pipe {
    const metas = this._reflector.annotations(resolveForwardRef(type));
    if (metas) {
      // 读取保存的Pipe metadata 信息
      const annotation = ListWrapper.findLast(metas, _isPipeMetadata);
      if (annotation) { return annotation; }
    }
    if (throwIfNotFound) {
      throw new Error(`No Pipe decorator found on ${stringify(type)}`);
    }
    return null;
  }
}

2.5 RepeatPipe 管道的创建与执行

2.5.1 管道的创建

// JS has NaN !== NaN
function looseIdentical(a, b): boolean {
  return a === b || typeof a === 'number' && typeof b === 'number' 
    && isNaN(a) && isNaN(b);
}

// 用于检测管道的输入值或参数值是否变化,若发生变化则自动调用管道transform转换函数
function jit_pureProxy214(fn) {
        var result;
        var v0 = UNINITIALIZED; // { toString: function() { return 'CD_INIT_VALUE'} };
        var v1 = UNINITIALIZED; 
        return function (p0, p1) { 
            if (!looseIdentical(v0, p0) || !looseIdentical(v1, p1)) {
                v0 = p0; // p0: "lo"
                v1 = p1; // p1: 3
            // fn: transform(value: any, times: number) { return value.repeat(times); }
                result = fn(p0, p1); // 调用管道的transform转换函数
            }
            return result;
        };
}

self._pipe_repeat_6 = new jit_RepeatPipe18(); // 创建RepeatPipe对象
self._pipe_repeat_6_0  = jit_pureProxy214( // 代理RepeatPipe中transform函数
  self._pipe_repeat_6.transform.bind(self._pipe_repeat_6));

2.5.2 管道的执行

在 Angular 执行变化检测时,会自动调用管道中的 transform 方法

var currVal_100 = jit_inlineInterpolate21(1,'',valUnwrapper.unwrap(
  jit_castByValue22 (self._pipe_repeat_6_0,
   self._pipe_repeat_6.transform)('lo',3)),'');

总结

本文介绍了 Angular 2 中的常用内建管道的用法和管道的分类,同时也介绍了 pure 和 impure 管道的区别。 此外我们通过两个示例展示了如何自定义管道,最后详细分析了 RepeatPipe 管道的工作原理。建议读者更改 RepeatePipe 的 pure 属性为 false,体验一下 impure 管道。

Ionic 2 开发大全

搭建开发环境

环境安装

1.Windows 平台

npm install -g cordova ionic  
# 安装ant
# 系统环境变量中配置android sdk路径

2.iOS 平台

sudo npm install -g cordova ionic   
sudo npm install -g ios-sim  
ionic platform add ios  # 添加ios平台
ionic build ios # 构建ios项目  
ionic emulate ios   # 模拟器运行  
ionic run ios   # 连接真机后直接运行

模拟器运行

  • 支持模拟器运行
    • npm install -g ios-sim
  • 列出 iOS 设备类型
    • ios-sim showdevicetypes
  • 模拟器运行
    • ionic emulate ios --target="iPad-Air"
  • 开启 livereload 和 consolelogs: ionic emulate ios -l -c
  • 开启日志(Logging)
    • consolelogs:ionic emulate ios -c
    • serverlogs:ionic emulate ios -s

命令行

1.初始化项目

  • ionic start myApp [template name] --v2
    • template name: blank、sidemenu、tabs
  • ionic start myApp -a "My Awesome Ionic App"
    • -a: appname
  • ionic start myApp -i com.mycompany.appname
    • -i: app id

2.添加构建的平台

  • ionic platform add [platform name]
    • platform name: ios、android、windows
  • ionic platform remove [platform name]

3.插件管理

  • ionic plugin add [plugin id] # 添加插件
  • ionic plugin rm [plugin id] # 移除插件
  • ionic plugin ls # 列出已安装的插件

4.ionic 生成器

  • ionic g [page|component|directive|pipe|provider|tabs]

5.预览应用程序

  • ionic serve
    • ionic serve -l 在浏览器中同时预览 iOS、Android、Window 平台

6.获取命令行信息

  • ionic info

IDE

1.Visual Studio Code

Visual Studio Code 插件:

  • Auto Import
  • Debugger for Chrome
  • Document This
  • Material Theme
  • Beautify
  • Auto Rename Tag
  • Git History
  • HTML Snippets
  • Path Intellisense
  • Angular 2 TypeScript Snippets - Johnpapa
  • Angular 2 TypeScript Emmet
  • Ionic 2 Commands with Snippets
  • ESLint
  • Code Runner
  • HTML CSS Class Completion
  • JavaScript(ES 6) code snippets
  • REST Client

2.WebStorm

开发调试

1.Browser 调试工具

  • Augury - Rangle.io 开发的 Angular 2 调试工具

2.模拟器/真机调试

Chrome 插件

  • OneTab - 节省高达95%的内存,并减轻标签页混乱现象
  • Proxy SwitchyOmega - 轻松快捷地管理和切换多个代理设置
  • QR Code Generator - 二维码生成器
  • Octotree - Code tree for GitHub
  • CORS Toggle - Allow/Disable Cross Domain Request.
  • Augury - Rangle.io 开发的 Angular 2 调试工具
  • Postman - 功能超级强大 HTTP Client

抓包工具

其他工具

  • Shadowsocks

官方资源

英文资源

中文资源

Ionic 2 - 博客(英文)

Angular 2 - 博客(英文)

Angular 2 constructor 与 ngOnInit

在 Angular 2 学习过程中,相信很多初学者对 constructor 和 ngOnInit 的应用场景和区别会存在困惑,本文我们会通过实际的例子,为读者一步步解开困惑。

constructor

在 ES6 中就引入了类,constructor(构造函数) 是类中的特殊方法,主要用来做初始化操作,在进行类实例化操作时,会被自动调用。马上来个例子:

class AppComponent {
  constructor(name) {
    console.log('Constructor initialization');
    this.name = name;
  }
}

let appCmp = new AppComponent('AppCmp');
console.log(appCmp.name); 

以上代码运行后,控制台的输出结果:

Constructor initialization
AppCmp

接下来我们看一下转换后的 ES5 代码:

var AppComponent = (function () {
    function AppComponent(name) {
        console.log('Constructor initialization');
        this.name = name;
    }
    return AppComponent;
}());

var appCmp = new AppComponent('AppCmp');
console.log(appCmp.name);

ngOnInit

ngOnInit 是 Angular 2 组件生命周期中的一个钩子,Angular 2 中的所有钩子和调用顺序如下:

  1. ngOnChanges - 当数据绑定输入属性的值发生变化时调用
  2. ngOnInit - 在第一次 ngOnChanges 后调用
  3. ngDoCheck - 自定义的方法,用于检测和处理值的改变
  4. ngAfterContentInit - 在组件内容初始化之后调用
  5. ngAfterContentChecked - 组件每次检查内容时调用
  6. ngAfterInit - 组件相应的视图初始化之后调用
  7. ngAfterViewChecked - 组件每次检查视图时调用
  8. ngOnDestroy - 指令销毁前调用

其中 ngOnInit 用于在 Angular 获取输入属性后初始化组件,该钩子方法会在第一次 ngOnChanges 之后被调用。

另外需要注意的是 ngOnInit 钩子只会被调用一次,我们来看一下具体示例:

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

@Component({
  selector: 'my-app',
  template: `
    <h1>Welcome to Angular World</h1>
    <p>Hello {{name}}</p>
  `,
})
export class AppComponent implements OnInit {

  name: string = '';

  constructor() {
    console.log('Constructor initialization');
    this.name = 'Semlinker';
  }

  ngOnInit() {
    console.log('ngOnInit hook has been called');
  }
}

以上代码运行后,控制台的输出结果:

Constructor initialization
ngOnInit hook has been called

接下来我们再来看一个 父 - 子组件传参的例子:

parent.component.ts

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

@Component({
  selector: 'exe-parent',
  template: `
    <h1>Welcome to Angular World</h1>
    <p>Hello {{name}}</p>
    <exe-child [pname]="name"></exe-child>
  `,
})
export class ParentComponent {
  name: string = '';

  constructor() {
    this.name = 'Semlinker';
  }
}

child.component.ts

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

@Component({
    selector: 'exe-child',
    template: `
     <p>父组件的名称:{{pname}} </p>
    `
})
export class ChildComponent implements OnInit {
    @Input()
    pname: string; // 父组件的名称

    constructor() {
        console.log('ChildComponent constructor', this.pname); // Output:undefined
    }

    ngOnInit() {
        console.log('ChildComponent ngOnInit', this.pname);
    }
}

以上代码运行后,控制台的输出结果:

ChildComponent constructor undefined
ChildComponent ngOnInit Semlinker

我们发现在 ChildComponent 构造函数中,是无法获取输入属性的值,而在 ngOnInit 方法中,我们能正常获取输入属性的值。因为 ChildComponent 组件的构造函数会优先执行,当 ChildComponent 组件输入属性变化时会自动触发 ngOnChanges 钩子,然后在调用 ngOnInit 钩子方法,所以在 ngOnInit 方法内能获取到输入的属性。

constructor 应用场景

在 Angular 2 中,构造函数一般用于依赖注入或执行一些简单的初始化操作。

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

@Component({
  selector: 'my-app',
  template: `
    <h1>Welcome to Angular World</h1>
    <p>Hello {{name}}</p>
  `,
})
export class AppComponent {
  name: string = '';

  constructor(public elementRef: ElementRef) { // 使用构造注入的方式注入依赖对象
    this.name = 'Semlinker'; // 执行初始化操作
  }
}

ngOnInit 应用场景

在项目开发中我们要尽量保持构造函数简单明了,让它只执行简单的数据初始化操作,因此我们会把其他的初始化操作放在 ngOnInit 钩子中去执行。如在组件获取输入属性之后,需执行组件初始化操作等。

我有话说

1.在ES6 或 TypeScript 中的 Class 是不会自动提升的

因为当 class 使用 extends 关键字实现继承的时候,我们不能确保所继承父类的有效性,那么就可能导致一些无法预知的行为。具体可以参考 - Angular 2 Forward Reference 这篇文章。

2.TypeScrip 中 Class 静态属性和成员属性的区别

AppComponent.ts

class AppComponent {
  static type: string = 'component';
  name: string;

  constructor() {
    this.name = 'AppComponent';
  }
}

转化后的 ES5 代码:

var AppComponent = (function () {
    function AppComponent() {
        this.name = 'AppComponent';
    }
    return AppComponent;
}());
AppComponent.type = 'component';

通过转换后的代码,我们可以知道类中的静态属性是属于 AppComponent 构造函数的,而成员属性是属于 AppComponent 实例。

总结

在 Angular 2 中 constructor 一般用于依赖注入或执行简单的数据初始化操作,ngOnInit 钩子主要用于执行组件的其它初始化操作或获取组件输入的属性值。

Ionic 2 i18n 方案设计

语言包结构设计

1.目录结构设计

  • common lang
    • server side lang - 与后台共用的部分
    • front end side lang - 前端通用的部分
  • feature lang
    • found
      • found.zh-cn.json - 发现模块 - 简体中文语言包
      • found.zh-tw.json - 发现模块 - 繁体中文(**)语言包
      • found.zh-hk.json - 发现模块 - 繁体中文(香港)语言包
      • found.en-ww.json - 发现模块 - 英文(全球)语言包
      • found.en-us.json - 发现模块 - 英文(美国)语言包

2.语言包内部结构设计

  • common

    // zh-cn.json
    {
    "TITLE": "欢迎使用职行力",
    "HOME": "首页",
    "LEARN": "学习",
    "FOUND": "发现",
    "MY": "我的",
    "CANCEL": "取消",
    "OK": "确认"
    }

    // en-ww.json
    {
    "TITLE": "Welcome To EXE APP",
    "HOME": "HOME",
    "LEARN": "LEARN",
    "FOUND": "FOUND",
    "MY": "MY",
    "CANCEL": "CANCEL",
    "OK": "OK"
    }

  • feature

    // found.zh-cn.json
    {
    "FOUND_PRE_POST_MSG": "帖子正在提交中" 或 "{{content}}正在提交中" - content: 帖子或评论
    }

    // found.en-ww.json
    {
    "FOUND_PRE_POST_MSG": "It is submitting."
    }

实现方案

利用已有的 ng2-translate 库实现多语言切换功能。

使用示例:

1.安装 ng2-translate

npm install ng2-translate --save

2.在 app.module.ts 中添加以下代码

import { Http } from '@angular/http';
import { TranslateModule, TranslateStaticLoader, TranslateLoader } from 'ng2-translate/ng2-translate';

export function createTranslateLoader(http: Http) {
  return new TranslateStaticLoader(http, './assets/i18n', '.json');
}

@NgModule({
  imports: [
    TranslateModule.forRoot({
      provide: TranslateLoader,
      useFactory: (createTranslateLoader),
      deps: [Http]
    })
  ]
})

3.在 src/assets/i18n 目录下添加语言包,如 en-ww.json 和 zh-cn.json 文件

3.1 en-ww.json

{
  "TITLE": "Welcome To EXE APP",
  "HINT": "Select your language",
  "HOME": "HOME",
  "LEARN": "LEARN",
  "FOUND": "FOUND",
  "MY": "MY",
  "CANCEL": "CANCEL",
  "OK": "OK"
}

3.2 zh-cn.json

{
    "TITLE": "欢迎使用职行力",
    "HINT": "请选择语言",
    "HOME": "首页",
    "LEARN": "学习",
    "FOUND": "发现",
    "MY": "我的",
    "CANCEL": "取消",
    "OK": "确认"
}

4.ng2-translate应用

4.1 TranslatePipe

<ion-title> {{ 'HELLO' | translate:param }} </ion-title>
param = {value: 'Dayana'};

4.2 TranslateService

import {TranslateService} from 'ng2-translate';

constructor(translate: TranslateService) {
    // 设置默认的语言包
    translate.setDefaultLang('en');
    // 切换语言包
    translate.use('en');
}

translate.get('HELLO', {value: 'Dayana'}).subscribe((res: string) => {
    console.log(res); //=> 'Hello Dayana'
});

4.3 TranslateDirective

<div [translate]="'HELLO'" [translateParams]="{value: 'Dayana'}"></div>
或
<div translate [translateParams]="{value: 'Dayana'}">HELLO</div>

5.示例

<ion-tabs>
  <ion-tab [root]="tab1Root" [tabTitle]="('HOME' | translate)" 
    tabIcon="home">
  </ion-tab>
  <ion-tab [root]="tab2Root" [tabTitle]="('LEARN' | translate)" 
    tabIcon="information-circle">
  </ion-tab>
  <ion-tab [root]="tab3Root" [tabTitle]="('FOUND' | translate)" 
    tabIcon="contacts">
  </ion-tab>
</ion-tabs>

打包方案

利用 npm scripts 提供的钩子,在运行 serve 或 build 任务前,合并各个目录下的语言包,统一输出至 src/assets/i18n 目录下。在开发阶段可以运行 npm run dev ,通过已注册的钩子 predev: gulp generate-lang-json,即调用 gulp generate-lang-json 任务生成语言包。

具体实现:

/**
 * 合并各个子目录下的语言包文件,生成独立的语言包
 */
var langJson = {};
gulp.task('generate-lang-json', function () {
  return gulp.src(['src/**/*' + i18nLang + '.json', '!src/assets/**/*.json'])
    .pipe(through2.obj(function (file, encoding, callback) {
      var originalContents = String(file.contents);
      var subLangJson;
      try {
        subLangJson = JSON.parse(originalContents);
        for (key in subLangJson)
        {
          if(langJson[key]){
            throw new Error('The key \''+langJson[key] 
              +' \'is repeat,file path:'+file.history);
          }else{
            langJson[key] = subLangJson[key] 
          }
        }
      } catch (e) {
        console.dir(e);
        throw new Error('Parse language file path failed');
      }
      file.contents = new Buffer(JSON.stringify(langJson));
      callback(null, file);
    }))
    .pipe(rename(i18nLang + '.json'))
    .pipe(gulp.dest('src/assets/i18n/'))
});

旧版本升级

公司现有的系统是采用 ionic 1.x 的版本开发,近期已经开始进行 ionic 2.x 的升级工作。因此需要抽取现有系统中的静态文本,然后对已有的文本进行分类。比如分为通用消息、功能模块内的消息。下面主要介绍一下,文本采集和处理思路。

  1. 使用正则匹配项目的JS文件(模板和业务逻辑文件)
  2. 对采集的文本进行去重处理
  3. 转成JavaScript对象,如代码段一
  4. 调用百度或其他翻译的API进行英文翻译,如代码段二
  5. 转换成标准的语言包

代码段一

{
  "签到表": "签到表",
  "讲师名单": "讲师名单",
  "助教名单": "助教名单",
  "全部评价": "全部评价",
  "线下课程详情": "线下课程详情",
  "培训地址": "培训地址",
  "全部培训": "全部培训",
  "混合培训": "混合培训",
  "线下培训": "线下培训",
  "在线培训": "在线培训",
  "报名中": "报名中",
  "预报名": "预报名"
}

代码段二

{
  '签到表': 'Attendance list',
  '讲师名单': 'lecturers',
  '助教名单': 'Teaching assistant list',
  '全部评价': 'All evaluation',
  '线下课程详情': 'Online course details',
  '培训地址': 'Training address',
  '全部培训': 'All training',
  '混合培训': 'Mixed training',
  '线下培训': 'Offline training',
  '在线培训': 'Online training',
  '报名中': 'Enrollment',
  '预报名': 'Forecast name'
}

参考资料

1.国家/地区 语言缩写代码

国家/地区 语言代码 国家/地区 语言代码
简体中文(**) zh-cn 繁体中文(**地区) zh-tw
繁体中文(香港) zh-hk 英语(香港) en-hk
英语(美国) en-us 英语(英国) en-gb
英语(全球) en-ww 英语(加拿大) en-ca
英语(澳大利亚) en-au 英语(爱尔兰) en-ie
英语(芬兰) en-fi 芬兰语(芬兰) fi-fi
英语(丹麦) en-dk 丹麦语(丹麦) da-dk
英语(以色列) en-il 希伯来语(以色列) he-il
英语(南非) en-za 英语(印度) en-in
英语(挪威) en-no 英语(新加坡) en-sg
英语(新西兰) en-nz 英语(印度尼西亚) en-id
英语(菲律宾) en-ph 英语(泰国) en-th
英语(马来西亚) en-my 英语(阿拉伯) en-xa
韩文(韩国) ko-kr 日语(日本) ja-jp
荷兰语(荷兰) nl-nl 荷兰语(比利时) nl-be
葡萄牙语(葡萄牙) pt-pt 葡萄牙语(巴西) pt-br
法语(法国) fr-fr 法语(卢森堡) fr-lu
法语(瑞士) fr-ch 法语(比利时) fr-be
法语(加拿大) fr-ca 西班牙语(拉丁美洲) es-la
西班牙语(西班牙) es-es 西班牙语(阿根廷) es-ar
西班牙语(美国) es-us 西班牙语(墨西哥) es-mx
西班牙语(哥伦比亚) es-co 西班牙语(波多黎各) es-pr
德语(德国) de-de 德语(奥地利) de-at
德语(瑞士) de-ch 俄语(俄罗斯) ru-ru
意大利语(意大利) it-it 希腊语(希腊) el-gr
挪威语(挪威) no-no 匈牙利语(匈牙利) hu-hu
土耳其语(土耳其) tr-tr 捷克语(捷克共和国) cs-cz
斯洛文尼亚语 sl-sl 波兰语(波兰) pl-pl
瑞典语(瑞典) sv-se 西班牙语 (智利) es-cl

2.参考文档

Angular 2 @Inject 详解

Inject 装饰器的作用

在 Angular 2 中,Inject 是参数装饰器,用来在类的构造函数中描述非 Type 类型的依赖对象。

Angular 2 中 Type 类型:

// Type类型 - @angular/core/src/type.ts
export const Type = Function;

export function isType(v: any): v is Type<any> {
  return typeof v === 'function';
}

export interface Type<T> extends Function { new (...args: any[]): T; }

Angular 2 中常用的非 Type 类型 Token:字符串、OpaqueToken对象、InjectionToken对象等。

/*
* 用于创建OpaqueToken实例
* export const CONFIG = new OpaqueToken('config');
*/
export class OpaqueToken {
  constructor(protected _desc: string) {}
  toString(): string { return `Token ${this._desc}`; }
}

/*
* 用于创建InjectionToken实例,使用泛型描述该Token所关联的依赖对象的类型
* const API_URL = new InjectionToken<string>('apiUrl'); 
*/
export class InjectionToken<T> extends OpaqueToken {
  private _differentiate_from_OpaqueToken_structurally: any;
  constructor(desc: string) { super(desc); }

  toString(): string { return `InjectionToken ${this._desc}`; }
}

(备注:各种 Token 类型的区别,请参照 Angular 2 OpaqueToken & InjectionToken)

Inject 装饰器的使用

config.ts

export const CONFIG = new OpaqueToken('config');

app.service.ts

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

@Injectable()
export class AppService {
    constructor() { }
}

app.component.ts

import { Component, Inject, ViewChild, HostListener, ElementRef } from '@angular/core';
import { CONFIG } from './config';
import { AppService } from './app.service';

@Component({
  selector: 'my-app',
  template: `<h1 #greet> Hello {{ name }} </h1>`,
})
export class AppComponent {
  name = 'Angular';

  @ViewChild('greet')
  private greetDiv: ElementRef;

  @HostListener('click', ['$event'])
  onClick($event: any) {
    console.dir($event);
  }

  constructor(public appService: AppService,
    @Inject(CONFIG) config: any) {
  }
}

编译后的 ES5 代码片段:

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {...};
var __metadata = (this && this.__metadata) || function (k, v) {
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function")
  return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
   return function (target, key) { decorator(target, key, paramIndex); }
};
  
var AppComponent = (function () {
  	// 构造函数
	function AppComponent(appService, config) {
        this.appService = appService;
        this.name = 'Angular';
    }
  
    AppComponent = __decorate([
        core_1.Component({ // 调用ComponentDecoratorFactory返回TypeDecorator
            selector: 'my-app',
            template: "<h1 #greet> Hello {{ name }} </h1>",
        }),
        // 调用ParamDecoratorFactory返回ParamDecorator
        __param(1, core_1.Inject(config_1.CONFIG)), 
        // 保存构造函数参数的类型
        __metadata('design:paramtypes', [app_service_1.AppService, Object])
    ], AppComponent);
    return AppComponent;
}());
exports.AppComponent = AppComponent;

Inject 装饰器实现

Inject 接口及 Inject 函数:

// Inject接口定义
export interface Inject { token: any; }

// InjectDecorator接口定义
export interface InjectDecorator {
  (token: any): any;
  new (token: any): Inject; // 构造函数的签名
}

// Inject装饰器:即示例中转成ES5代码后的 core_1.Inject 对象 - core_1.Inject(config_1.CONFIG)
export const Inject: InjectDecorator = makeParamDecorator('Inject', [['token', undefined]]);

makeParamDecorator函数片段:

 /*
 * 创建ParamDecorator工厂
 *
 * 调用 makeParamDecorator('Inject', [['token', undefined]])后返回ParamDecoratorFactory
 */
function makeParamDecorator(name, props, parentClass) {
  	    // name: 'Inject', props: [['token', undefined]]
  		// 创建Metadata构造函数
        var metaCtor = makeMetadataCtor(props);

 		// __param(1, core_1.Inject(config_1.CONFIG))
        function ParamDecoratorFactory() {
          // 解析参数并创建annotationInstance实例
            var args = [];
           // arguments: {0: CONFIG}
            for (var _i = 0; _i < arguments.length; _i++) {
                args[_i - 0] = arguments[_i];
            }
            if (this instanceof ParamDecoratorFactory) {
                // args: [CONFIG]
                metaCtor.apply(this, args);
                return this;
            }
            ... 
            return ParamDecorator;
      
            function ParamDecorator(cls, unusedKey, index) {
               // 获取类已经定义的metadata信息 
                var parameters = Reflect.getOwnMetadata('parameters', cls) || [];
                while (parameters.length <= index) {
                    parameters.push(null);
                }
                // parameters是一个二维数组,因为支持同时应用多个装饰器
                // eg:  @Inject(CONFIG) @Optional() @SkipSelf() config: any
                parameters[index] = parameters[index] || [];
                parameters[index].push(annotationInstance);
                Reflect.defineMetadata('parameters', parameters, cls);
                return cls;
            }
            var _a;
        }
        return ParamDecoratorFactory;
}

makeMetadataCtor 函数:

// 生成Metadata构造函数: var metaCtor = makeMetadataCtor(props); 
// props: [['token', undefined]]
  function makeMetadataCtor(props) {
        return function ctor() {
          /*
          * metaCtor.apply(this, args);
          */ 
            var _this = this;
            var args = [];
            for (var _i = 0; _i < arguments.length; _i++) {
                args[_i - 0] = arguments[_i];
            }
            props.forEach(function (prop, i) { // prop: ['token', undefined]
                var argVal = args[i]; 
                if (Array.isArray(prop)) { // prop: ['token', undefined]
                  	// prop[0]: token, argVal: CONFIG - {_desc: "config"}
                    _this[prop[0]] = argVal === undefined ? prop[1] : argVal;
                }
                else {
                    for (var propName in prop) {
                        _this[propName] =
                            argVal && argVal.hasOwnProperty(propName) ? 
                          argVal[propName] : prop[propName];
                    }
                }
            });
        };
}

接下来我们可以在控制台输入 window['core-js_shared'] ,查看通过 Reflect API 保存后的metadata信息

angular2-inject

最后我们来了解一下,Angular 如何获取 AppComponent 构造函数中,通过 @Inject 装饰器设置的 metadata信息。

// @angular/core/src/reflection/reflection_capabilities.ts
export class ReflectionCapabilities implements PlatformReflectionCapabilities {
  // 获取ParamDecorator函数中通过Reflect.defineMetadata('parameters', parameters, cls)
  // 保存的metadata信息
   parameters(type: Type<any>): any[][] {
    if (!isType(type)) { return []; }
    const parentCtor = getParentCtor(type);
    let parameters = this._ownParameters(type, parentCtor);
    if (!parameters && parentCtor !== Object) {
      parameters = this.parameters(parentCtor);
    }
    return parameters || [];
  }
}

private _ownParameters(type: Type<any>, parentCtor: any): any[][] {
  /* 
   * constructor(
   *  public appService: AppService,
   *  @Inject(CONFIG) config: any) {
   * }
   */
   if (this._reflect != null && this._reflect.getOwnMetadata != null) {
     // @Inject(CONFIG) config: any -> 'parameters'
      const paramAnnotations = this._reflect.getOwnMetadata('parameters', type);
     // appService: AppService -> 'design:paramtypes'
      const paramTypes = this._reflect.getOwnMetadata('design:paramtypes', type);
      if (paramTypes || paramAnnotations) {
        return this._zipTypesAndAnnotations(paramTypes, paramAnnotations);
      }
    }
}

我有话说

1.为什么在构造函数中,非 Type 类型的参数只能用 @Inject(Something) 的方式注入 ?

因为只有是 Type 类型的对象,才会被 TypeScript 编译器编译。具体参照下图:

angular2-inject-1

2.为什么 TypeScript 会自动保存 metadata 信息 ?

因为我们在 tsconfig.json 文件中,进行如下配置:

{
  "compilerOptions": {
  	...,
    "emitDecoratorMetadata": true
    }
 }   

3.AppService 中 @Injectable() 是必须的么 ?

如果 AppService 不依赖于其他对象,是可以不用使用 Injectable 类装饰器。当 AppService 需要在构造函数中注入依赖对象,就需要使用 Injectable 类装饰器。比较推荐的做法不管是否有依赖对象,service 中都使用 Injectable 类装饰器。

4.Reflect 对象还有哪些方法 ?

Reflect
  .defineMetadata(metadataKey, metadataValue, target, propertyKey?) -> void
  .getMetadata(metadataKey, target, propertyKey?) -> var
  .getOwnMetadata(metadataKey, target, propertyKey?) -> var
  .hasMetadata(metadataKey, target, propertyKey?) -> bool
  .hasOwnMetadata(metadataKey, target, propertyKey?) -> bool
  .deleteMetadata(metadataKey, target, propertyKey?) -> bool
  .getMetadataKeys(target, propertyKey?) -> array
  .getOwnMetadataKeys(target, propertyKey?) -> array
  .metadata(metadataKey, metadataValue) -> decorator(target, targetKey?) -> void

Reflect API 使用示例

var O = {};
Reflect.defineMetadata('foo', 'bar', O);
Reflect.ownKeys(O);               // => []
Reflect.getOwnMetadataKeys(O);    // => ['foo']
Reflect.getOwnMetadata('foo', O); // => 'bar'

5.使用 Reflect API 有什么好处 ?

  • 使用 Reflect API 我们能够方便的对类相关的 metadata 信息进行保存和读取
  • Reflect API 把类相关的 metadata 信息保存在 window['core-js_shared'] 对象中,避免对类造成污染。

总结

本文通过一个示例,一步步分析 Inject 装饰器的作用及内部实现原理。最终我们分析了,Inject 装饰器和 Injectable 的应用场景。我们已经知道 metadata 信息如何保存,保存到哪里,后续会有专门的文章介绍如何读取和利用已保存的 metadata 信息。

Angular 2 ElementRef

Angular 2 的口号是 - "一套框架,多种平台。同时适用手机与桌面(One framework.Mobile & desktop.)",即 Angular 2 是支持开发跨平台的应用,比如:Web应用、移动Web应用、原生移动应用和原生桌面应用等。

为了能够支持跨平台,Angular 2 通过抽象层封装了不同平台的差异,统一了 API 接口。如定义了抽象类 Renderer 、抽象类 RootRenderer 等。此外还定义了以下引用类型:ElementRef、TemplateRef、ViewRef 、ComponentRef 和 ViewContainerRef 等。下面我们就来分析一下 ElementRef 类:

ElementRef 的作用

在应用层直接操作 DOM,就会造成应用层与渲染层之间强耦合,导致我们的应用无法运行在不同环境,如 web worker 中,因为在 web worker 环境中,是不能直接操作 DOM。有兴趣的读者,可以阅读一下 Web Workers 中支持的类和方法 这篇文章。通过 ElementRef 我们就可以封装不同平台下视图层中的 native 元素 (在浏览器环境中,native 元素通常是指 DOM 元素),最后借助于 Angular 2 提供的强大的依赖注入特性,我们就可以轻松地访问到 native 元素。

ElementRef 的定义

export class ElementRef {
  public nativeElement: any;
  constructor(nativeElement: any) { this.nativeElement = nativeElement; }
}

ElementRef 的应用

我们先来介绍一下整体需求,我们想在页面成功渲染后,获取页面中的 div 元素,并改变该 div 元素的背景颜色。接下来我们来一步步,实现这个需求。

首先我们要先获取 div 元素,在文中 "ElementRef 的作用" 部分,我们已经提到可以利用 Angular 2 提供的强大的依赖注入特性,获取封装后的 native 元素。在浏览器中 native 元素就是 DOM 元素,我们只要先获取 myapp元素,然后利用 querySelector API 就能获取页面中 div 元素。具体代码如下:

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

@Component({
  selector: 'my-app',
  template: `
    <h1>Welcome to Angular World</h1>
    <div>Hello {{ name }}</div>
  `,
})
export class AppComponent {

  name: string = 'Semlinker';

  constructor(private elementRef: ElementRef) {
    let divEle = this.elementRef.nativeElement.querySelector('div');
    console.dir(divEle);
  }
}

运行上面代码,在控制台中没有出现异常,但是输出的结果却是 null 。什么情况 ? 没有抛出异常,我们可以推断 this.elementRef.nativeElement 这个对象是存在,但却找不到它的子元素,那应该是在调用构造函数的时候,my-app 元素下的子元素还未创建。那怎么解决这个问题呢 ?沉思中… ,不是有 setTimeout 么,我们在稍微改造一下:

constructor(private elementRef: ElementRef) {
  setTimeout(() => { // 此处需要使用箭头函数哈,你懂的...
      let divEle = this.elementRef.nativeElement.querySelector('div');
      console.dir(divEle);
   }, 0);
}

更新一下代码,此时控制台成功输出了 div 。为什么添加个 setTimeout 就能成功获取到想要的 div 元素呢?此处就不展开了,有兴趣的读者可以参考 - What the heck is the event loop anyway? 这个演讲的示例。

问题解决了,但感觉不是很优雅 ?有没有更好的方案,答案是肯定的。Angular 2 不是有提供组件生命周期的钩子,我们可以选择一个合适的时机,然后获取我们想要的 div 元素。

import { Component, ElementRef, AfterViewInit } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>Welcome to Angular World</h1>
    <div>Hello {{ name }}</div>
  `,
})
export class AppComponent {

  name: string = 'Semlinker';

  // 在构造函数中 this.elementRef = elementRef 是可选的,编译时会自动赋值
  // function AppComponent(elementRef) { this.elementRef = elementRef; }
  constructor(private elementRef: ElementRef) { } 

  ngAfterViewInit() { // 模板中的元素已创建完成
    console.dir(this.elementRef.nativeElement.querySelector('div'));
    // let greetDiv: HTMLElement = this.elementRef.nativeElement.querySelector('div'); 
    // greetDiv.style.backgroundColor = 'red';
  }
}

运行一下上面的代码,我们看到了意料中的 div 元素。我们直接选用 ngAfterViewInit 这个钩子,不要问我为什么,因为它看得最顺眼咯。不过我们后面也会有专门的文章,详细分析一下 Angular 2 组件的生命周期。成功取到 div 元素,就剩下的事情就好办了,直接通过 style 对象设置元素的背景颜色。

功能虽然已经实现了,但还有优化的空间么?在 Angular 2 Decorators part - 2 文章中我们有谈到 Angular 2 内置的属性装饰器,如 @ContentChild@ContentChildren@ViewChild@ViewChildren 等。相信读者看完后,已经猜到我们的优化方案了。具体示例如下:

import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>Welcome to Angular World</h1>
    <div #greet>Hello {{ name }}</div>
  `,
})
export class AppComponent {
  name: string = 'Semlinker';

  @ViewChild('greet')
  greetDiv: ElementRef;

  constructor(private elementRef: ElementRef) { }

  ngAfterViewInit() {
    this.greetDiv.nativeElement.style.backgroundColor  = 'red';
  }
}

是不是感觉瞬间高大上了,不过先等等,上面的代码是不是还有进一步的优化空间呢 ?我们看到设置 div 元素的背景,我们是默认应用的运行环境在是浏览器中。前面已经介绍了,我们要尽量减少应用层与渲染层之间强耦合关系,从而让我们应用能够灵活地运行在不同环境。最后我们来看一下,最终优化后的代码:

import { Component, ElementRef, ViewChild, AfterViewInit, Renderer } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>Welcome to Angular World</h1>
    <div #greet>Hello {{ name }}</div>
  `,
})
export class AppComponent {
  name: string = 'Semlinker';

  @ViewChild('greet')
  greetDiv: ElementRef;

  constructor(private elementRef: ElementRef, private renderer: Renderer) { }

  ngAfterViewInit() {
    // this.greetDiv.nativeElement.style.backgroundColor  = 'red';
    this.renderer.setElementStyle(this.greetDiv.nativeElement, 'backgroundColor', 'red');
  }
}

总结

本文主要介绍了 ElementRef 的作用和定义,然后通过一个简单的示例,展示了如何一步步优化已有的功能,希望对初学者能有所启发。

Angular 2 Provider

Angular 2 Provider

依赖注入(DI) 是 Angular 2 的核心,在深入了解DI的工作原理之前,我们必须先搞清楚 Provider 的概念。

在 Angular 2 中我们使用 Provider 来描述与 Token 关联的依赖对象的创建方式。Angular 2 中依赖对象的创建方式有四种,它们分别是:

  • useClass
  • useValue
  • useExisting
  • useFactory

useClass

@Injectable()
export class ApiService {
   constructor(
      public http: Http, 
      public loadingCtrl: LoadingController) {
   }
   ...
}

@NgModule({
  ...
  providers: [
   	{ provide: ApiService, useClass: ApiService } // 可使用简洁的语法,即直接使用ApiService
  ]
})
export class CoreModule { }

useValue

{ provide: 'API_URL', useValue: 'http://my.api.com/v1' }

useExisting

{ provide: 'ApiServiceAlias', useExisting: ApiService }

useFactory

export function configFactory(config: AppConfig) {
  return () => config.load();
}

@NgModule({
  ...
  providers: [
   	{ provide: APP_INITIALIZER, useFactory: configFactory, 
  	  deps: [AppConfig], multi: true }
  ]
})
export class CoreModule { }

使用 Provider 的正确姿势

1.创建 Token

Token 的作用是用来标识依赖对象,Token值可能是 Type、InjectionToken、OpaqueToken 类的实例或字符串。通常不推荐使用字符串,因为如果使用字符串存在命名冲突的可能性比较高。在 Angular 4.x 以前的版本我们一般使用 OpaqueToken 来创建 Token,而在 Angular 4.x 以上的版本版本,推荐使用 InjectionToken 来创建 Token 。详细的内容可以参考, 如何解决 Angular 2 中 Token 命名冲突

2.根据实际需求选择依赖对象的创建方式,如 useClass 、useValue、useExisting、useFactory

3.在 NgModule 或 Component 中注册 providers

4.使用构造注入的方式,注入与 Token 关联的依赖对象

/**
* 封装Http服务,如在每个Http的请求头中添加token,类似于Ng1.x中的拦截器
*/
@Injectable() 
export class ApiService {
   constructor(
  	// 注入Angular 2 中的Http服务,与Ng1.x的区别:
   // 在Ng1.x中调用Http服务后,返回Promise对象
   // 在Ng2.x中调用Http服务后,返回Observable对象
      public http: Http) { 
   }
   ...
}

/**
* AppModule
*/
@NgModule({
  ...
  providers: [
   	{ provide: ApiService, useClass: ApiService } // 可使用简洁的语法,即直接使用ApiService
  ]
})
export class AppModule { }

/**
* 系统首页
*/
@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {
  constructor(
    public apiService: ApiService // 使用构造注入的方式,注入ApiService的实例对象
  ) { }
  
  ngOnInit(): void {
    this.apiService.get(HOME_URL) // 获取首页相关的数据
    .map(res => res.json()) // 返回的res对象是Response类型的实例
    .subscribe(result => {
      ...
    })
  }
}

我有话说

1.当DI解析 Providers 时,都会对提供的每个 provider 进行规范化处理,即转换成标准的形式。

function _normalizeProviders(providers: Provider[], res: Provider[]): Provider[] {
  providers.forEach(b => {
    if (b instanceof Type) { // 支持简洁的语法,转换为标准格式
      res.push({provide: b, useClass: b});
    } else if (b && typeof b == 'object' && (b as any).provide !== undefined) {
      res.push(b as NormalizedProvider);
    } else if (b instanceof Array) {
      _normalizeProviders(b, res); // 如果是数组,进行递归处理
    } else {
      throw invalidProviderError(b);
    }
  });
  return res;
}

2.创建 Token 时为了避免命名冲突,尽量避免使用字符串作为Token。

3.若要创建模块内通用的依赖对象,需要在 NgModule 中注册相关的 provider,若在每个组件中,都有唯一的依赖对象,就需要在 Component 中注册相关的 provider。

4.multi providers 的具体作用,具体请参考 - Angular2 Multi Providers

5.Provider 是用来描述与 Token 关联的依赖对象的创建方式。当我们使用 Token 向 DI 系统获取与之相关连的依赖对象时,DI 会根据已设置的创建方式,自动的创建依赖对象并返回给使用者。

Provider接口

export interface ClassProvider {
  // 用于设置与依赖对象关联的Token值,Token值可能是Type、InjectionToken、OpaqueToken的实例或字符串
  provide: any; 
  useClass: Type<any>;
  // 用于标识是否multiple providers,若是multiple类型,则返回与Token关联的依赖对象列表
  multi?: boolean; 
}
  
export interface ValueProvider {
  provide: any;
  useValue: any;
  multi?: boolean;
}
  
export interface ExistingProvider {
  provide: any;
  useExisting: any;
  multi?: boolean;
}
  
export interface FactoryProvider {
  provide: any;
  useFactory: Function;
  deps?: any[]; // 用于设置工厂函数的依赖对象
  multi?: boolean;
}

总结

在文章的最后,想举一个现实生活中的例子,帮助初学者更好地理解 Angular 2 DI 和 Provider。

Provider 中的 token 可以理解为菜名,useClass、useValue可以理解为菜的烹饪方式,而依赖对象就是我们所点的菜,而 DI 系统就是我们的厨师了。如果没有厨师,我们就得关心煮这道菜需要哪些原材料,怎么煮菜,重要的是还得自己煮,可想而知多麻烦。而有了厨师(DI),我们只要在菜谱上点菜,必要时备注一下烹饪方式,不过多久香喷喷的菜就上桌鸟~~~。

Ionic 2 中如何引入第三方脚本库

一、typings 中包含 Type Definitions 文件

1.使用 npm 安装依赖

npm install bcryptjs --save

2.添加 Type Definitions 文件阻止编译器错误

2.1 安装 typings

npm install -g typings

2.2 安装 bcryptjs dt 文件

typings install dt~bcryptjs --save --global

二、 typings 中不包含Type Definitions 文件

index.html 文件中引入对应的文件:

<script src="bcrypt.min.js"></script>

在使用的 ts 文件中声明变量:

declare var dcodeIO: any;

export class HomePage {
    public bcryptjs: any;
    public hash: string;
    
    constructor() {
       this.bcryptjs = dcodeIO.bcrypt;
       this.hash = this.bcryptjs.hashSync("nraboy", 8);
    }
}

Angular 2 模板语法与常用指令简介

一、模板语法简介

插值表达式

<div>Hello {{name}}</div>

等价于

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

模板表达式

1.属性绑定

1.1输入属性的值为常量

<show-title title="Some Title"></show-title>

等价于

<show-title [title]="'Some Title'"></show-title>

1.2输入属性的值为实例属性

<show-title [title]="title"></show-title>

等价于

<show-title bind-title="title"></show-title>

2.事件绑定

<date-picker (dateChanged)="statement()"></date-picker>

等价于

<date-picker on-dateChanged="statement()"></date-picker>

模板引用变量

<video-player #player></video-player> 
<button (click)="player.pause()">Pause</button>

等价于

<video-player ref-player></video-player>

双向绑定

<input [ngModel]="todo.text" (ngModelChange)="todo.text=$event">

等价于

<input [(ngModel)]="todo.text"> 

*与template

1.*ngIf

<hero-detail *ngIf="currentHero" [hero]="currentHero"></hero-detail>

最终转换为

<template [ngIf]="currentHero">
  <hero-detail [hero]="currentHero"></hero-detail>
</template>

2.*ngFor

<hero-detail *ngFor="let hero of heroes; trackBy:trackByHeroes" 
    [hero]="hero">
</hero-detail>

最终转换为

<template ngFor let-hero [ngForOf]="heroes" [ngForTrackBy]="trackByHeroes">
  <hero-detail [hero]="hero"></hero-detail>
</template>

常用指令简介

NgIf

<div *ngIf="false"></div> <!-- never displayed -->
<div *ngIf="a > b"></div> <!-- displayed if a is more than b -->
<div *ngIf="str == 'yes'"></div> <!-- displayed if str holds the string "yes" -->
<div *ngIf="myFunc()"></div> <!-- displayed if myFunc returns a true value -->

NgSwitch

有时候需要根据不同的条件,渲染不同的元素,此时我们可以使用多个 ngIf 来实现。

<div class="container">
	<div *ngIf="myVar == 'A'">Var is A</div>
	<div *ngIf="myVar == 'B'">Var is B</div>
	<div *ngIf="myVar != 'A' && myVar != 'B'">Var is something else</div>
</div>	

如果 myVar 的可选值多了一个 'C',就得相应增加判断逻辑:

<div class="container">
	<div *ngIf="myVar == 'A'">Var is A</div>
	<div *ngIf="myVar == 'B'">Var is B</div>
	<div *ngIf="myVar == 'C'">Var is C</div>
	<div *ngIf="myVar != 'A' && myVar != 'B' && myVar != 'C'">
      Var is something else
  	</div>
</div>

可以发现 Var is something else 的判断逻辑,会随着 myVar 可选值的新增,变得越来越复杂。遇到这种情景,我们可以使用 ngSwitch 指令。

<div class="container" [ngSwitch]="myVar">
	<div *ngSwitchCase="'A'">Var is A</div>
	<div *ngSwitchCase="'B'">Var is B</div>
	<div *ngSwitchCase="'C'">Var is C</div>
	<div *ngSwitchDefault>Var is something else</div>
</div>

NgStyle

NgStyle 让我们可以方便得通过 Angular 表达式,设置 DOM 元素的 CSS 属性。

  • 设置元素的背景颜色

    Use fixed yellow background
  • 设置元素的字体大小

    red text

NgStyle 支持通过键值对的形式设置 DOM 元素的样式:

<div [ngStyle]="{color: 'white', 'background-color': 'blue'}">
   Uses fixed white text on blue background
</div>

注意到 background-color 需要使用单引号,而 color 不需要。这其中的原因是,ng-style 要求的参数是一个 Javascript 对象,color 是一个有效的 key,而 background-color 不是一个有效的 key ,所以需要添加 ''。

NgStyle 源码片段

@Directive({selector: '[ngStyle]'})
export class NgStyle implements DoCheck {
  private _ngStyle: {[key: string]: string};
  private _differ: KeyValueDiffer<string, string|number>;

  constructor(
    private _differs: KeyValueDiffers, 
    private _ngEl: ElementRef, 
    private _renderer: Renderer) {}

  @Input()
  set ngStyle(v: {[key: string]: string}) { 
    // <div [ngStyle]="{color: 'white', 'background-color': 'blue'}">
    this._ngStyle = v;
    if (!this._differ && v) {
      this._differ = this._differs.find(v).create();
    }
  }
 
  // 设置元素的样式
  private _setStyle(nameAndUnit: string, value: string|number): void {
    const [name, unit] = nameAndUnit.split('.'); // 截取样式名和单位
    value = value != null && unit ? `${value}${unit}` : value;

    this._renderer.setElementStyle(this._ngEl.nativeElement, name, value as string);
  }
}

NgClass

NgClass 接收一个对象字面量,对象的 key 是 CSS class 的名称,value 的值是 truthy/falsy 的值,表示是否应用该样式。

CSS Class

.bordered {
  border: 1px dashed black; background-color: #eee;
}

HTML

<!-- Use boolean value -->
<div [ngClass]="{bordered: false}">This is never bordered</div>
<div [ngClass]="{bordered: true}">This is always bordered</div>

<!-- Use component instance property -->
<div [ngClass]="{bordered: isBordered}">
   Using object literal. Border {{ isBordered ? "ON" : "OFF" }}
</div>

<!-- Class names contains dashes -->
<div[ngClass]="{'bordered-box': false}">
   Class names contains dashes must use single quote
</div>

<!-- Use a list of class names -->
<div class="base" [ngClass]="['blue', 'round']"> 
  This will always have a blue background and round corners
</div>

NgFor

NgFor 指令用来根据集合(数组) ,创建 DOM 元素,类似于 ng1 中 ng-repeat 指令

<div class="ui list" *ngFor="let c of cities; let num = index"> 
  <div class="item">{{ num+1 }} - {{ c }}</div>
</div>

使用 trackBy 提高列表的性能

@Component({
  selector: 'my-app',
  template: `
    <ul>
      <li *ngFor="let item of collection;trackBy: trackByFn">{{item.id}}</li>
    </ul>
    <button (click)="getItems()">Refresh items</button>
  `,
})
export class App {

  constructor() {
    this.collection = [{id: 1}, {id: 2}, {id: 3}];
  }
  
  getItems() {
    this.collection = this.getItemsFromServer();
  }
  
  getItemsFromServer() {
    return [{id: 1}, {id: 2}, {id: 3}, {id: 4}];
  }
  
  trackByFn(index, item) {
    return index; // or item.id
  }
}

NgNonBindable

ngNonBindable 指令用于告诉 Angular 编译器,无需编译页面中某个特定的HTML代码片段。

<div class='ngNonBindableDemo'>
    <span class="bordered">{{ content }}</span>
    <span class="pre" ngNonBindable>
      &larr; This is what {{ content }} rendered
    </span>
</div>

注意事项

1.使用 [hidden] 属性控制元素的可见性

<div [hidden]="!showGreeting">
  Hello, there!
</div>

上面的代码在通常情况下,都能正常工作。但当在对应的 DOM 元素上设置 display: flex 属性时,尽管[hidden] 对应的表达式为 true,但元素却能正常显示。对于这种特殊情况,则推荐使用 *ngIf

2.直接使用 DOM API 获取页面上的元素

@Component({
  selector: 'my-comp',
  template: `
    <input type="text" />
    <div> Some other content </div>
  `
})
export class MyComp {
  constructor(el: ElementRef) {
    el.nativeElement.querySelector('input').focus();
  }
}

以上的代码直接通过 querySelector() 获取页面中的元素,通常不推荐使用这种方式。更好的方案是使用 @ViewChild 和模板变量,具体示例如下:

@Component({
  selector: 'my-comp',
  template: `
    <input #myInput type="text" />
    <div> Some other content </div>
  `
})
export class MyComp implements AfterViewInit {
  @ViewChild('myInput') input: ElementRef;

  constructor(private renderer: Renderer) {}

  ngAfterViewInit() {
    this.renderer.invokeElementMethod(
        this.input.nativeElement, 'focus');
    }
}

另外值得注意的是,@ViewChild() 属性装饰器,还支持设置返回对象的类型,具体使用方式如下:

@ViewChild('myInput') myInput1: ElementRef;
@ViewChild('myInput', {read: ViewContainerRef}) myInput2: ViewContainerRef;

若未设置 read 属性,则默认返回的是 ElementRef 对象实例。

Angular 2 forwardRef的作用

Angular 2 通过引入 forwardRef 让我们可以在使用构造注入时,可以使用尚未定义的依赖对象类型。下面我们先看一下如果没有使用 forwardRef ,在开发中可能会遇到的问题:

@Injectable()
class Socket {
  constructor(private buffer: Buffer) { }
}

console.log(Buffer); // undefined

@Injectable()
class Buffer {
  constructor(@Inject(BUFFER_SIZE) private size: Number) { }
}

console.log(Buffer); // [Function: Buffer]

若运行上面的例子,将会抛出以下异常:

Error: Cannot resolve all parameters for Socket(undefined).
Make sure they all have valid type or annotations

为什么会出现这个问题 ?在探究产生问题的具体原因时,我们要先明白一点。不管我们是使用开发语言是 ES6、ES7 还是 TypeScript,最终我们都得转换成 ES5 的代码。然而在 ES5 中是没有 Class ,只有 Function 对象。这样一来,我们的解决问题的思路就是先看一下 Socket 类转换后的 ES5 代码:

var Buffer = (function () {
    function Buffer(size) {
        this.size = size;
    }
    return Buffer;
}());

我们发现 Buffer 类最终转成 ES5 中的函数表达式。我们也知道,JavaScript VM 在执行 JS 代码时,会有两个步骤,首先会先进行编译,然后才开始执行。编译阶段,变量声明和函数声明会自动提升,而函数表达式不会自动提升。了解完这些后,问题原因一下子明朗了。

那么要解决上面的问题,最简单的处理方式是交换类定义的顺序。除此之外,我们还可以使用 Angular2 提供的 forward reference 特性来解决问题,具体如下:

import { forwardRef } from'@angular2/core';

@Injectable()
class Socket {
  constructor(@Inject(forwardRef(() => Buffer)) 
  	private buffer) { }
}

class Buffer {
  constructor(@Inject(BUFFER_SIZE) private size: Number) { }
}

问题来了,出现上面的问题,我交互个顺序不就完了,为什么还要如此大费周章 ?话虽如此,但这样增加了开发者的负担,要时刻警惕类定义的顺序,特别当一个 ts 文件内包含多个内部类的时候。所以更好地方式还是通过 forwardRef 来解决问题,下面我们就来进一步揭开 forwardRef 的神秘面纱。

forwardRef 原理分析

// @angular/core/src/di/forward_ref.ts

/**
 * Allows to refer to references which are not yet defined.
 */
export function forwardRef(forwardRefFn: ForwardRefFn): Type<any> {
  // forwardRefFn: () => Buffer
  (<any>forwardRefFn).__forward_ref__ = forwardRef;
  (<any>forwardRefFn).toString = function() { return stringify(this()); };
  return (<Type<any>><any>forwardRefFn);
}

/**
 * Lazily retrieves the reference value from a forwardRef.
 */
export function resolveForwardRef(type: any): any {
  if (typeof type === 'function' && type.hasOwnProperty('__forward_ref__') &&
      type.__forward_ref__ === forwardRef) {
    return (<ForwardRefFn>type)(); // Call forwardRefFn get Buffer 
  } else {
    return type;
  }
}

通过源码可以看出,当调用 forwardRef 方法时,我们只是在 forwardRefFn 函数对象上,增加了一个私有属性__forward_ref__,同时覆写了函数对象的 toString 方法。在上面代码中,我们还发现了resolveForwardRef 函数,通过函数名和注释信息,我们很清楚地了解到,该函数是用来解析通过 forwardRef 包装过的引用值。

那么 resolveForwardRef 这个函数是由谁负责调用,又是什么时候调用呢 ?其实 resolveForwardRef 这个函数由 Angular 2 的依赖注入系统调用,当解析 Provider 和创建依赖对象的时候,会自动调用该函数。

// @angular/core/src/di/reflective_provider.ts

/**
 * 解析Provider
 */
function resolveReflectiveFactory(provider: NormalizedProvider): ResolvedReflectiveFactory {
  let factoryFn: Function;
  let resolvedDeps: ReflectiveDependency[];
  ...
  if (provider.useClass) {
    const useClass = resolveForwardRef(provider.useClass);
    factoryFn = reflector.factory(useClass);
    resolvedDeps = _dependenciesFor(useClass);
  }
}

/***************************************************************************************/

/**
 * 构造依赖对象
 */
export function constructDependencies(
    typeOrFunc: any, dependencies: any[]): ReflectiveDependency[] {
  if (!dependencies) {
    return _dependenciesFor(typeOrFunc);
  } else {
    const params: any[][] = dependencies.map(t => [t]);
    return dependencies.map(t => _extractToken(typeOrFunc, t, params));
  }
}

/**
 * 抽取Token
 */
function _extractToken(
  typeOrFunc: any, metadata: any[] | any, params: any[][]): ReflectiveDependency {
    
  token = resolveForwardRef(token);
  if (token != null) {
    return _createDependency(token, optional, visibility);
  } else {
    throw noAnnotationError(typeOrFunc, params);
  }
}

我有话说

1.为什么 JavaScript 解释器不自动提升 class ?

因为当 class 使用 extends 关键字实现继承的时候,我们不能确保所继承父类的有效性,那么就可能导致一些无法预知的行为。

class Dog extends Animal {}

function Animal {
  this.move = function() {
    alert(defaultMove);
  }
}

let defaultMove = "moving";

let dog = new Dog();
dog.move();

以上代码能够正常的输出 moving,因为 JavaScript 解释器把会把代码转化为:

let defaultMove,dog;

function Animal {
  this.move = function() {
    alert(defaultMove);
  }
}

class Dog extends Animal { }

defaultMove = "moving";

dog = new Dog();
dog.move();

然而,当我们把 Animal 转化为函数表达式,而不是函数声明的时候:

class Dog extends Animal {}

let Animal = function () {
  this.move = function () {
    alert(defaultMove);
  }
}

let defaultMove = "moving";

let dog = new Dog();
dog.move();

此时以上代码将会转化为:

let Animal, defaultMove, dog;

class Dog extends Animal { }

Animal = function () {
  this.move = function () {
    alert(defaultMove);
  }
}

defaultMove = "moving";

dog = new Dog();
dog.move();

当 class Dog extends Animal 被解释执行的时候,此时 Animal 的值是 undefined,这样就会抛出异常。我们可以简单地通过调整 Animal 函数表达式的位置,来解决上述问题。

let Animal = function () {
  this.move = function () {
    alert(defaultMove);
  }
}

class Dog extends Animal{

}

let defaultMove = "moving";

let dog = new Dog();
dog.move();

假设 class 也会自动提升的话,上面的代码将被转化为以下代码:

let Animal, defaultMove, dog;

class Dog extends Animal{ }

Animal = function () {
  this.move = function () {
    alert(defaultMove);
  }
}

defaultMove = "moving";

dog = new Dog();
dog.move();

此时 Dog 被提升了,当解释器执行 extends Animal 语句的时候,此时的 Animal 仍然是 undefined,同样会抛出异常。所以 ES6 中的 Class 不会自动提升,主要还是为了解决继承父类时,父类不可用的问题。

Angular 2 Multi Providers

Multi providers 让我们可以使用相同的 Token 去注册多个 Provider ,具体如下:

const SOME_TOKEN: OpaqueToken = new OpaqueToken('SomeToken');

var injector = ReflectiveInjector.resolveAndCreate([
  provide(SOME_TOKEN, {useValue: 'dependency one', multi: true}),
  provide(SOME_TOKEN, {useValue: 'dependency two', multi: true})
]);

// dependencies == ['dependency one', 'dependency two']
var dependencies = injector.get(SOME_TOKEN);

上面例子中,我们使用 multi: true 告诉 Angular 2的依赖注入系统,我们设置的 provider 是 multi provider。正如之前所说,我们可以使用相同的 token 值,注册不同的 provide。当我们使用对应的 token 去获取依赖项时,我们获取的是已注册的依赖对象列表。

为什么 Angular 2 中会引入 multi provider ?

首先我们先来分析一下,若没有设置 multi: true 属性时,使用同一个 token 注册 provider 时,会出现什么问题 ?

class Engine { }
class TurboEngine { }

var injector = ReflectiveInjector.resolveAndCreate([
  provide(Engine, {useClass: Engine}),
  provide(Engine, {useClass: TurboEngine})
]);

var engine = injector.get(Engine); // engine instanceof TurboEngine == true

这说明如果使用同一个 token 注册 provider,后面注册的 provider 将会覆盖前面已注册的 provider。此外,Angular 2 使用 multi provider 的这种机制,为我们提供可插拔的钩子(pluggable hooks) 。另外需要注意的是,multi provider是不能和普通的 provider 混用。

Angular 2 框架中 multi provider 的应用

1.NG_VALIDATORS

该 Token 用于配置自定义验证器 Provider

 @Directive({
  selector: '[customValidator][ngModel]',
  providers: [
    provide: NG_VALIDATORS,
    useValue: (formControl) => {
      // validation happens here
    },
    multi: true
  ]
})
class CustomValidator {}

以上是我们自定义的表单验证器,为了能够正常工作,我们必须在指令的 providers 数组中,使用 NG_VALIDATORS 注册相应的 provider。

2.APP_INITIALIZER

该 Token 用于配置系统初始化相关的 Provider

// exe-app-v2/src/core/core_module.ts
export function configFactory(config: AppConfig) {
    return function () { config.load(); }
}

@NgModule({
  	...,
    providers: [
  	  // 系统启动时,加载项目的配置文件,如系统登录、首页模块的ApiUrl等信息
      { provide: APP_INITIALIZER, useFactory: configFactory, 
          deps: [AppConfig], multi: true }
    ]
})
export class CoreModule { }

APP_INITIALIZER 详解

1.APP_INITIALIZER 的定义

// 使用 InjectionToken<T> 的方式声明,APP_INITIALIZER关联的对象是数组,数组内的元素是函数对象
export const APP_INITIALIZER = new InjectionToken<Array<() => void>>
('Application Initializer');

2.注册 APP_INITIALIZER 关联的 Provider

// @angular/core/src/application_module.ts
@NgModule({
  providers: [
    {provide: APP_INITIALIZER, useValue: _initViewEngine, multi: true},
  ]
})
export class ApplicationModule {
}

3.APP_INITIALIZER 在系统中的应用

/**
* 用于反应 APP_INITIALIZER 初始化函数的执行状态
*/
@Injectable()
export class ApplicationInitStatus {
  private _donePromise: Promise<any>;
  private _done = false; // 标识是否完成初始化

  // 在构造函数中注入 APP_INITIALIZER,关联的依赖对象
  constructor(@Inject(APP_INITIALIZER) @Optional() appInits: (() => any)[]) {
    const asyncInitPromises: Promise<any>[] = [];
    if (appInits) {
      // 循环调用已注册的初始化函数
      for (let i = 0; i < appInits.length; i++) {
        const initResult = appInits[i]();
        // 验证初始化函数的调用结果是否为Promise对象,若是则添加至异步队列
        if (isPromise(initResult)) {
          asyncInitPromises.push(initResult);
        }
      }
    }
    this._donePromise = Promise.all(asyncInitPromises).then(() => { this._done = true; });
    if (asyncInitPromises.length === 0) { // 不包含异步任务
      this._done = true;
    }
  }

  get done(): boolean { return this._done; }

  get donePromise(): Promise<any> { return this._donePromise; }
}

// 启动ModuleFactory
bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>): Promise<NgModuleRef<M>> {
    return this._bootstrapModuleFactoryWithZone(moduleFactory, null);
}
// 在新创建的zone中,启动ModuleFactory
private _bootstrapModuleFactoryWithZone<M>(moduleFactory: NgModuleFactory<M>, 
  ngZone: NgZone): Promise<NgModuleRef<M>> {  
    return _callAndReportToErrorHandler(exceptionHandler, () => {
  	  // 获取ApplicationInitStatus关联的依赖对象
      const initStatus: ApplicationInitStatus = 
  		moduleRef.injector.get(ApplicationInitStatus); 
  		// initStatus.donePromise = Promise.all(asyncInitPromises)
        // .then(() => { this._done = true; });
        return initStatus.donePromise.then(() => {
          this._moduleDoBootstrap(moduleRef);
          return moduleRef;
        });
      });
}

参考资料

Angular 2 Decorators part - 2

Angular 2 Decorators - part 1 文章中,我们介绍了 TypeScript 中的四种装饰器。本文的主要目的是介绍 Angular 2 中常见的内置装饰器。Angular 2 内置装饰器分类:

Angular 2 类装饰器示例:

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

@Component({
  selector: 'example-component',
  template: '<div>Woo a component!</div>'
})
export class ExampleComponent {
  constructor() {
    console.log('Hey I am a component!');
  }
}

Angular 2 属性装饰器示例:

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

@Component({
  selector: 'example-component',
  template: '<div>Woo a component!</div>'
})
export class ExampleComponent {
  @Input()
  exampleProperty: string;
}

Angular 2 方法装饰器示例:

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

@Component({
  selector: 'example-component',
  template: '<div>Woo a component!</div>'
})
export class ExampleComponent {
  @HostListener('click', ['$event'])
  onHostClick(event: Event) {
    // clicked, `event` available
  }
}

Angular 2 参数装饰器示例:

import { Component, Inject } from '@angular/core';
import { MyService } from './my-service';

@Component({
  selector: 'example-component',
  template: '<div>Woo a component!</div>'
})
export class ExampleComponent {
  constructor(@Inject(MyService) myService) { // 与myService: MyService等价
    console.log(myService);
  }
}

下面我们就着重分析一下最常用的类装饰器 @component ,其它的装饰器读者有兴趣的话,可以参考 Component 的分析思路自行分析。

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

@Component({
  selector: 'my-app', 
  template: `<h1>Hello {{name}}</h1>`, 
})
export class AppComponent  {
  name = 'Angular'; 
}

首先从最简单的例子入手,我们都知道采用 TypeScript 开发,为了保证兼容性最终都会转换成标准的 ES 5代码。上面的例子转成如下的代码:

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
  ...
};

define(["require", "exports", "@angular/core"], function (require, exports, core_1) {
    "use strict";
  
    Object.defineProperty(exports, "__esModule", { value: true });
    var AppComponent = (function () {
        function AppComponent() {
            this.name = 'Angular';
        }
        return AppComponent;
    }());
    AppComponent = __decorate([
        core_1.Component({ // (1)
            selector: 'my-app',
            template: "<h1>Hello {{name}}</h1>",
        })
    ], AppComponent);
    exports.AppComponent = AppComponent;
});

通过 Angular 2 Decorators - part 1 文章,我们知道 TypeScript 类装饰器的声明:

declare type ClassDecorator = <TFunction extends Function>(target: TFunction)
  => TFunction | void;

而转换后 ES5 代码中 __decorate 函数的方法签名是 function (decorators, target, key, desc) 。因此我们可以推断,core_1.Component 是一个函数,该函数调用后返回一个 ClassDecorator 。类似于 Angular 2 Decorators - part 1 文章中的 Greeter 装饰器:

function Greeter(greeting: string) {
  return function(target: Function) {
    target.prototype.greet = function(): void {
      console.log(greeting);
    }
  }
}

@Greeter('您好')
class Greeting {
  constructor() {
    // 内部实现
  }
}

let myGreeting = new Greeting();
myGreeting.greet(); // console output: '您好!';

那我们来看一下 @angular/core 模块中导出的 Component 函数:

/**
 * Component decorator and metadata.
 */
export const Component: ComponentDecorator = <ComponentDecorator>makeDecorator(
    'Component', {
      selector: undefined, // 用于定义组件在HTML代码中匹配的标签
      inputs: undefined, // 组件的输入属性
      outputs: undefined, // 组件的输出属性
      host: undefined, // 绑定宿主的属性、事件等
      exportAs: undefined, // 导出指令,使得可以在模板中调用
      moduleId: undefined, // 包含该组件模块的id,它被用于解析模板和样式的相对路径 
      providers: undefined, // 设置组件及其子组件可以用的服务
      viewProviders: undefined, // 设置组件及其子组件(不含ContentChildren)可以用的服务
      changeDetection: ChangeDetectionStrategy.Default, // 指定组件使用的变化检测策略
      queries: undefined, // 设置组件的查询条件
      templateUrl: undefined, // 为组件指定一个外部模板的URL地址
      template: undefined, // 为组件指定一个内联的模板
      styleUrls: undefined, // 为组件指定一系列用于该组件的样式表文件
      styles: undefined, // 为组件指定内联样式
      animations: undefined, // 设置组件相关动画
      encapsulation: undefined, // 设置组件视图包装选项
      interpolation: undefined, // 设置默认的插值运算符,默认是"{{"和"}}"
      entryComponents: undefined // 设置需要被提前编译的组件
    },
    Directive);

让我们继续来看一下 makeDecorator 这个函数:

// @angular/core/src/util/decorators.ts

/**
 * const Component: ComponentDecorator = <ComponentDecorator>makeDecorator(
 *   'Component', {...}, Directive);
 */
function makeDecorator(name, props, parentClass, chainFn) { 
  		// name: 'Component', props: {...}, parentClass: Directive
        if (chainFn === void 0) { chainFn = null; }
  
  		// 创建Metadata构造函数
        var metaCtor = makeMetadataCtor([props]); 
  		// objOrType: { selector: 'my-app', template: "<h1>Hello {{name}}</h1>" }
        function DecoratorFactory(objOrType) { 
          
          	// 确保已经引入了Reflect库
            if (!(Reflect && Reflect.getMetadata)) {
                throw 'reflect-metadata shim is required when using class decorators';
            }
          
          	// 判断this对象是否为DecoratorFactory的实例,若是则合并metadata信息
            if (this instanceof DecoratorFactory) { 
                metaCtor.call(this, objOrType);
                return this;
            }

            var annotationInstance = new DecoratorFactory(objOrType); 
            var chainAnnotation = typeof this === 'function' && 
                Array.isArray(this.annotations) ? this.annotations : [];
            chainAnnotation.push(annotationInstance);
          
          	// 定义类装饰器,参数即要装饰的类
            var TypeDecorator = function TypeDecorator(cls) {
                // 首先先获取装饰类关联的annotations信息,若不存在则创建
                // 保存上面创建的annotationInstance实例,并调用保存更新后的annotations信息
                var annotations = Reflect.getOwnMetadata('annotations', cls) || [];
                annotations.push(annotationInstance); 
                Reflect.defineMetadata('annotations', annotations, cls);
                return cls;
            };
          
            TypeDecorator.annotations = chainAnnotation;
            TypeDecorator.Class = Class;
            if (chainFn) chainFn(TypeDecorator);
          
            return TypeDecorator;
        }
        if (parentClass) {
            DecoratorFactory.prototype = Object.create(parentClass.prototype);
        }
        DecoratorFactory.prototype.toString = function () { return ("@" + name); };
        DecoratorFactory.annotationCls = DecoratorFactory;
        return DecoratorFactory;
}

// 生成Metadata构造函数
function makeMetadataCtor(props: ([string, any] | {[key: string]: any})[]): any {
   // args: [{ selector: 'my-app', template: "<h1>Hello {{name}}</h1>" }]
  return function ctor(...args: any[]) {
    props.forEach((prop, i) => {
       // argVal: { selector: 'my-app', template: "<h1>Hello {{name}}</h1>" }
      const argVal = args[i];
      if (Array.isArray(prop)) {
        this[prop[0]] = argVal === undefined ? prop[1] : argVal;
      } else {
        // propName: 'selector' | 'template'
        for (const propName in prop) { 
          this[propName] =
              argVal && argVal.hasOwnProperty(propName) ? 
            	argVal[propName] : prop[propName];
        }
      }
    });
  };
}

通过阅读以上的源码,我们发现当调用 makeDecorator('Component', {..}, Directive) 方法时,返回的是

DecoratorFactory 函数,该函数只接收一个参数,当调用该工厂函数时,则返回 TypeDecorator 函数即类装饰器。回到最早的例子,当我们调用 core_1.Component({ selector: 'my-app', template: "..." }) 创建的 annotationInstance 实例,内部结构如下:

{
      selector: 'my-app', 
      inputs: undefined, 
      outputs: undefined, 
      host: undefined, 
      exportAs: undefined, 
      moduleId: undefined,  
      providers: undefined, 
      viewProviders: undefined, 
      changeDetection: ChangeDetectionStrategy.Default, 
      queries: undefined, 
      templateUrl: undefined, 
      template: "<h1>Hello {{name}}</h1>",
      styleUrls: undefined, 
      styles: undefined, 
      animations: undefined, 
      encapsulation: undefined, 
      interpolation: undefined, 
      entryComponents: undefined
}

现在我们来梳理一下整个流程,系统初始化的时候,会调用 makeDecorator('Component', {..}, Directive) 方法,创建 ComponentDecorator 工厂 。我们编写的 @component 组件转换成 ES 5 代码后,会使用用户自定义的 metadata 信息作为参数,自动调用 ComponentDecorator 工厂函数,该函数内部实现的主要功能就是创建 annotationInstance 对象,最后返回 TypeDecorator 类装饰器。该类装饰器会被 __decorate([...], AppComponent) 函数调用,参数 traget 就是我们要装饰的类 。

我有话说

  1. 因为一个类可以应用多个装饰器,所以 var annotations = Reflect.getOwnMetadata('annotations', cls) || [] 语句中,annotations 的值是数组。在 Angular 2 中,应用多个装饰器的情形是使用 @optional@Inject()、@host 等参数装饰器,描述构造函数中需要注入的依赖对象。
  2. 通过 Reflect.defineMetadata API 定义的 metadata 信息,是保存在 window['__core-js_shared__'] 对象的 metadata 属性中。感兴趣的话,大家可以直接在 Console 控制台,输入 window['__core-js_shared__'] 查看该对象内部保存的信息。
  3. @component 中 @ 符号的作用是为了告诉 TypeScript 编译器,@ 后面的是装饰器函数或装饰器工厂,需要特殊处理。假设在 @component({...}) 中去掉 @ 符号,那么变成了普通的函数调用,这样马上就会报错,因为我们并没有定义 Component 函数。通过观察转换后的代码,我们发现 @component({...}) 被转换成 core_1.Component ,它就是从 @angular/core 导入的装饰器函数。

总结

本文介绍了 Angular 2 中最常用的 ComponentDecorator 装饰器,并通过简单的例子,一步步分析该装饰器的内部工作流程。不过我们只介绍了 Angular 2 框架内部如何解析、创建及保存 metadata 信息,还未涉及到组件初始化的过程中,如何读取、应用组件对应的 metadata 信息。另外在后续的 Angular 2 DI 文章中,我们还会继续分析其它装饰器的工作原理。

Angular 2 ViewEncapsulation

在介绍 Angular 2 ViewEncapsulation 之前,我们先来介绍一下 Web Components 标准。

Web Components

近年来,web 开发者们通过插件或者模块的形式在网上分享自己的代码,便于其他开发者们复用这些优秀的代码。同样的故事不断发生,人们不断的复用 JavaScript 文件,然后是 CSS 文件,当然还有 HTML 片段。但是你又必须祈祷这些引入的代码不会影响到你的网站或者web app。

WebComponents 是解决这类问题最好的良药,它通过一种标准化的非侵入的方式封装一个组件,每个组件能组织好它自身的 HTML 结构、CSS 样式、JavaScript 代码,并且不会干扰页面上的其他元素。

Web Components 由以下四种技术组成:

因为 Shadow DOM 与 Angular ViewEncapsulation 相关, 所以这篇文章我们主要介绍 Shadow DOM 相关的内容。

Shadow DOM

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Shadow DOM</title>
    <style type="text/css">
        .shadowroot_son {
            color: #f00;
        }
    </style>
</head>
<body>
<p class="shadowroot_son">我不在 Shadow Host内</p>
<div class="shadowhost">Hello, world!</div>
<script>
    // 影子宿主(shadow host)
    var shadowHost = document.querySelector('.shadowhost');
    // 创建影子根(shadow root)
    var shadowRoot = shadowHost.createShadowRoot();
    // 影子根作为影子树的第一个节点,其他的节点比如p节点都是它的子节点。
    shadowRoot.innerHTML = '<p class="shadowroot_son">我在 Shadow Host内</p>';
</script>
</body>
<html>

以上代码成功运行后,如下图:

angular2-view-encapsulation

我们发现在 #shadow-root 中的元素,不受我们外界定义的 CSS shadowroot_son 类影响。因此我们可以利用 Shadow DOM 来封装我们自定义的 HTML 标签、CSS 样式和 JavaScript 代码。需要注意的是 Shadow DOM 兼容性还不是很好,具体请参考 - Can I Use Shadow DOM

接下来我们开始介绍 Angular ViewEncapsulation Modes:

ViewEncapsulation

ViewEncapsulation 允许设置三个可选的值:

  • ViewEncapsulation.Emulated - 无 Shadow DOM,但是通过 Angular 提供的样式包装机制来封装组件,使得组件的样式不受外部影响。这是 Angular 的默认设置。
  • ViewEncapsulation.Native - 使用原生的 Shadow DOM 特性
  • ViewEncapsulation.None - 无 Shadow DOM,并且也无样式包装

ViewEncapsulation 枚举定义:

export enum ViewEncapsulation {
  Emulated, // 默认值
  Native,
  None
}

ViewEncapsulation.None

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

@Component({
  selector: 'my-app',
  template: `
    <h4>Welcome to Angular World</h4>
    <p class="greet">Hello {{name}}</p>
  `,
  styles: [`
    .greet {
      background: #369;
      color: white;
    }
  `],
  encapsulation: ViewEncapsulation.None // None | Emulated | Native
})
export class AppComponent {
  name: string = 'Semlinker';
}

运行后的结果:

angular2-view-encapsulation-none

ViewEncapsulation.None 设置的结果是没有 Shadow DOM,并且所有的样式都应用到整个 document,换句话说,组件的样式会受外界影响,可能被覆盖掉。

ViewEncapsulation.Emulated

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

@Component({
  selector: 'my-app',
  ...,
  encapsulation: ViewEncapsulation.Emulated // None | Emulated | Native
})
export class AppComponent {
  name: string = 'Semlinker';
}

运行后的结果:

angular2-view-encapsulation-emulated

ViewEncapsulation.Emulated 设置的结果是没有 Shadow DOM,但是通过 Angular 提供的样式包装机制来封装组件,使得组件的样式不受外部影响。虽然样式仍然是应用到整个 document,但 Angular 为 .greet 类创建了一个 [_ngcontent-cmy-0] 选择器。可以看出,我们为组件定义的样式,被 Angular 修改了。其中的 _nghost-cmy-* 和 _ngcontent-cmy-* 用来实现局部的样式。

ViewEncapsulation.Native

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

@Component({
  selector: 'my-app',
  ...,
  encapsulation: ViewEncapsulation.Native // None | Emulated | Native
})
export class AppComponent {
  name: string = 'Semlinker';
}

运行后的结果:

angular2-view-encapsulation-native

ViewEncapsulation.Emulated 设置的结果是使用原生的 Shadow DOM 特性。Angular 会把组件按照浏览器支持的 Shadow DOM 形式渲染,渲染结果如上图所示。

总结

在了解 Angular 2 ViewEncapsulation(视图包装) 特性前,我们先介绍了 WebComponents 标准及标准中的 Shadow DOM 技术。后面我们通过实际的示例,展示了 ViewEncapsulation 支持的三种模式,并总结了各个模式的特点。后面我们还会有专门的文章介绍 Angular 2 中样式的应用方案。

参考资料

如何解决 Angular 2 中 Token 命名冲突

字符串Token VS Type类型Token

在 Angular 2 中,provider 的 token 的类型可以是字符串或 Type 类型。我们可以根据实际应用场景,选用不同的类型。假设我们有一个服务类 DataService,并且我们想要在组件中注入该类的实例,我们可以这样使用:

@Component({
  selector: 'my-component',
  providers: [
    { provide: DataService, useClass: DataService }
  ]
})
class MyComponent {
  constructor(private dataService: DataService) { }
}

Type 类型

// Type类型 - @angular/core/src/type.ts
export const Type = Function;

export function isType(v: any): v is Type<any> {
  return typeof v === 'function';
}

export interface Type<T> extends Function { new (...args: any[]): T; }

这是非常酷炫的事情,只要我们知道依赖对象的类型,我们就可以方便地注入对应类型的实例对象。但是有时候,我们需要注入的是普通的JavaScript对象,而不是Type 类型的对象。比如,我们需要注入一个config对象:

const CONFIG = {
  apiUrl: 'http://my.api.com',
  theme: 'suicid-squad',
  title: 'My awesome app'
};

有时候我们需要注入一个原始数据类型的数值,如字符串或布尔值:

const FEATURE_ENABLED = true;

在这种情况下,我们是不能使用 String 或 Boolean 类型,因为如果使用这些类型,我们只能获得类型对应的默认值。想解决这个问题,但我们又不想引入一种新的类型来表示原始数据类型的值。这时我们可以考虑使用字符串作为 token,而不用引入新的类型:

let featureEnabledToken = 'featureEnable';
let configToken = 'config';

providers: [
  { provide: featureEnabledToken, useValue: FEATURE_ENABLED },
  { provide: configToken, useValue: CONFIG }
]

使用字符串作为 token 设置完 providers 后,我们就可以使用 @Inject 装饰器注入相应依赖:

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

class MyComponent {
  constructor(
  @Inject(featureEnabledToken) private featureEnabled,
  @Inject(configToken) private config
  )
}

使用字符串作为 Token 存在的问题

让我们回顾一下之前的例子,config 是一个很通用的名字,这样的话就可能在项目中留下隐患。因为若在项目中也存在同样名称的 provider,那么后面声明的 provider 将会覆盖之前声明的 provider。

假设在项目中,我们引入了第三方脚本库。该库的 provides 的配置信息如下:

export const THIRDPARTYLIBPROVIDERS = [
  { provide: 'config', useClass: ThirdParyConfig }
];

实际使用时,我们可能这样做:

import THIRDPARTYLIBPROVIDERS from './third-party-lib';

providers = [
  DataService,
  THIRDPARTYLIBPROVIDERS
];

到目前为止,一切都能正常工作。但我们是不知道 THIRDPARTYLIBPROVIDERS 内部的具体情况,除非我们已经阅读了第三方库的官方文档或源码。在未知的情况下,我们可能这样使用:

providers = [
  DataService,
  THIRDPARTYLIBPROVIDERS,
  { provide: configToken, useValue: CONFIG }
];

此时第三方库就不能正常工作了。因为它获取不到它所依赖的配置对象,因为它被我们自定义的 provider 替换了。

救世主 - OpaqueToken

为了解决上述问题,Angular 2 引入了 OpaqueToken,它允许我们创建基于字符串的 Token 类。创建 OpaqueToken 对象很简单,只需导入 Opaque 类。这样的话,上面提到的第三方类库,可以调整为:

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

const CONFIG_TOKEN = new OpaqueToken('config');

export const THIRDPARTYLIBPROVIDERS = [
  { provide: CONFIG_TOKEN, useClass: ThirdPartyConfig }
];

而之前提到的冲突问题,也可以按照下面的方式解决。

import { OpaqueToken } from '@angular/core';
import THIRDPARTYLIBPROVIDERS from './third-party-lib';

const MY_CONFIG_TOKEN = new OpaqueToken('config');

providers = [
  DataService,
  THIRDPARTYLIBPROVIDERS,
  { provide: MY_CONFIG_TOKEN, useValue: CONFIG }
]

OpaqueToken 的工作原理

// OpaqueToken - @angular/core/src/di/injection_token.ts
export class OpaqueToken {
  constructor(protected _desc: string) {}
  toString(): string { return `Token ${this._desc};` }
}

通过查看 OpaqueToken 类,我们可以发现,尽管是使用相同的字符串去创建 OpaqueToken 实例对象,但每次都是返回一个新的实例,从而保证了全局的唯一性。

const TOKEN_A = new OpaqueToken('token');
const TOKEN_B = new OpaqueToken('token');

TOKEN_A === TOKEN_B // false

救世主 - OpaqueToken 不给力了

让我们看一下示例中 DataService Provider 配置信息:

const API_URL = new OpaqueToken('apiUrl');

providers: [
  {
    provide: DataService,
    useFactory: (http, apiUrl) => {
      // create data service
    },
    deps: [
      Http,
      new Inject(API_URL)
    ]
  }
]

我们使用工厂函数创建 DataService 实例,DataService 依赖 http 和 apiUrl 对象,为了让 Angular 能够准确地注入依赖对象,我们使用 deps 属性声明依赖对象的类型。因为 Http 是 Type 类型的 Token,我们只需直接声明。但 API_URL 是 OpaqueToken 类的实例,不属于 Type 类型。因此我们需要使用 new Inject(API_URL) 方式声明依赖对象。(备注:new Inject()与在构造函数中使用 @Inject() 的方式声明依赖对象是等价的)。

上面的代码能够正常运行,但在实际开发过程中,开发者很容易忘记调用 new Inject()。为了解决这个问题,Angular 团队引入了 InjectionToken。

新救世主 - Angular 4.x InjectionToken

// InjectionToken - @angular/core/src/di/injection_token.ts

/**
* InjectionToken 继承于 OpaqueToken,同时支持泛型,用于描述依赖对象的类型
*
*/
export class InjectionToken<T> extends OpaqueToken {
  private _differentiate_from_OpaqueToken_structurally: any;
  constructor(desc: string) { super(desc);  }
  
  toString(): string { 
     return `InjectionToken ${this._desc};` 
  }
}

使用 InjectionToken 重写上面的示例:

// InjectionToken<T> - 使用泛型描述该Token所关联的依赖对象的类型
const API_URL = new InjectionToken<string>('apiUrl'); 

providers: [
  {
    provide: DataService,
    useFactory: (http, apiUrl) => {
      // create data service
    },
    deps: [
      Http,
      API_URL // no `new Inject()` needed!
    ]
  }
]

总结

我们可以通过 OpaqueToken 避免定义 Provider 时,出现 Token 命名冲突的问题。除此之外,使用 OpaqueToken 也为我们提供更好的异常信息。但如果我们使用的 Angular 4.x 以上的版本,我们最好使用 InjectionToken 替换原有的 OpaqueToken。

参考资源
opaque-tokens-in-angular-2

Ionic 2 多主题、多租户构建方案探索

项目背景

公司的产品是一款2B的在线教育产品,已有的客户大多数都有定制化的需求,主要包括UI主题和二次开发的功能。本文围绕的主要内容是如何基于 Ionic 2 平台提供的工具,实现灵活的多主题方案。

Ionic 2 提供的主题方案

Ionic 2 使用 $color map 的 key 作为组件的输入属性,用于设置组件的样式。$colors map 中的内容如下:

// variables.scss 文件(路径: src/theme/variables.scss )
$colors: {
  primary: #387ef5,
  secondary: #32db64,
  danger: #f53d3d,
  light: #f4f4f4,
  dark: #222,
  favorite: #69BB7B
};

使用示例:

<ion-item-options side="right">
	<button ion-button color="primary" (click)="buttonOne(page.title)">
		<ion-icon name="text"></ion-icon>
		BUTTON 1
	</button>
	<button ion-button color="secondary" (click)="buttonTwo(page.title)">
		<ion-icon name="call"></ion-icon>
		BUTTON 2
	</button>
</ion-item-options>

此外 $colors map 还支持字面量形式,对象内部有 base 和 contrast 属性

  • base: 用于标识组件的背景颜色
  • contract: 用于标识组件的文本颜色

备注:Ionic 框架内部使用 node_modules/ionic-angular/themes/ionic.functions.scss 中的颜色处理函数,解析 map中设置的 base 和 contrast 值。

使用示例:

$colors: (
 twitter: {
   	base: #55acee,
    contrast: #ffffff
 },
 facebook: {
 	base: #38669f;
    contrast: #ffffff
 }
);

切换 Ionic 的主题:

在 src/theme/variables.scss 文件中导入 Ionic 预置的黑色主题

@import "ionic.build.dark";

自定义主题

  • 修改Variables文件中,$colors map 定义的属性值
  • 覆写已有的变量值
  • 自定义 Sass 变量
  • 自定义组件样式
  • 配置组件的mode(模式)

1.修改Variables文件中,$colors map 定义的属性值

// variables.scss 文件(路径: src/theme/variables.scss )
$colors: (
  primary: #387ef5,
  secondary: #32db64,
  danger: #f53d3d,
  light: #f4f4f4,
  dark: #222,
  favorite: #69BB7B,
  
  twitter:(
  	base: #55acee,
  	contrast: #ffffff
  ),
  
  facebook:(
  	base: #38669f,
  	contrast: #ffffff
  )
);

<button ion-button color="facebook" (click)="buttonOne(page.title)">
  <ion-icon name="text"></ion-icon>
  BUTTON 1
</button>

<button ion-button color="twitter" (click)="buttonTwo(page.title)">
  <ion-icon name="call"></ion-icon>
  BUTTON 2
</button>

在任意自定义组件中,可以使用 color 函数获取相应的颜色值:

my-component {
  background: color($colors, twitter, base);
}

2.覆写已有的变量值

$text-color: #686868;
$font-size-base: 1.6rem;
$list-ios-background-color: #ffffff;
$list-md-background-color: #ffffff;
$list-wp-background-color: #ffffff;

Ionic 中可以覆写的 Sass 变量列表:overriding-ionic-variables

3.自定义 Sass 变量

// variables.scss 文件(路径: src/theme/variables.scss )
$colors: (
  ...
);
$my-padding: 20px; // Custom Sass variables

4.自定义组件样式

// about.scss 文件 (路径: myApp/src/pages/about/about.scss)
page-about {
  .isSuccess {
	color: #1E88E5 !important;
  }
  .isError {
	color: #D43669 !important;
  }  
}

about.scss 文件中的内容将会被编译到 main.css文件中

page-home .isSuccess {
	color: #1E88E5 !important;
}
page-home .isError {
	color: #D43669 !important;
}

5.配置组件的mode(模式)

每个平台都有对应的模式:

  • md (Android)
  • ios (iOS)
  • wp (Windows Phone)
  • md (Core - used for any platform other than the above)

我们也可以通过 Ionic 的 Config API 方便地配置 mode 下可配置字段的值,具体如下:

imports: [
  IonicModule.forRoot(MyApp, {
  	backButtonText: "",
  	backButtonIcon: "md-arrow-back",
  	iconMode: "md",
 	modalEnter: "modal-md-slide-in",
  	modalLeave: "modal-md-slide-out",
  	pageTransition: "md"
  });
]

以上的配置信息,用于设置在 iOS 平台下,App 的风格采用 Android material 设计。

我们也可以单独针对特定的平台,进行配置:

imports: [
	IonicModule.forRoot(MyApp, {
    platforms: {
	  android: {
			backButtonText: "",
			backButtonIcon: "md-arrow-back",
			iconMode: "md",
			modalEnter: "modal-md-slide-in",
			modalLeave: "modal-md-slide-out",
			pageTransition: "md",
	  },
	  ios : {
			backButtonText: "Previous",
			backButtonIcon: "ios-arrow-back",
        	iconMode: "ios",
			modalEnter: "modal-ios-slide-in",
			modalLeave: "modal-ios-slide-out",
			pageTransition: "ios",
	 }
}];

此外,通过 Config API 提供的方法,我们可以在 TypeScript classes 中,方便的设置特定平台下的配置信息,具体如下:

config.set('ios', 'textColor', '#AE1245');

该方法接受三个参数:

  • platform (optional - 'ios' or 'android', if omitted this will apply to all platforms)
  • key (optional - The name of the key used to store our value I.e. 'textColor')
  • value (optional - The value for the key I.e. '#AE1245')

通过 Config API 提供的 get 方法获取配置的值:

config.get('textColor');

我们也能够在组件层级,方便地配置组件,如通过 ion-tabs 的输入属性 tbasPlacement 设置 ion-tabs 组件的显示位置:

<ion-tabs tabsPlacement="bottom">
	<ion-tab tabTitle="Dash" tabIcon="pulse" [root]="tabRoot"></ion-tab>
</ion-tabs>

配置指定平台的样式

Ionic 使用 mode 来定义组件的外观。每个平台都有默认的 mode , 任何 mode 下的样式信息,都能被我们覆写。我们可以在 ion-app 中指定mode的值,具体如下:

<ion-app class="md">

覆写 md 模式下的样式

.md button {
  text-transform: capitalize;
}

覆写 md 下 Sass 变量的值

$button-md-border-radius: 8px;

// Ionic Sass
// ---------------------------------
@import "ionic";

动态的设置组件的属性

<ion-list [attr.no-lines]="isMD ? '' : null"

EXE - 多主题、多租户构建方案

Ionic 2 团队基于 webpack 开发了项目的构建工具 - ionic-app-scripts

Ionic-app-scripts 的功能很强大,通过它我们可以非常灵活地控制项目构建的每个环节。比如,可以通过 command-line 指定某个环节使用的配置文件:

npm run build --rollup ./config/rollup.config.js

此外,也可以在 package.json 文件中设置配置文件的路径和系统构建参数的值:

"config": {
  "t": "sf", // 租户的名称
  "l": "zh-cn", // 默认的语言包
  "ionic_sass": "./config/sass.config.js", // 自定义Sass构建环节的配置文件 
  "ionic_copy": "./config/copy.config.js" // 自定义Copy构建环节的配置文件
}

copy.config.js 文件 - 用于配置项目构建过程中文件拷贝的环节:

// copy.config.js 代码片段
module.exports = {
  copyResources: {
    src: ['{{ROOT}}/materials/' + process.env.npm_package_config_t + '/resources/**/*'],
    dest: '{{ROOT}}/resources'
  }
}

备注:根目录下的 '{{ROOT}}/materials/' 目录下,用于存放不同租户自定义的资源,如 icon.png 和 splash.png 等图片资源文件。

sass.config.js 文件 - 用于配置项目构建过程中 Sass 编译的环节:

// sass.config.js 代码片段
module.exports = {
  variableSassFiles: [
    '{{SRC}}/theme/variables.scss',
    `{{SRC}}/theme/${process.env.npm_package_config_t}.theme.scss` 
  ]
}

备注:不同租户的主题的命名规则:租户名 + .theme.scss 。项目构建时通过动态设置 process.env.npm_package_config_t 的值,来实现动态构建。

我有话说

1.为什么在 variables.scss 文件中 @import "ionic.globals" 、@import "ionic.ionicons" 能正常导入对应的文件:

在 Scss 文件中导入其他依赖的 *.scss 文件,我们一般使用相对路径的方式。但 variables.scss 导入 ionic.globals 或 ionic.ionicons 文件却不是使用相对路径的方式,此外在我们项目的 src 目录下也没有对应的文件。怎么会那么神奇,刚开始接触的时候,我也一脸懵逼。后面通过深入发掘,终于解开了疑惑。

看完下面的代码,你应该也猜到了答案:

// sass.config.js 代码片段
{
  ...,
  includePaths: [
   'node_modules/ionic-angular/themes',
   'node_modules/ionicons/dist/scss',
   'node_modules/ionic-angular/fonts'
  ]
}

总结

目前的主题方案,还比较初级,后续还有很多地方需要优化和升级,我们会持续更新相关的内容,有兴趣的小伙伴可以一起探讨哈。

Angular 2 Decorators(装饰器) - part 1

Angular 2 Decorators(装饰器) - part 1

在我们深入了解 Angular 2 中 @NgModule@component@Injectable 等常见的装饰器之前,我们要先了解 TypeScript 中的装饰器。装饰器是一个非常酷的特性,最早出现在 Google 的 AtScript 中,它出现的目的是为了让开发者,开发出更容易维护、更容易理解的 Angular 代码。令人兴奋的是,在2015年 Angular 团队跟 MicroSoft 的 TypeScript 团队经过数月的的交流,最终决定采用 TypeScript 来重写 Angular 2 项目 。

装饰器是什么

  • 它是一个表达式
  • 该表达式被执行后,返回一个函数
  • 函数的入参分别为 targe、name 和 descriptor
  • 执行该函数后,可能返回 descriptor 对象,用于配置 target 对象 

装饰器的分类

  • 类装饰器 (Class decorators)
  • 属性装饰器 (Property decorators)
  • 方法装饰器 (Method decorators)
  • 参数装饰器 (Parameter decorators)

TypeScript 类装饰器

类装饰器声明:

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void

类装饰器顾名思义,就是用来装饰类的。它接收一个参数:

  • target: TFunction - 被装饰的类

看完第一眼后,是不是感觉都不好了。没事,我们马上来个例子:

function Greeter(target: Function): void {
  target.prototype.greet = function (): void {
    console.log('Hello!');
  }
}

@Greeter
class Greeting {
  constructor() {
    // 内部实现
  }
}

let myGreeting = new Greeting();
myGreeting.greet(); // console output: 'Hello!';

上面的例子中,我们定义了 Greeter 类装饰器,同时我们使用了 @greeter 新的语法,来使用装饰器。

(备注:读者可以直接复制上面的代码,在 TypeScript Playground 中运行查看结果)。

有的读者可能想问,例子中总是输出 Hello! ,能自定义输出的问候语么 ?这个问题很好,答案是可以的。具体实现如下:

function Greeter(greeting: string) {
  return function(target: Function) {
    target.prototype.greet = function(): void {
      console.log(greeting);
    }
  }
}

@Greeter('您好')
class Greeting {
  constructor() {
    // 内部实现
  }
}

let myGreeting = new Greeting();
myGreeting.greet(); // console output: '您好!';

TypeScript 属性装饰器

属性装饰器声明:

declare type PropertyDecorator = (target:Object, propertyKey: string | symbol ) => void;

属性装饰器顾名思义,用来装饰类的属性。它接收两个参数:

  • target: Object - 被装饰的类
  • propertyKey:string | symbol - 被装饰类的属性名

趁热打铁,马上来个例子热热身:

function LogChanges(target: Object, key: string) {
  var propertyValue: string = this[key];
  if(delete this[key]) {
    Object.defineProperty(target, key, {
      get: function () {
        return propertyValue;
      },
      set: function(newValue) {
        propertyValue = newValue;
        console.log(`${key} is now ${propertyValue}`);
      }
    });
  }
}

class Fruit {
  @LogChanges
  name: string;
}

let fruit = new Fruit();
fruit.name = 'apple'; // console output: 'name is now apple'
fruit.name = 'banana'; // console output: 'name is now banana'

那么问题来了,如果用户想在属性变化的时候,自动刷新页面,而不是简单地在控制台输出消息,那要怎么办?我们能不能参照类装饰器自定义问候语的方式,来实现监测属性变化的功能。具体实现如下:

function LogChanges(callbackObject: any) {
  return function(target: Object, key: string): void {
    var propertyValue: string = this[key];
 	 if(delete this[key]) {
    	Object.defineProperty(target, key, {
          get: function () {
              return propertyValue;
          },
          set: function(newValue) {
              propertyValue = newValue;
              callbackObject.onchange.call(this, propertyValue);
          }
     });
    }
  }
}

class Fruit {
  @LogChanges({
    onchange: function(newValue: string): void {
      console.log(`The fruit is ${newValue} now`);
    }
  })
  name: string;
}

let fruit = new Fruit();
fruit.name = 'apple'; // console output: 'The fruit is apple now'
fruit.name = 'banana'; // console output: 'The fruit is banana now'

TypeScript 方法装饰器

方法装饰器声明:

declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol, descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;

方法装饰器顾名思义,用来装饰类的属性。它接收三个参数:

  • target: Object - 被装饰的类
  • propertyKey: string | symbol - 方法名
  • descriptor: TypePropertyDescript - 属性描述符

废话不多说,直接上例子:

function LogOutput(tarage: Function, key: string, descriptor: any) {
  var originalMethod = descriptor.value;
  var newMethod = function(...args: any[]): any {
    var result: any = originalMethod.apply(this, args);
    if(!this.loggedOutput) {
      this.loggedOutput = new Array<any>();
    }
    this.loggedOutput.push({
      method: key,
      parameters: args,
      output: result,
      timestamp: new Date()
    });
    return result;
  };
  descriptor.value = newMethod;
}

class Calculator {
  @LogOutput
  double (num: number): number {
    return num * 2;
  }
}

let calc = new Calculator();
calc.double(11);
// console ouput: [{method: "double", output: 22, ...}]
console.log(calc.loggedOutput); 

最后我们来看一下参数装饰器:

TypeScript 参数装饰器

参数装饰器声明:

declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number ) => void

参数装饰器顾名思义,是用来装饰函数参数,它接收三个参数:

  • target: Object - 被装饰的类
  • propertyKey: string | symbol - 方法名
  • parameterIndex: number - 方法中参数的索引值
function Log(target: Function, key: string, parameterIndex: number) {
  var functionLogged = key || target.prototype.constructor.name;
  console.log(`The parameter in position ${parameterIndex} at 
        ${functionLogged} has been decorated`);
}

class Greeter {
  greeting: string;
  constructor(@Log phrase: string) {
	this.greeting = phrase; 
  }
}
// console output: The parameter in position 0 at Greeter has
// been decorated

我有话说

1.Object.defineProperty() 方法有什么用 ?

Object.defineProperty 用于在一个对象上定义一个新的属性或者修改一个已存在的属性,并返回这个对象。 方法的签名:Object.defineProperty(obj, prop, descriptor) ,参数说明如下:

  • obj 需要定义的属性对象
  • prop 需被定义或修改的属性名
  • descriptor 需被定义或修改的属性的描述符

对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个拥有可写或不可写值的属性。存取描述符是由一对 getter-setter 函数功能来描述的属性。描述符必须是两种形式之一,不能同时是两者。

数据描述符和存取描述符均具有以下可选键值:

  • configurable
    当且仅当该属性的 configurable 为 true 时,该属性才能够被改变,也能够被删除。默认为 false。
  • enumerable
    当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。

数据描述符同时具有以下可选键值:

  • value
    该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
  • writable
    当且仅当仅当该属性的writable为 true 时,该属性才能被赋值运算符改变。默认为 false。

存取描述符同时具有以下可选键值:

  • get
    一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为undefined。
  • set
    一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为undefined。

使用示例:

var o = {}; // 创建一个新对象
Object.defineProperty(o, "a", {value : 37, writable : true, enumerable : true, 	
  configurable : true});  

总结

本文主要介绍了 TypeScript 中的四种装饰器,了解装饰器的基本分类和实现原理,为我们下一篇深入 Angular 2 的 @NgModule@component@Injectable 等常用装饰器做好铺垫。

Angular 2 TemplateRef & ViewContainerRef

Angular 2 TemplateRef & ViewContainerRef

TemplateRef

在介绍 TemplateRef 前,我们先来了解一下 HTML 模板元素 - 。模板元素是一种机制,允许包含加载页面时不渲染,但又可以随后通过 JavaScript 进行实例化的客户端内容。我们可以将模板视作为存储在页面上稍后使用的一小段内容。

在 HTML5 标准引入 template 模板元素之前,我们都是使用 <script> 标签进行客户端模板的定义,具体如下:

<script id="tpl-mock" type="text/template">
   <span>I am span in mock template</span>
</script>

对于支持 HTML5 template 模板元素的浏览器,我们可以这样创建客户端模板:

<template id="tpl">
    <span>I am span in template</span>
</template>

下面我们来看一下 HTML5 template 模板元素的使用示例:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"> <title>HTML5 Template Element Demo</title></head>
<body>
<h4>HTML5 Template Element Demo</h4>
<!-- Template Container -->
<div class="tpl-container"></div>
<!-- Template -->
<template id="tpl">
    <span>I am span in template</span>
</template>
<!-- Script -->
<script type="text/javascript">
    (function renderTpl() {
        if ('content' in document.createElement('template')) {
            var tpl = document.querySelector('#tpl');
            var tplContainer = document.querySelector('.tpl-container');
            var tplNode = document.importNode(tpl.content, true);
            tplContainer.appendChild(tplNode); 
        } else {
            throw  new Error("Current browser doesn't support template element");
        }
    })();
</script>
</body>
</html>

以上代码运行后,在浏览器中我们会看到以下内容:

HTML5 Template Element Demo

I am span in template

而当我们注释掉 tplContainer.appendChild(tplNode) 语句时,刷新浏览器后看到的是:

HTML5 Template Element Demo

这说明页面中 模板元素中的内容,如果没有进行处理对用户来说是不可见的。Angular 2 中, 模板元素主要应用在结构指令中,此外在 Angular 2 属性指令 vs 结构指令 文章中我们也介绍了 模板元素和自定义结构指令,接下来我们先来介绍一下本文中的第一个主角 - TemplateRef:

import {Component, TemplateRef, ViewChild, AfterViewInit} from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>Welcome to Angular World</h1>
    <template #tpl>
      <span>I am span in template</span>
    </template>
  `,
})
export class AppComponent {
  name: string = 'Semlinker';

  @ViewChild('tpl')
  tpl: TemplateRef<any>;

  ngAfterViewInit() {
    console.dir(this.tpl);
  }
}

上述代码运行后的控制台的输出结果如下:

angular2-template-ref

从上图中,我们发现 @component template 中定义的 模板元素,渲染后被替换成 comment 元素,其内容为 "template bindings={}" 。此外我们通过 @ViewChild 获取的模板元素,是 TemplateRef_ 类的实例,接下来我们来研究一下 TemplateRef_ 类:

TemplateRef_

// @angular/core/src/linker/template_ref.d.ts
export declare class TemplateRef_<C> extends TemplateRef<C> {
    private _parentView;
    private _nodeIndex;
    private _nativeElement;
    constructor(_parentView: AppView<any>, _nodeIndex: number, _nativeElement: any);
    createEmbeddedView(context: C): EmbeddedViewRef<C>;
    elementRef: ElementRef;
}

TemplateRef

// @angular/core/src/linker/template_ref.d.ts
// 用于表示内嵌的template模板,能够用于创建内嵌视图(Embedded Views)
export declare abstract class TemplateRef<C> {
    elementRef: ElementRef;
    abstract createEmbeddedView(context: C): EmbeddedViewRef<C>;
}

(备注:抽象类与普通类的区别是抽象类有包含抽象方法,不能直接实例化抽象类,只能实例化该抽象类的子类)

我们已经知道 模板元素,渲染后被替换成 comment 元素,那么应该如何显示我们模板中定义的内容呢 ?我们注意到了 TemplateRef 抽象类中定义的 createEmbeddedView

抽象方法,该方法的返回值是 EmbeddedViewRef 对象。那好我们马上来试一下:

import {Component, TemplateRef, ViewChild, AfterViewInit} from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>Welcome to Angular World</h1>
    <template #tpl>
      <span>I am span in template</span>
    </template>
  `,
})
export class AppComponent {
  name: string = 'Semlinker';

  @ViewChild('tpl')
  tpl: TemplateRef<any>;

  ngAfterViewInit() {
    let embeddedView = this.tpl.createEmbeddedView(null);
    console.dir(embeddedView);
  }
}

上述代码运行后的控制台的输出结果如下:

angular2-template-ref-1

从图中我们可以知道,当调用 createEmbeddedView 方法后返回了 ViewRef_ 视图对象。该视图对象的 rootNodes 属性包含了 模板中的内容。在上面的例子中,我们知道了 TemplateRef 实例对象中的 elementRef 属性封装了我们的 comment 元素,那么我们可以通过 insertBefore 方法来创建我们模板中定义的内容。

import { Component, TemplateRef, ViewChild, AfterViewInit } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>Welcome to Angular World</h1>
    <template #tpl>
      <span>I am span in template {{name}}</span>
    </template>
  `,
})
export class AppComponent {
  name: string = 'Semlinker';

  @ViewChild('tpl')
  tpl: TemplateRef<any>;

  ngAfterViewInit() {
    // 页面中的<!--template bindings={}-->元素
    let commentElement = this.tpl.elementRef.nativeElement;
    // 创建内嵌视图
    let embeddedView = this.tpl.createEmbeddedView(null);
    // 动态添加子节点
    embeddedView.rootNodes.forEach((node) => {
        commentElement.parentNode
          .insertBefore(node, commentElement.nextSibling);
    });
  }
}

成功运行上面的代码后,在浏览器中我们会看到以下内容:

Welcome to Angular World

I am span in template

现在我们来回顾一下,上面的处理步骤:

  • 创建内嵌视图(embedded view)
  • 遍历内嵌视图中的 rootNodes,动态的插入 node

虽然我们已经成功的显示出 template 模板元素中的内容,但发现整个流程还是太复杂了,那有没有简单地方式呢 ?是时候介绍本文中第二个主角 - ViewContainerRef。

ViewContainerRef

我们先来检验一下它的能力,然后再来好好地分析它。具体示例如下:

import { Component, TemplateRef, ViewChild, ViewContainerRef, AfterViewInit } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>Welcome to Angular World</h1>
    <template #tpl>
      <span>I am span in template</span>
    </template>
  `,
})
export class AppComponent {
  name: string = 'Semlinker';

  @ViewChild('tpl')
  tplRef: TemplateRef<any>;

  @ViewChild('tpl', { read: ViewContainerRef })
  tplVcRef: ViewContainerRef;

  ngAfterViewInit() {
    // console.dir(this.tplVcRef); (1)
    this.tplVcRef.createEmbeddedView(this.tplRef);
  }
}

移除上面代码中的注释,即可在控制台看到以下的输出信息:

angular2-template-ref-2

而在浏览器中我们会看到以下内容:

Welcome to Angular World

I am span in template

接下来我们来看一下 ViewContainerRef_ 类:

// @angular/core/src/linker/view_container_ref.d.ts
// 用于表示一个视图容器,可添加一个或多个视图
export declare class ViewContainerRef_ implements ViewContainerRef {
	...
    length: number; // 返回视图容器中已存在的视图个数
    element: ElementRef;
    injector: Injector;
    parentInjector: Injector;
  	// 基于TemplateRef创建内嵌视图,并自动添加到视图容器中,可通过index设置
    // 视图添加的位置
    createEmbeddedView<C>(templateRef: TemplateRef<C>, context?: C, 
      index?: number): EmbeddedViewRef<C>;
    // 基 ComponentFactory创建组件视图
    createComponent<C>(componentFactory: ComponentFactory<C>,
      index?: number, injector?: Injector, projectableNodes?: any[][]): ComponentRef<C>;
    insert(viewRef: ViewRef, index?: number): ViewRef;
    move(viewRef: ViewRef, currentIndex: number): ViewRef;
    indexOf(viewRef: ViewRef): number;
    remove(index?: number): void;
    detach(index?: number): ViewRef;
    clear(): void;
}

通过源码我们可以知道通过 ViewContainerRef_ 实例,我们可以方便地操作视图,也可以方便地基于 TemplateRef 创建视图。现在我们来总结一下 TemplateRef 与 ViewContainerRef :

TemplateRef:用于表示内嵌的 template 模板元素,通过 TemplateRef 实例,我们可以方便创建内嵌视图(Embedded Views),且可以轻松地访问到通过 ElementRef 封装后的 nativeElement。需要注意的是组件视图中的 template 模板元素,经过渲染后会被替换成 comment 元素。

ViewContainerRef:用于表示一个视图容器,可添加一个或多个视图。通过 ViewContainerRef 实例,我们可以

基于 TemplateRef 实例创建内嵌视图,并能指定内嵌视图的插入位置,也可以方便对视图容器中已有的视图进行管理。简而言之,ViewContainerRef 的主要作用是创建和管理内嵌视图或组件视图。

我有话说

1.Angular 2 支持的 View(视图) 类型有哪几种 ?

  • Embedded Views - Template 模板元素
  • Host Views - Component 组件

1.1 如何创建 Embedded View

ngAfterViewInit() {
    let view = this.tpl.createEmbeddedView(null);
}

1.2 如何创建 Host View

constructor(private injector: Injector,
    private r: ComponentFactoryResolver) {
    let factory = this.r.resolveComponentFactory(AppComponent);
    let componentRef = factory.create(injector);
    let view = componentRef.hostView;
}

2.Angular 2 Component 组件中定义的 模板元素为什么渲染后会被移除 ?

因为 模板元素,已经被 Angular 2 解析并封装成 TemplateRef 实例,通过 TemplateRef 实例,我们可以方便地创建内嵌视图(Embedded View),我们不需要像开篇中的例子那样,手动操作 模板元素。

3.ViewRef 与 EmbeddedViewRef 之间有什么关系 ?

ViewRef 用于表示 Angular View(视图),视图是可视化的 UI 界面。EmbeddedViewRef 继承于 ViewRef,用于表示 模板元素中定义的 UI 元素。

ViewRef

// @angular/core/src/linker/view_ref.d.ts
export declare abstract class ViewRef {
    destroyed: boolean;
    abstract onDestroy(callback: Function): any;
}

EmbeddedViewRef

// @angular/core/src/linker/view_ref.d.ts
export declare abstract class EmbeddedViewRef<C> extends ViewRef {
    context: C;
    rootNodes: any[]; // 保存<template>模板中定义的元素
    abstract destroy(): void; // 用于销毁视图
}

总结

Angular 2 中 TemplateRef 与 ViewContainerRef 的概念对于初学者来说会比较羞涩难懂,本文从基本的 HTML 5 模板元素开始,介绍了如何操作和应用页面中定义的模板。然后通过实例介绍了 Angular 2 中 TemplateRef 和 ViewContainerRef 的定义和作用。希望通过这篇文章,读者能更好的理解 TemplateRef 与 ViewContainerRef。

Angular 2 Decorators part - 3

在 Angular 2 Decorators part -1 和 part -2 文章中,我们介绍了 Decorator 的分类和 Angular 2 常见的内置装饰器,并且我们深入分析了 ComponentDecorator 内部工作原理。此外,我们还发现在 TypeDecorator 类装饰器内部,使用了 Reflect 对象提供的 getOwnMetadata 和 defineMetadata 方法,实现 metadata 信息的读取和保存。具体可参照下图:
decorators-in-angular2

Angular 2 metadata 类型分为:

  • annotations
  • design:paramtypes
  • propMetadata
  • parameters

(备注:其中 design:paramtypes 和 parameters metadata 类型是用于依赖注入)

接下来我们来看一下具体示例:
decorators-in-angular2-1

以上代码成功运行后,在浏览器控制台中,输入 window['core-js_shared'] 即可查看 AppComponent 相关连的 metadata 信息:
decorator-in-angular2-2

示例代码

import { Component, Inject, ViewChild, HostListener, ElementRef } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `<h1 #greet> Hello {{ name }} </h1>`,
})
export class AppComponent {
  name = 'Angular';

  @ViewChild('greet')
  private greetDiv: ElementRef;

  @HostListener('click', ['$event'])
  onClick($event: any) {
    console.dir($event);
  }

  constructor(public appService: AppService,
    @Inject(CONFIG) config: any) {
  }
}

编译后的 ES 5 代码片段:

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {...};
var __metadata = (this && this.__metadata) || function (k, v) {...};
var __param = (this && this.__param) || function (paramIndex, decorator) {...};
  
var AppComponent = (function () {
    // AppComponent构造函数
    function AppComponent(appService, config) {
        this.appService = appService;
        this.name = 'Angular';
    }
  
    AppComponent.prototype.onClick = function (event) {
        console.dir(event);
    };
  
    __decorate([
        core_1.ViewChild('greet'), 
        __metadata('design:type', core_1.ElementRef) // 标识greetDiv属性类型
    ], AppComponent.prototype, "greetDiv", void 0);
  
    __decorate([
        core_1.HostListener('click', ['$event']), 
        __metadata('design:type', Function),  // 标识onClick类型
        __metadata('design:paramtypes', [Object]),  // 标识onClick参数类型
        __metadata('design:returntype', void 0) // 标识返回值类型
    ], AppComponent.prototype, "onClick", null);
  
    AppComponent = __decorate([
        core_1.Component({ // 调用ComponentDecoratorFactory返回TypeDecorator
            selector: 'my-app',
            template: "<h1 #greet> Hello {{ name }} </h1>",
        }),
        __param(1, core_1.Inject(config_1.CONFIG)), 
        __metadata('design:paramtypes', [app_service_1.AppService, Object])
    ], AppComponent);
    return AppComponent;
}());
exports.AppComponent = AppComponent;

总结

本文主要介绍了 angular 2 中 metadata 分类,并通过一个实际的案例,阐述了 Angular 2 内部装饰器与 metadata 之间的映射关系。window['core-js_shared'] 对象内保存的 metadata 信息,是 Angular 2 依赖注入的基础,也为我们揭开了 Angular 2 依赖注入神秘的面纱。

Angular 2 DI - 控制反转和依赖注入及在Angular 1.x 中的应用

IoC 是什么

Ioc - Inversion of Control , 即"控制反转"。在开发中, IoC 意味着你设计好的对象交给容器控制,而不是使用传统的方式,在对象内部直接控制。  

如何理解好 IoC 呢?理解好 IoC的关键是要明确"谁控制谁,控制什么,为何是反转(有反转就应该有正转),哪些方面反转了",我们来深入分析一下。  

  • 谁控制谁,控制什么: 在传统的程序设计中,我们直接在对象内部通过 new 的方式创建对象,是程序主动创建依赖对象;而 IoC 是有专门一个容器来创建这些对象,即由 IoC 容器控制对象的创建;谁控制谁?当然是 IoC 容器控制了对象;控制什么?主要是控制外部资源获取。
  • 为何是反转了,哪些方面反转了: 有反转就有正转,传统应用程序是由我们自己在对象中主动控制去获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转了;哪些方面反转了?依赖对象的获取被反转了。

IoC 能做什么

Ioc 不是一种技术,只是一种**,一个重要的面向对象编程法则,它能指导我们如何设计松耦合、更优良的系统。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了 IoC 容器后,把创建和查找依赖对象的控制权交给了容器,由容器注入组合对象,所以对象之间是松散耦合,这样也便于测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。  

其实 IoC 对编程带来的最大改变不是从代码上,而是**上,发生了"主从换位"的变化。应用程序本来是老大,要获取什么资源都是主动出击,但在 IoC**中,应用程序就变成被动了,被动的等待 IoC 容器来创建并注入它所需的资源了。    

IoC 和 DI

DI - Dependency Injection,即"依赖注入":组件之间的依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。  

理解 DI 的关键是:"谁依赖了谁,为什么需要依赖,谁注入了谁,注入了什么",那我们来深入分析一下:  

  • 谁依赖了谁:当然是应用程序依赖 IoC 容器
  • 为什么需要依赖:应用程序需要 IoC 容器来提供对象需要的外部资源
  • 谁注入谁:很明显是 IoC 容器注入应用程序依赖的对象
  • 注入了什么:注入某个对象所需的外部资源(包括对象、资源、常量数据)

IoC 和 DI 有什么关系?其实它们是同一个概念的不同角度描述,由于控制反转的概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护依赖关系),所以 2004 年大师级人物 Martin Fowler 又给出了一个新的名字:"依赖注入",相对 IoC 而言,"依赖注入" 明确描述了被注入对象依赖 IoC 容器配置依赖对象。  

总的来说, 控制反转(Inversion of Control)是说创建对象的控制权发生转移,以前创建对象的主动权和创建时机由应用程序把控,而现在这种权利转交给 IoC 容器,它就是一个专门用来创建对象的工厂,你需要什么对象,它就给你什么对象。有了 IoC 容器,依赖关系就改变了,原先的依赖关系就没了,它们都依赖 IoC容器了,通过 IoC 容器来建立它们之间的关系。  

DI 在 angular1 中的应用  

angular1 中声明依赖项的方式有3种,分为如下:  

// 方式一: 使用 $inject annotation 方式
var fn = function (a, b) {};
fn.$inject = ['a', 'b'];

// 方式二: 使用 array-style annotations 方式
var fn = ['a', 'b', function (a, b) {}];

// 方式三: 使用隐式声明方式 
var fn = function (a, b) {}; // 不推荐

为了支持以上多种声明方式,angular1 内部使用 annotate 函数来解析依赖项,该函数的实现如下:

var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m; // 匹配参数列表
var FN_ARG_SPLIT = /,/; // 参数分隔符
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; // 匹配参数项
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; // 去除 // 或 /**/注释

function extractArgs(fn) { // 抽取参数列表
  var fnText = fn.toString().replace(STRIP_COMMENTS, ''), // 去除注释
      args = fnText.match(ARROW_ARG) || fnText.match(FN_ARGS);
  return args;
}

function anonFn(fn) {
  var args = extractArgs(fn);
  if (args) {
    return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')';
  }
  return 'fn';
}

function annotate(fn, strictDi, name) {
  var $inject,
      argDecl,
      last;
      
  if (typeof fn === 'function') {
    if (!($inject = fn.$inject)) { // 判断是否使用$inject方式声明依赖项
      $inject = [];
      if (fn.length) {
        if (strictDi) { // 使用严格注入模式,即不能使用隐式声明方式
         // 函数名非字符串或为falsy值(如undefined、null),未设置时默认值为undefined 
          if (!isString(name) || !name) { 
            name = fn.name || anonFn(fn);
          }
          throw $injectorMinErr('strictdi',
            '{0} is not using explicit annotation and cannot be 
                 invoked in strict mode', name);
        }
        argDecl = extractArgs(fn); // 处理隐式声明方式
        forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) {
          arg.replace(FN_ARG, function(all, underscore, name) {
              $inject.push(name);
          });
        });
      }
      fn.$inject = $inject;
    }
  } else if (isArray(fn)) { // 使用 array-style annotations 方式
    last = fn.length - 1; // 获取fn函数
    assertArgFn(fn[last], 'fn');
    $inject = fn.slice(0, last); // 获取依赖项
  } else {
    assertArgFn(fn, 'fn', true);
  }
  return $inject; // 返回依赖数组
}

angular1 内部通过调用 annotate 函数,获取函数的依赖列表(即依赖数组)后,应该如何获取每个项对应的依赖对象呢?我们来进一步分析一下:  

假设我们使用 array-style annotations 方式声明 fn 函数: 

var fn = ['a', 'b', function (a, b) {}]

调用annotate函数后,我们获得 fn 的依赖列表,即返回 ['a','b']。

获取依赖列表后,我们就能够根据依赖项的名称来获取对应的依赖对象。因此,依赖名与依赖对象的存储方式应该是使用 Key - Value 的方式进行存储(在 ES5 中我们可以使用对象字面量,如 var cache = {} 实现 K-V 存储)。在 angular1 内部提供了一个 getService 方法,用来获取依赖对象。它的具体实现如下:  

var INSTANTIATING = {}, // 是否实例化中
	providerSuffix = 'Provider', // provider后缀
	path = []; // 依赖路径

var factory = function(serviceName, caller) { // 实例工厂
   var provider = providerInjector.get(serviceName + providerSuffix, caller);
   return instanceInjector.invoke(provider.$get, provider, undefined, 
    	serviceName);
});

function getService(serviceName, caller) {
      if (cache.hasOwnProperty(serviceName)) { // 依赖对象已创建
        if (cache[serviceName] === INSTANTIATING) {// 判断是否存在循环依赖
          throw $injectorMinErr('cdep', 'Circular dependency found:   
              {0}',serviceName + ' <- ' + path.join(' <- '));
        }
        return cache[serviceName];
      } else { // 依赖对象未创建
        try {
          path.unshift(serviceName); // 用于跟踪依赖路径
          cache[serviceName] = INSTANTIATING;
          // 实例化 serviceName 对应的依赖对象并存储
          return cache[serviceName] = factory(serviceName, caller);
        } catch (err) {
          if (cache[serviceName] === INSTANTIATING) {
            delete cache[serviceName]; // 实例化失败,从缓存中移除
          }
          throw err;
        } finally {
          path.shift();
        }
      }
    }

通过 getService 的实现方式,我们可以知道,若依赖对象已存在,我们直接从缓存中获取,如果依赖对象不存在,我们通过调用 serviceName 对象的provider来创建依赖对象,然后保存在对象实例缓存中。这样的话,间接说明了一个问题,即在 angular1 中,所有的依赖对象都是单例。  

这里我们先稍微解释一下Provider,然后再来列举 angular1 DI系统存在的一些问题。  

什么是Provider ?在 angular1 中,Provider是一个包含 $get 属性的普通 JS 对象。创建 provider 有两种方式:  

// 方式一: 使用对象方式
module.provider('a',{
  $get: function () {
     return 42;
   }
});

// 方式二: 使用构造函数方式
module.provider('a', function AProvider() {
   this.$get = function() { return 42; };
});

以上两种方式都是使用 module 对象提供的provider方法来注册 provider,angular1 中 provider 的具体实现如下:  

function provider(name, provider_) {
    // provider 的名称不能为hasOwnProperty
    assertNotHasOwnProperty(name, 'service');
    // 构造函数方式,先进行实例化
    if (isFunction(provider_) || isArray(provider_)) {
      provider_ = providerInjector.instantiate(provider_);
    }
    if (!provider_.$get) { // 判断 provider_ 对象是否存在 $get属性
      throw $injectorMinErr('pget', "Provider '{0}' must define $get factory 
      	method.", name);
    }
 // 使用 name + "Provider"作为 Key 值,保存在 providerCache 中,用于创建实例
    return providerCache[name + providerSuffix] = provider_;
  }

angular1 DI 系统存在的问题

  • 内部缓存: angular1 应用程序中所有的依赖项都是单例,我们不能控制是否使用新的实例
  • 命名空间冲突: 在系统中我们使用字符串来标识 service 的名称,假设我们在项目中已有一个 CarService,然而第三方库中也引入了同样的服务,这样的话就容易出现混淆
  • DI 耦合度太高: angular1 中 DI 功能已经被框架集成了,我们不能单独使用它的 DI 特性
  • 未能和模块加载器结合: 在浏览器环境中,很多场景都是异步的过程,我们需要的依赖模块并不是一开始就加载好的,或许我们在创建的时候才会去加载依赖模块,再进行依赖创建,而 angualr 的 IoC 容器没法做到这点。  

总结  

本文首先介绍了 IoC 和 DI 的概念及作用,然后讲述了 DI 在 angular1 中的实际应用。此外,简单的介绍了, angular1 DI 的实现方式,但并未深入介绍 angular1 中的 injector ,有兴趣的同学可以自行了解一下。最后,我们介绍了 angular1 DI 系统中存在的问题,这样为我们后面学习 angular2 DI 系统做好了铺垫,我们能更好地理解它设计的意图。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.