使用Angular(Angular 2 及以上版本)开发程序时,装饰是一个核心概念。还有一个正式的TC39 提案,目前处于阶段2中,该提案期望装饰器能够很快成为JavaScript 的核心语言功能。 回到Angular ,Angular 的内部代码广泛使用了装饰器,本篇文章中我们将学习不同类型的装饰器和它们的源码并且了解它们是如何工作的。 我第一次接触到TypeScript 和装饰器的时候,我不知道我为什么需要它们,但是当你稍微往深处发掘的时候你才能了解到了创建装饰器的好处(不仅是在Angular 中)。 在AngularJS 中没有使用装饰器,而是使用了不同的注册方法——例如用 .component() 方法定义一个组件。那为什么Angular 选择使用装饰器呢?让我们开始探索吧!
Angular 装饰器
在我们创建装饰器和了解为什么Angular 使用它们之前,我们先看看Angular 提供的不同类型的装饰器。主要右四个类型: 类装饰器,例如@Component 和@NgModule。 属性内部的属性装饰器,例如@Input 和 @Output。 方法内部的方法装饰器,例如@HostListener。 类构造函数中参数的参数装饰器,例如 @Inject 每个装饰器都有一个独特的作用,让我们看几个示例来扩展上面的列表。
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!');
}
}
@NgModule({
imports: [],
declarations: []
})
export class ExampleModule {
constructor() {
console.log('Hey I am a module!');
}
}
请注意,不管这两个类本身是如何的它们实际上是相同的。在类中不需要任何代码去告知Angluar 这个类是component 还是module。我们需要做的只是修饰这个类,余下的工作交给Angular 就可以了。
import { Component, Input } from '@angular/core'; @Component({ selector: 'example-component', template: '<div>Woo a component!</div>' }) export class ExampleComponent { @Input() exampleProperty: string; }
然后我们通过一个组件属性绑定来传递输入绑定:
<example-component [exampleProperty]="exampleData"> </example-component>
属性装饰器会在ExampleComponentdefinition内发生“魔术”。 在AngularJS 1.x(我打算在这里也使用TypeScript,只是为了声明一个类的属性),我们有一个不同的机制,使用scope或bindToController与指令,并在新的组件方法中bindings:
const exampleComponent = { bindings: { exampleProperty: '<' }, template: ` <div>Woo a component!</div> `, controller: class ExampleComponent { exampleProperty: string; $onInit() { // access this.exampleProperty } } }; angular .module('app') .component('exampleComponent', exampleComponent);
您可以在上面看到,如果我们扩展,重构或更改组件的API绑定和类内的属性名称,我们有两个单独的属性可以维护。然而,在Angular中,有一个属性exampleProperty被装饰,随着我们的代码库的增长,这个属性更容易更改,维护和追踪。
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 } }
深入挖掘依赖注入(DI),令牌,@Inject和@Injectable,可以看看我以前的文章。
参数装饰器允许我们在我们的类构造函数中修饰参数。 这个例子是@Inject,它让我们告诉Angular我们想要什么参数来启动:
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) { console.log(myService); // MyService } }
由于TypeScript公开接口允许给我们使用元数据,我们实际上并不需要这么做。 我们可以让TypeScript和Angular通过指定要注入的作为参数类型来完成我们的辛苦工作:
import { Component } from '@angular/core'; import { MyService } from './my-service'; @Component({ selector: 'example-component', template: '<div>Woo a component!</div>' }) export class ExampleComponent { constructor(myService: MyService) { console.log(myService); // MyService } }
现在我们已经介绍了我们可以使用的装饰器类型,让我们深入了解他们正在做的事情 – 以及为什么我们需要它们。
function Console(target) { console.log('Our decorated class', target); }
在这里,我们已经创建了控制台(Angular通常使用大写命名约定),并指定一个名为目标的参数。目标参数实际上是我们装饰的类,这意味着我们现在可以用装饰器来装饰任何类,并在控制台中看到它的输出结果:
@Console class ExampleClass { constructor() { console.log('Yo!'); } }
想要看到实际操作?看看现场演示。
@Console('Hey!') class ExampleClass { constructor() { console.log('Yo!'); } }
如果我们现在运行这个代码,我们只会得到’Hey!’。这是因为我们的装饰器没有返回给予类的函数。 @Console(’Hey!’)的输出是无效的。 我们需要调整我们的控制台代码的装饰器,以返回给予类的函数闭包。这样我们都可以从装饰器(在我们的例子中是字符串Hey!)以及类中获得一个值:
function Console(message) { // access the "metadata" message console.log(message); // return a function closure, which // is passed the class as `target` return function (target) { console.log('Our decorated class', target); } } @Console('Hey!') class ExampleClass { constructor() { console.log('Yo!'); } } // console output: 'Hey!' // console output: 'Our decorated class', class ExampleClass{}...
你可以看到这里的变化。 这是Angular装饰器工作的基础。他们首先获取一个配置值,然后接收类/方法/属性来应用装饰。现在我们对装饰器的功能有了一个简单的了解,我们将介绍Angular如何创建并使用它自己的装饰器。
{ selector: undefined, inputs: undefined, outputs: undefined, host: undefined, exportAs: undefined, moduleId: undefined, providers: undefined, viewProviders: undefined, changeDetection: ChangeDetectionStrategy.Default, queries: undefined, templateUrl: undefined, template: undefined, styleUrls: undefined, styles: undefined, animations: undefined, encapsulation: undefined, interpolation: undefined, entryComponents: undefined }
这里有很多不同的选项,你会注意到只有一个有一个默认值 – changeDetection。这是在创建装饰器时指定的,所以无论何时创建组件,我们都不需要添加它。您可能已经应用这一行代码来修改更改策略:
changeDetection: ChangeDetectionStrategy.OnPush
注释实例在使用装饰器时创建。这会将该装饰器的默认配置(例如上面看到的对象)与您指定的配置合并在一起,例如:
import { NgModule, Component } from '@angular/core'; @Component({ selector: 'example-component', styleUrls: ['example.component.scss'], template: '<div>Woo a component!</div>' }) export class ExampleComponent { constructor() { console.log('Hey I am a component!'); } }
这将创建一个具有以下属性的注释实例:
{ selector: 'example-component', inputs: undefined, outputs: undefined, host: undefined, exportAs: undefined, moduleId: undefined, providers: undefined, viewProviders: undefined, changeDetection: ChangeDetectionStrategy.Default, queries: undefined, templateUrl: undefined, template: '<div>Woo a component!</div>', styleUrls: ['example.component.scss'], styles: undefined, animations: undefined, encapsulation: undefined, interpolation: undefined, entryComponents: undefined }
一旦这个注解实例被创建,它就会被存储,以便Angular可以访问它。
export class TestComponent { @Input() @HostListener('click', ['$event']) onClick: Function; }
与此同时,Angular还可以使用反射API(通常使用反射元数据进行填充)来存储这些注释,并将该类用作数组。 这意味着它可以稍后通过指向该类来获取特定类的所有注释。
class ExampleClass { constructor() { console.log('Yo!'); } }
然后TypeScript把它转换为一个函数:
var ExampleClass = (function () { function ExampleClass() { console.log('Yo!'); } return ExampleClass; }());
现在,如果我们加入装饰器装饰我们的类,我们可以看到实际应用的装饰器。
@ConsoleGroup('ExampleClass') class ExampleClass { constructor() { console.log('Yo!'); } }
然后TypeScript输出:
var ExampleClass = (function () { function ExampleClass() { console.log('Yo!'); } return ExampleClass; }()); ExampleClass = __decorate([ ConsoleGroup('ExampleClass') ], ExampleClass);