使用Angular(Angular 2 及以上版本)開發程序時,裝飾是一個核心概念。還有一個正式的TC39 提案,目前處于階段2中,該提案期望裝飾器能夠很快成為JavaScript 的核心語言功能。
回到Angular ,Angular 的內部代碼廣泛使用了裝飾器,本篇文章中我們將學習不同類型的裝飾器和它們的源碼并且了解它們是如何工作的。
我第一次接觸到TypeScript 和裝飾器的時候,我不知道我為什么需要它們,但是當你稍微往深處發掘的時候你才能了解到了創建裝飾器的好處(不僅是在Angular 中)。
在AngularJS 中沒有使用裝飾器,而是使用了不同的注冊方法——例如用?.component()?方法定義一個組件。那為什么Angular 選擇使用裝飾器呢?讓我們開始探索吧!
目錄
Angular 裝飾器
在我們創建裝飾器和了解為什么Angular 使用它們之前,我們先看看Angular 提供的不同類型的裝飾器。主要右四個類型:
類裝飾器,例如@Component和@NgModule。
屬性內部的屬性裝飾器,例如@Input和@Output。
方法內部的方法裝飾器,例如@HostListener。
類構造函數中參數的參數裝飾器,例如@Inject
每個裝飾器都有一個獨特的作用,讓我們看幾個示例來擴展上面的列表。
類裝飾器
Angular 提供了幾個類裝飾器。這些是我們用來表示類的意圖時使用的頂級裝飾器。例如,這些裝飾器允許我們告訴Angluar 一個特定的類是一個組件或者是一個組件。裝飾器允許我們定義類的意圖而不用在類的內部寫實際的代碼。
一個類中的@Component和@NgModule 實例:
import{?NgModule,?Component?}from'@angular/core';@Component({??selector:'example-component',??template:'
請注意,不管這兩個類本身是如何的它們實際上是相同的。在類中不需要任何代碼去告知Angluar 這個類是component 還是module。我們需要做的只是修飾這個類,余下的工作交給Angular 就可以了。
屬性裝飾器
這些可能是第二個最常見的裝飾器了。他們允許我們在我們的類內部裝飾特定的屬性 - 一個非常強大的機制。
我們來看看@Input()。想象一下,我們有一個屬性,我們想要一個輸入綁定。
如果沒有裝飾器,我們必須在我們的類中定義這個屬性,以便TypeScript知道它,然后在其他地方告訴Angular我們有一個屬性,我們希望有一個輸入方法。
使用裝飾器,我們可以簡單地將@Input()裝飾器放在屬性的上方 - Angular的編譯器會自動從屬性名稱創建一個輸入綁定并將它們鏈接起來。
import?{?Component,?Input?}?from?'@angular/core';
@Component({
selector:?'example-component',
template:?'
@Input()
exampleProperty:?string;
}
然后我們通過一個組件屬性綁定來傳遞輸入綁定:
[exampleProperty]="exampleData">?
屬性裝飾器會在ExampleComponentdefinition內發生“魔術”。
在AngularJS 1.x(我打算在這里也使用TypeScript,只是為了聲明一個類的屬性),我們有一個不同的機制,使用scope或bindToController與指令,并在新的組件方法中bindings:
const?exampleComponent?=?{
bindings:?{
exampleProperty:?'<'???},
template:?`
`,
controller:?class?ExampleComponent?{
exampleProperty:?string;
$onInit()?{
//?access?this.exampleProperty?????}
}
};
angular
.module('app')
.component('exampleComponent',?exampleComponent);
您可以在上面看到,如果我們擴展,重構或更改組件的API綁定和類內的屬性名稱,我們有兩個單獨的屬性可以維護。然而,在Angular中,有一個屬性exampleProperty被裝飾,隨著我們的代碼庫的增長,這個屬性更容易更改,維護和追蹤。
裝飾器方法
裝飾器方法與裝飾器屬性非常相似,但是用來寫方法的。 這可以用來在我們的類中修飾特定的方法。 一個很好的例子是@HostListener。 這使我們可以告訴Angular,當我們的主程序發生事件時,我們希望用事件調用裝飾的方法。
import?{?Component,?HostListener?}?from?'@angular/core';
@Component({
selector:?'example-component',
template:?'
@HostListener('click',?['$event'])
onHostClick(event:?Event)?{
//?clicked,?`event`?available???}
}
裝飾器參數
裝飾器的參數十分有趣。 在將基元注入到構造函數中時,您可能遇到過這些問題,您需要手動通知Angular注入特定的提供程序。
深入挖掘依賴注入(DI),令牌,@Inject和@Injectable,可以看看我以前的文章。
參數裝飾器允許我們在我們的類構造函數中修飾參數。 這個例子是@Inject,它讓我們告訴Angular我們想要什么參數來啟動:
import?{?Component,?Inject?}?from?'@angular/core';?import?{?MyService?}?from?'./my-service';
@Component({
selector:?'example-component',
template:?'
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:?'
constructor(myService:?MyService)?{
console.log(myService);?//?MyService???}
}
現在我們已經介紹了我們可以使用的裝飾器類型,讓我們深入了解他們正在做的事情 - 以及為什么我們需要它們。
創建一個裝飾器
如果我們了解一個裝飾器實際上正在做什么,然后再研究Angular如何使用它們,它會使事情變得更容易。要做到這一點,我們可以創建一個快速的裝飾器示例。
裝飾器函數
裝飾器實際上只是一個函數,就這么簡單,并且隨著裝飾器的調用而被調用。一個裝飾器方法被正在被裝飾的方法調用裝飾器的值,并且一個類裝飾器將被被裝飾的類所調用。
讓我們快速做一個裝飾器,我們可以在課堂上進一步證明這一點。這個裝飾器只是簡單地把類記錄到控制臺:
function?Console(target)?{
console.log('Our?decorated?class',?target);
}
在這里,我們已經創建了控制臺(Angular通常使用大寫命名約定),并指定一個名為目標的參數。目標參數實際上是我們裝飾的類,這意味著我們現在可以用裝飾器來裝飾任何類,并在控制臺中看到它的輸出結果:
@Console?class?ExampleClass?{
constructor()?{
console.log('Yo!');
}
}
想要看到實際操作?看看現場演示。
將數據傳遞給裝飾器
當我們在Angular中使用裝飾器時,我們傳遞一些特定于裝飾器的配置。
例如,當我們使用@Component時,我們通過一個對象,并使用@HostListener,通過一個字符串作為第一個參數(事件名稱,比如'click')和可選的字符串數組(如$事件)被傳遞到裝飾的方法里。
讓我們稍微修改我們上面的控制臺代碼來展示如何使用Angular裝飾器。
@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如何創建并使用它自己的裝飾器。
裝飾器實際上做什么
每種類型的裝飾器共享相同的核心功能。 從純粹的裝飾角度來看,@Component和@Directive都以相同的方式工作,就像@Input和@Output一樣。 Angular通過使用每種類型的裝飾器的工廠方法來實現這一點。
讓我們來看看Angular中最常見的裝飾器@Component。
我們不打算用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:?'
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:?'
styleUrls:?['example.component.scss'],
styles:?undefined,
animations:?undefined,
encapsulation:?undefined,
interpolation:?undefined,
entryComponents:?undefined?}
一旦這個注解實例被創建,它就會被存儲,以便Angular可以訪問它。
裝飾器鏈
如果第一次在類上使用裝飾器,它將創建一個新的數組,并將注釋實例推入其中。 如果這不是在類上使用的第一個裝飾器,則將其推送到現有的注釋數組中。 這允許裝飾器被鏈接在一起并且全部存儲在一個地方。
例如,在Angular中,你可以這么寫一個類中的屬性:
export?class?TestComponent?{
@Input()
@HostListener('click',?['$event'])
onClick:?Function;
}
與此同時,Angular還可以使用反射API(通常使用反射元數據進行填充)來存儲這些注釋,并將該類用作數組。 這意味著它可以稍后通過指向該類來獲取特定類的所有注釋。
如何使用裝飾器
所以我們現在知道Angular如何使用以及為什么使用裝飾器,但是他們如何實際應用于一個類?
如前所述,裝飾器本身并不是JavaScript本身 - 目前TypeScript為我們提供了這一功能。 這意味著我們可以檢查編譯的代碼,看看我們使用裝飾器時會發生什么。
以下一個標準的ES6類 -
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);
這給了我們一些關于我們的裝飾器如何應用的實際上下文。
__decorate調用是一個輔助函數,可以在編譯好的文件頂部輸出。 所有這一切能將裝飾器應用到我們的類中(使用ExampleClass作為參數來調用ConsoleGroup('ExampleClass'))。
總結
揭秘裝飾者是理解更多Angular“魔法”和如何使用它們的其中一小步。 他們讓Angular能夠存儲類的元數據,并同時簡化我們的工作流程。