从 TypeScript 到 JavaScript

在 Angular 中,TypeScript 可以做的任何事,也可以用 JavaScript 实现。 将一种语言翻译成另一种语言,主要是改变了组织代码和访问 Angular API 的方式。

TypeScript 在 Angular 开发中比较流行。 互联网上和本网站中的大多数范例都是用 TypeScript 写的。

目录

运行在线例子,比较 TypeScript / 可下载的例子 版和 JavaScript / 可下载的例子 版的代码。

TypeScript to ES6 to ES5

##TypeScriptES6ES5

TypeScript ES6 JavaScript 类型化的超集ES6 JavaScriptES5 JavaScript 的超集。ES5 是可以在所有现代浏览器中运行的 JavaScript。

降级的过程是

TypeScript 翻译到 带装饰器的 ES6 时,移除了类属性访问修饰符,如publicprivate。 移除了大部分的类型声明,如:string:boolean。 但保留了用于依赖注入的构造函数参数类型

带装饰器的 ES6 翻译到普通 ES6 时,移除了所有的装饰器和剩下的类型。 必须在构造函数中声明属性(this.title = '...'),而不是在类的代码体中。

最后,普通 ES6 翻译成 ES5,缺少的主要特性是importclass声明。

普通 ES6 的翻译,可以从类似 TypeScript 快速开始的设置开始, 调整相应代码。然后用 Babel 进行转译,使用es2015预设值。 要在 Babel 中使用装饰器和注释,还需安装angular2预设值。

导入和导出

导入 Angular 代码

TypeScriptES6 中,可以使用 ES6 import语句导入 Angular 类、函数和其它成员。

ES5 中,通过全局ng对象访问 Angular 包中的 Angular 实体。 凡是可以从@angular导入的,都是该ng对象的嵌套成员。

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import {
  LocationStrategy,
  HashLocationStrategy
} from '@angular/common';

导出应用代码

TypeScriptES6 Angular 应用中每个文件都构成一个 ES6 模块。 当想要让某个东西对其它模块可用时,就export它。

ES5 不支持模块。在 Angular ES5 应用中,需要在index.html中添加<script>标签,手工加载每个文件。

<script>标签的顺序通常很重要。 必须在引用实体的文件之前,加载定义该公共 JavaScript 实体的文件。

ES5 中,最佳实践是,创建某种形式的模块化,避免污染全局作用域。 添加一个应用命名空间对象(如app)到全局的document。 接着,每个代码文件都通过附加到该命名空间来“导出”公共实体,例如,app.HeroComponent。 可以把一个大型应用中分解成多个子命名空间,可以象这样进行“导出”,app.heroQueries.HeroComponent

每个 ES5 文件都应包裹在立即调用函数表达式 (IIFE) 中, 防止把私有符号无意地泄漏到全局作用域。

下面是HeroComponent定义和“导出”的四种不同语言变种。

  1. export class HeroComponent {
  2. title = 'Hero Detail';
  3. getName() {return 'Windstorm'; }
  4. }

导入应用代码

TypeScriptES6 应用中,可以导入 (import) 其它模块已导出的东西。

ES5 中,使用共享的命名空间对象访问其它文件“导出”的实体。

import { HeroComponent } from './hero.component';

还可以在 Angular JavaScript 项目中使用模块加载器,如 Webpack 或 Browserify。 在这样的项目中,使用 CommonJS 模块和require函数来加载 Angular 框架代码。 用module.exportsrequire导入和导出应用代码。

类和类的元数据

大多数 Angular TypeScriptES6 代码是写成了类。

TypeScript 类的属性和方法参数可以用访问修饰符privateinternalpublic标记。 当翻译成 JavaScript 时,移除这些修饰符。

当翻译成 JavaScript 时,移除大多数类型声明(如,:string:boolean)。 当翻译成带装饰器的 ES6 时,不移除构造函数参数类型!

看一下 TypeScript 属性声明中的类型。通常,最好用缺省值初始化这些属性,因为许多浏览器的 JavaScript 引擎可生成更高性能的代码。当 TypeScript 代码遵循这一建议时,它可以推导出属性类型,翻译时就不需要移除任何内容。

不带装饰器的 ES6 中,类的属性必须在构造函数中指定。

ES5 JavaScript 没有类。 使用构造函数模式,把方法添加到 prototype 中。

  1. export class HeroComponent {
  2. title = 'Hero Detail';
  3. getName() {return 'Windstorm'; }
  4. }

元数据

当用 TypeScript带装饰器的 ES6 编写代码时,使用一个或多个装饰器 (decorator) 来修饰类, 提供配置和元数据。

普通 ES6 中,通过向附加一个annotations数组来提供元数据。

ES5中,也是提供一个annotations数组,但把它附加到构造函数,而不是类。

看一下这些变种:

  1. import { Component } from '@angular/core';
  2. @Component({
  3. selector: 'hero-view',
  4. template: '<h1>{{title}}: {{getName()}}</h1>'
  5. })
  6. export class HeroComponent {
  7. title = 'Hero Detail';
  8. getName() {return 'Windstorm'; }
  9. }

外部模块文件

大的组件模板通常是放在独立的文件中。

src/app/hero-title.component.html

<h1>{{titlePrefix}} {{title}}</h1>
<button (click)="ok()">OK</button>
<p>{{ msg }}</p>

接着,组件(这里是HeroTitleComponent)在它的元数据templateUrl属性中引用该模板文件:

@Component({
  selector: 'hero-title',
  templateUrl: './hero-title.component.html'
})

注意,TypeScript 和两个ES6templateUrl属性相对于组件模块来标识模板文件的位置。

ES5 领域专用语言

创建构造函数并用元数据对它进行注释,是一个常见的 ES5 模式,Angular 提供了一套方便的 API,使代码更简洁, 并且元数据也刚好位于构造函数的上方,看起来就像 TypeScript带装饰器的 ES6 写的代码。

这个 API (Application Programming Interface,应用编程接口) 通常称作 ES5 DSL (Domain Specific Language,领域专用语言)。

ng.core.Component函数调用的结果设置到应用命名空间属性,如app.HeroDslComponent。 向ng.core.Component传递与之前一样的元数据对象。 接着,在调用链上调用Class函数,它接收一个对象,其中定义了类的构造函数和实例方法。

下例中的HeroComponent,用 DSL 进行了重写,跟原来的 ES5 版本进行对比一下:

  1. app.HeroComponent = ng.core.Component({
  2. selector: 'hero-view-dsl',
  3. template: '<h1>{{title}}: {{getName()}}</h1>',
  4. })
  5. .Class({
  6. constructor: function HeroComponent() {
  7. this.title = "Hero Detail";
  8. },
  9. getName: function() { return 'Windstorm'; }
  10. });
命名构造函数

如果组件抛出运行时异常,命名的构造函数在控制台日志中显示得更清楚。 未命名的构造函数显示为匿名函数(如,class0),不可能在源代码中找到它。

具有 getter 和 setter 的属性

TypeScriptES6 支持 getter 和 setter。 下面是 TypeScript 只读属性的例子,它有一个 getter,为下一次点击状态准备切换按钮的标签:

ts/src/app/hero-queries.component.ts

get buttonLabel() {
  return this.active ? 'Deactivate' : 'Activate';
}

这个 TypeScript "getter" 属性会翻译成 ES5 已定义属性ES5 DSL 不直接支持已定义属性,你仍可提取“类”原型,象下面这样添加已定义属性

js/src/app/hero-queries.component.ts

// add prototype property w/ getter outside the DSL
var proto = app.heroQueries.HeroQueriesComponent.prototype;
Object.defineProperty(proto, "buttonLabel", {
    get: function () {
        return this.active ? 'Deactivate' : 'Activate';
    },
    enumerable: true
});

用于其它类的 DSL

其它被装饰的类也有类似的DSL,可以用ng.core.Directive定义指令:

app.MyDirective = ng.core.Directive({
  selector: '[myDirective]'
}).Class({
  ...
});

ng.core.Pipe添加一个管道:

app.MyPipe = ng.core.Pipe({
  name: 'myPipe'
}).Class({
  ...
});

接口

TypeScript用于确保一个类正确地实现了接口成员。 在适当的地方,我们强烈推荐使用 Angular 接口。 例如,实现了ngOnInit生命周期钩子方法的组件类应实现OnInit接口。

TypeScript 接口只是为了方便开发人员,Angular 在运行时并不使用它。 它们在生成的 JavaScript中并不存在。 当从 TypeScript 翻译成 JavaScript 时,只保留了实现方法,而忽略接口。

  1. import { Component, OnInit } from '@angular/core';
  2. @Component({
  3. selector: 'hero-lifecycle',
  4. template: `<h1>Hero: {{name}}</h1>`
  5. })
  6. export class HeroComponent implements OnInit {
  7. name: string;
  8. ngOnInit() {
  9. // todo: fetch from server async
  10. setTimeout(() => this.name = 'Windstorm', 0);
  11. }
  12. }

输入和输出元数据

输入和输出装饰器

TypeScript带装饰器的 ES6 中,经常会用属性装饰器往类的属性上添加元数据。 例如,向公共类属性添加@Input@Output属性装饰器 , 会使这些属性成为父组件绑定表达式的目标。

ES5普通 ES6 中,没有等价的属性装饰器。 幸运的是,每个属性装饰器在类的装饰器元数据属性中有等价的表示形式。 TypeScript@Input属性装饰器可以表示为Component元数据的inputs数组中的一项。

你已经知道如何用任意的 JavaScript 方言添加ComponentDirective元数据, 所以添加另一个属性也没什么新鲜的。 但要注意的是,用于每个类属性的那些分离@Input@Output属性装饰器,都合并到了inputsoutputs数组中。

  1. @Component({
  2. selector: 'app-confirm',
  3. templateUrl: './confirm.component.html'
  4. })
  5. export class ConfirmComponent {
  6. @Input() okMsg = '';
  7. @Input('cancelMsg') notOkMsg = '';
  8. @Output() ok = new EventEmitter();
  9. @Output('cancel') notOk = new EventEmitter();
  10. onOkClick() {
  11. this.ok.emit(true);
  12. }
  13. onNotOkClick() {
  14. this.notOk.emit(true);
  15. }
  16. }

上例中,其中一个面向公共的绑定名 (cancelMsg),不同于相应的类属性名 (notOkMsg)。 这样做没有问题,但必须把它告诉 Angular,这样 Angular 才能把cancelMsg的外部绑定映射到组件的notOkMsg属性。

TypeScript带装饰器的 ES6 中,在属性装饰器的参数中指定特定的绑定名。

ES5普通 ES6 中,用propertyName: bindingName语法表示在类的元数据中。

依赖注入

Angular 严重依赖依赖注入来为它创建的对象提供服务。 当 Angular 创建一个新组件、指令、管道或其它服务时, 它把注入器提供的服务的实例传递给类的构造函数参数。

开发人员必须告诉 Angular 向每个参数中注入什么。

按类的类型注入

TypeScript带装饰器的 ES6 中,最简单和流行的技术是把构造函数参数的类型设置为待注入服务的类。

TypeScript 转译器把参数类型信息写进生成的 JavaScript。 Angular 在运行时读取该信息,并在适当的注入器中定位相应的服务。 带装饰器的 ES6 转译器本质上也使用同样的参数类型语法,做同样的工作。

ES5普通 ES6 缺少类型,必须向构造函数附加parameters数组来标识“可注入对象”。 数组中的每一项指定一个服务的注入令牌。

TypeScript 中,最常用的令牌是类,而ES5普通 ES6 使用构造函数表示一个类。 因此,parameters数组会有所不同:

当用 ES5 DSL 时,把Class.constructor属性设置为一个数组,它的前面的参数是 注入的服务,最后一个参数是类构造函数本身。 AngularJS 的开发人员对这种形式应该很熟悉。

  1. import { Component } from '@angular/core';
  2. import { DataService } from './data.service';
  3. @Component({
  4. selector: 'hero-di',
  5. template: `<h1>Hero: {{name}}</h1>`
  6. })
  7. export class HeroComponent {
  8. name = '';
  9. constructor(dataService: DataService) {
  10. this.name = dataService.getHeroName();
  11. }
  12. }

用 @Inject 装饰器注入

有时,依赖注入的令牌不是类或构造函数。

TypeScript带装饰器的 ES6 中,可以在类的构造函数参数前调用@Inject()装饰器来指定注入令牌。 在这个例子中,这个令牌是字符串'heroName'

其它 JavaScript 方言是通过向类的构造函数添加parameters数组。 其中的每一项是Inject的实例。

当用 ES5 DSL 时,象前面那样把Class.constructor属性设置为函数定义数组。 为每个参数创建一个ng.core.Inject(token)

  1. @Component({
  2. selector: 'hero-di-inject',
  3. template: `<h1>Hero: {{name}}</h1>`
  4. })
  5. export class HeroComponent {
  6. constructor(@Inject('heroName') private name: string) { }
  7. }

其它注入装饰器

可以使用@angular/core中的注入装饰器来限定注入行为。

TypeScript带装饰器的 ES6 中,可以将下列注入限定符加在构造函数参数前面:

ES5普通 ES6 中,通过在parameters数组中创建一个嵌套数组,创建等价的注入限定符实例。

当用 ES5 DSL 时,象前面那样把Class.constructor属性设置为函数定义数组。 用嵌套数组来定义参数完整的注入规格说明。

  1. @Component({
  2. selector: 'hero-title',
  3. templateUrl: './hero-title.component.html'
  4. })
  5. export class HeroTitleComponent {
  6. msg: string = '';
  7. constructor(
  8. @Inject('titlePrefix') @Optional() private titlePrefix: string,
  9. @Attribute('title') private title: string
  10. ) { }
  11. ok() {
  12. this.msg = 'OK!';
  13. }
  14. }

上例中,'titlePrefix'令牌没有提供商。 如果没有Optional,Angular 将抛出错误。 加上Optional,Angular 将构造函数参数设置为null, 组件显示没有前缀的标题。

宿主绑定

Angular 支持绑定到宿主元素的属性和事件, 宿主元素是那些标签匹配组件选择器的元素。

宿主装饰器

TypeScript带装饰器的 ES6 中,可以使用宿主属性装饰器把宿主元素绑定到组件或指令。 @HostBinding装饰器把宿主元素属性绑定到组件数据属性。 @HostListener装饰器把宿主元素事件绑定到组件事件处理器。

ES5普通 ES6 中,向组件元数据添加host属性可以获得同样的效果。

host的值是一个对象,它的属性是宿主属性和监听器绑定:

  1. @Component({
  2. selector: 'hero-host',
  3. template: `
  4. <h1 [class.active]="active">Hero Host in Decorators</h1>
  5. <div>Heading clicks: {{clicks}}</div>
  6. `,
  7. // Styles within (but excluding) the <hero-host> element
  8. styles: ['.active {background-color: yellow;}']
  9. })
  10. export class HeroHostComponent {
  11. // HostBindings to the <hero-host> element
  12. @HostBinding() title = 'Hero Host in Decorators Tooltip';
  13. @HostBinding('class.heading') headingClass = true;
  14. active = false;
  15. clicks = 0;
  16. // HostListeners on the entire <hero-host> element
  17. @HostListener('click')
  18. clicked() {
  19. this.clicks += 1;
  20. }
  21. @HostListener('mouseenter', ['$event'])
  22. enter(event: Event) {
  23. this.active = true;
  24. this.headingClass = false;
  25. }
  26. @HostListener('mouseleave', ['$event'])
  27. leave(event: Event) {
  28. this.active = false;
  29. this.headingClass = true;
  30. }
  31. }

宿主元数据

一些开发人员更喜欢在组件元数据中指定宿主属性和监听器。 它们宁愿采用这种方式,也是 ES5普通 ES6 中必须采用的方式。

下面重新实现了HeroComponent,它提醒我们,在 TypeScript带装饰器的 ES6 中, 任何属性元数据装饰器都可以表示为组件或指令元数据。

  1. @Component({
  2. selector: 'hero-host-meta',
  3. template: `
  4. <h1 [class.active]="active">Hero Host in Metadata</h1>
  5. <div>Heading clicks: {{clicks}}</div>
  6. `,
  7. host: {
  8. // HostBindings to the <hero-host-meta> element
  9. '[title]': 'title',
  10. '[class.heading]': 'headingClass',
  11. // HostListeners on the entire <hero-host-meta> element
  12. '(click)': 'clicked()',
  13. '(mouseenter)': 'enter($event)',
  14. '(mouseleave)': 'leave($event)'
  15. },
  16. // Styles within (but excluding) the <hero-host-meta> element
  17. styles: ['.active {background-color: coral;}']
  18. })
  19. export class HeroHostMetaComponent {
  20. title = 'Hero Host in Metadata Tooltip';
  21. headingClass = true;
  22. active = false;
  23. clicks = 0;
  24. clicked() {
  25. this.clicks += 1;
  26. }
  27. enter(event: Event) {
  28. this.active = true;
  29. this.headingClass = false;
  30. }
  31. leave(event: Event) {
  32. this.active = false;
  33. this.headingClass = true;
  34. }
  35. }

视图和子组件装饰器

有几个属性装饰器可用于查询组件的嵌套视图和内容组件。

视图子组件与出现在组件模板的元素标签相关联。

内容子组件与出现在组件元素标签之间的那些元素相关联, 它们被投影到组件模板的<ng-content>中。

@ViewChild@ViewChildren 属性装饰器允许组件查询位于其视图中的其它组件的实例。

ES5ES6 中,通过向组件元数据添加queries属性来访问组件的视图子组件。 queries属性是一个映射表。

  1. @Component({
  2. selector: 'hero-queries',
  3. template: `
  4. <view-child *ngFor="let hero of heroData" [hero]="hero">
  5. <content-child></content-child>
  6. </view-child>
  7. <button (click)="activate()">{{buttonLabel}} All</button>
  8. `
  9. })
  10. export class HeroQueriesComponent {
  11. active = false;
  12. heroData = [
  13. {id: 1, name: 'Windstorm'},
  14. {id: 2, name: 'LaughingGas'}
  15. ];
  16. @ViewChildren(ViewChildComponent) views: QueryList<ViewChildComponent>;
  17. activate() {
  18. this.active = !this.active;
  19. this.views.forEach(
  20. view => view.activate()
  21. );
  22. }
  23. get buttonLabel() {
  24. return this.active ? 'Deactivate' : 'Activate';
  25. }
  26. }

@ContentChild@ContentChildren 装饰器允许组件查询从其它地方投影到视图里的其它组件的实例。

添加它们的方式与@ViewChild@ViewChildren 相同。

  1. @Component({
  2. selector: 'view-child',
  3. template: `
  4. <h2 [class.active]=active>
  5. {{hero.name}}
  6. <ng-content></ng-content>
  7. </h2>`,
  8. styles: ['.active {font-weight: bold; background-color: skyblue;}']
  9. })
  10. export class ViewChildComponent {
  11. @Input() hero: any;
  12. active = false;
  13. @ContentChild(ContentChildComponent) content: ContentChildComponent;
  14. activate() {
  15. this.active = !this.active;
  16. this.content.activate();
  17. }
  18. }

TypeScript带装饰器的 ES6 中,还可以使用queries元数据代替 @ViewChild@ContentChild属性装饰器。

只用于 TypeScript 的预编译

Angular 模板编译有两种方式:JiT (Just-in-Time,即时编译) 和 AoT (Ahead-of-Time,预编译)。 目前,预编译只能用于 TypeScript 应用,因为(部分原因)它生成的中间结果是 TypeScript 文件。 当前,预编译不能用于纯 JavaScript 应用