前言
原創文章彙總:github/Nealyangcss
正在着手寫 THE LAST TIME 系列的 Typescript
篇,而Decorator
一直是我我的看來一個很是不錯的切面方案。所謂的切面方案就是咱們常說的切面編程 AOP
。一種編程思想,簡單直白的解釋就是,一種在運行時,動態的將代碼切入到類的指定方法、指定位置上的編程思想就是 AOP。AOP
和咱們熟悉的 OOP
同樣**,只是一個編程範式**,AOP
沒有說什麼規定要使用什麼代碼協議,必需要用什麼方式去實現,這只是一個範式。而 Decorator
也就是AOP
的一種形式。html
而本文重點不在於討論編程範式,主要介紹 Typescript
+Decorator
下圖的一些知識講解,其中包括最近筆者在寫項目的一些應用。前端
介紹
什麼是 Decorator
貌似在去年的時候在公衆號:【全棧前端精選】中,有分享過關於 Decorator
的基本介紹:Decorator 從原理到實戰,裏面有對 Decorator
很是詳細的介紹。node
本質上,它也就是個函數的語法糖。git
Decorator
是 ES7
添加的新特性,固然,在 Typescript
很早就有了。早在此以前,就有提出與 Decorator
思想很是相近的設計模式:裝飾者模式。github
上圖的WeaponAccessory
就是一個Decorator
,他們添加額外的功能到基類上。讓其可以知足你的需求。typescript
簡單的理解 Decorator
,能夠認爲它是一種包裝,對 對象,方法,屬性的包裝。就像 Decorator 俠,一身盔甲,只是裝飾,以知足需求,未改變是人類的本質。編程
爲何要使用 Decorator
爲何要使用 Decorator
,其實就是介紹到 AOP
範式的最大特色了:非侵入式加強。json
好比筆者正在寫的一個頁面容器,叫 PageContainer.tsx
,基本功能包括滾動、autoCell
、事件注入與解綁、placeHolder Container
的添加等基本功能。設計模式
class PageContainer extends Components{ xxx }
這時候我正使用這個容器,想接入微信分享功能。或者錯誤兜底功能。可是使用這個容器的人很是多。分享不必定都是微信分享、錯誤兜底不必定都是張着我想要的樣子。因此我一定要對容器進行改造和加強。
從功能點劃分,這些的確屬於容器的能力。因此在無侵入式的加強方案中,裝飾者模式是一個很是好的選擇。也就是話落到咱們所說的 Decorator
。(對於 React
或者 Rax
,HOC
也是一種很好的方案,固然,其思想是一致的。)
+ @withError + @withWxShare class PageContainer extends Components{ xxx }
咱們添加 Decorator
,這樣的作法,對原有代碼毫無入侵性,這就是AOP
的好處了,把和主業務無關的事情,放到代碼外面去作。
關於 Typescript
JavaScript
毋庸置疑是一門很是好的語言,可是其也有不少的弊端,其中不乏是做者設計之處留下的一些 「bug」。固然,瑕不掩瑜~
話說回來,JavaScript
畢竟是一門弱類型語言,與強類型語言相比,其最大的編程陋習就是可能會形成咱們類型思惟的缺失(高級詞彙,我從極客時間學到的)。而思惟方式決定了編程習慣,編程習慣奠基了編程質量,工程質量劃定了能力邊界,而學習 Typescript
,最重要的就是咱們類型思惟的重塑。
那麼其實,Typescript
在我我的理解,並不能算是一個編程語言,它只是 JavaScript
的一層殼。固然,咱們徹底能夠將它做爲一門語言去學習。網上有不少推薦 or 不推薦 Typescript
之類的文章這裏咱們不作任何討論,學與不學,用或不用,利與弊。各自拿捏~
再說說 typescript
,其實對於 ts
相比你們已經不陌生了。更多關於 ts
入門文章和文檔也是已經爛大街了。此文不去翻譯或者搬運各類 api或者教程章節。只是總結羅列和解惑,筆者在學習 ts 過程當中曾疑惑的地方。道不到的地方,歡迎你們評論區積極討論。
首先推薦下各自 ts 的編譯環境:typescriptlang.org
再推薦筆者收藏的兩個網站:
Typescript 中的 Decorator 簽名
interface TypedPropertyDescriptor<T> { enumerable?: boolean; configurable?: boolean; writable?: boolean; value?: T; get?: () => T; set?: (value: T) => void; } declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void; declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void; declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void; declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;
如上是 ClassDecorator
、PropertyDecorator
以及 MethodDecorator
的三個類型簽名。
基本配置
因爲 Decorator
在 Typescript
中仍是一項實驗性的給予支持,因此在 ts
的配置配置文件中,咱們指明編譯器對 Decorator
的支持。
在命令行或tsconfig.json
裏啓用experimentalDecorators
編譯器選項:
- 命令行:
tsc --target ES5 --experimentalDecorators
- tsconfig.json
{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true } }
類型
在 Typescript
中,Decorator
能夠修飾五種語句:類、屬性、方法、訪問器和方法參數。
class definitions
類裝飾器應用於構造函數之上,會在運行時看成函數被調用,類的構造函數做爲其惟一的參數。
注意,在 Typescript
中的class
關鍵字只是 JavaScript
構造函數的一個語法糖。因爲類裝飾器的參數是一個構造函數,其也應該返回一個構造函數。
咱們先看一下官網的例子:
function classDecorator<T extends { new (...args: any[]): {} }>( constructor: T ) { return class extends constructor { newProperty = "new property"; hello = "override"; }; } @classDecorator class Greeter { property = "property"; hello: string; constructor(m: string) { this.hello = m; } } const greeter: Greeter = new Greeter("world"); console.log({ greeter }, greeter.hello);
{ new (...args: any[]): {} }
表示一個構造函數,爲了看起來清晰一些,咱們也能夠將其聲明到外面:
/** *構造函數類型 * * @export * @interface Constructable */ export interface IConstructable { new (...args:any[]):any }
properties
屬性裝飾器有兩個參數:
- 對於靜態成員來講是類的構造函數,對於實例成員是類的原型對象。
- 成員的key。
descriptor不會作爲參數傳入屬性裝飾器,這與TypeScript是如何初始化屬性裝飾器的有關。 由於目前沒有辦法在定義一個原型對象的成員時描述一個實例屬性,而且沒辦法監視或修改一個屬性的初始化方法。返回值也會被忽略。所以,屬性描述符只能用來監視類中是否聲明瞭某個名字的屬性。
function setDefaultValue(target: Object, propertyName: string) { target[propertyName] = "Nealayng"; } class Person { @setDefaultValue name: string; } console.log(new Person().name); // 輸出: Nealayng
將上面的代碼修改一下,咱們給靜態成員添加一個 Decorator
function setDefaultValue(target: Object, propertyName: string) { console.log(target === Person); target[propertyName] = "Nealayng"; } class Person { @setDefaultValue static displayName = 'PersonClass' name: string; constructor(name:string){ this.name = name; } } console.log(Person.prototype); console.log(new Person('全棧前端精選').name); // 輸出: 全棧前端精選 console.log(Person.displayName); // 輸出: Nealayng
以此能夠驗證,上面咱們說的: Decorator 的第一個參數,對於靜態成員來講是類的構造函數,對於實例成員是類的原型對象
methods
方法裝飾器表達式會在運行時看成函數被調用,傳入下列3個參數:
- 對於靜態成員來講是類的構造函數,對於實例成員是類的原型對象。
- 成員的名字。
- 成員的屬性描述符
descriptor
。
注意: 若是代碼輸出目標版本小於ES5,descriptor將會是undefined。
function log( target: Object, propertyName: string, descriptor: TypedPropertyDescriptor<(...args: any[]) => any> ) { const method = descriptor.value; descriptor.value = function(...args: any[]) { // 將參數轉爲字符串 const params: string = args.map(a => JSON.stringify(a)).join(); const result = method!.apply(this, args); // 將結果轉爲字符串 const resultString: string = JSON.stringify(result); console.log(`Call:${propertyName}(${params}) => ${resultString}`); return result; }; } class Author { constructor(private firstName: string, private lastName: string) {} @log say(message: string): string { return `${message} by: ${this.lastName}${this.firstName}`; } } const author:Author = new Author('Yang','Neal'); author.say('《全站前端精選》');//Call:say("全站前端精選") => "全站前端精選 by: NealYang"
上述的代碼比較簡單,也就不作過多解釋了。其中須要注意的是屬性描述符 descriptor
的類型和許多文章寫的類型有些不一樣:propertyDescriptor: PropertyDescriptor
。
從官方的聲明文件能夠看出,descriptor 設置爲TypedPropertyDescriptor
加上泛型約束感受更加的嚴謹一些。
固然,官網也是直接聲明爲類型PropertyDescriptor
的。這個,仁者見仁。
accessors
訪問器,不過是類聲明中屬性的讀取訪問器和寫入訪問器。訪問器裝飾器表達式會在運行時看成函數被調用,傳入下列3個參數:
- 對於靜態成員來講是類的構造函數,對於實例成員是類的原型對象。
- 成員的名字。
- 成員的屬性描述符。
若是代碼輸出目標版本小於ES5,Property Descriptor將會是undefined。同時 TypeScript 不容許同時裝飾一個成員的get和set訪問器
function Enumerable( target: any, propertyKey: string, descriptor: PropertyDescriptor ) { //make the method enumerable descriptor.enumerable = true; } class Person { _name: string; constructor(name: string) { this._name = name; } @Enumerable get name() { return this._name; } } console.log("-- creating instance --"); let person = new Person("Diana"); console.log("-- looping --"); for (let key in person) { console.log(key + " = " + person[key]); }
若是上面 get
不添加Enumerable
的話,那麼 for in
只能出來 _name
_name = Diana
parameters
參數裝飾器表達式會在運行時看成函數被調用,傳入下列3個參數:
- 對於靜態成員來講是類的構造函數,對於實例成員是類的原型對象。
- 成員的名字。
- 參數在函數參數列表中的索引。
參數裝飾器只能用來監視一個方法的參數是否被傳入。
在下面的示例中,咱們將使用參數裝飾器@notNull
來註冊目標參數以進行非空驗證,可是因爲僅在加載期間調用此裝飾器(而不是在調用方法時),所以咱們還須要方法裝飾器@validate
,它將攔截方法調用並執行所需的驗證。
function notNull(target: any, propertyKey: string, parameterIndex: number) { console.log("param decorator notNull function invoked "); Validator.registerNotNull(target, propertyKey, parameterIndex); } function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) { console.log("method decorator validate function invoked "); let originalMethod = descriptor.value; //wrapping the original method descriptor.value = function (...args: any[]) {//wrapper function if (!Validator.performValidation(target, propertyKey, args)) { console.log("validation failed, method call aborted: " + propertyKey); return; } let result = originalMethod.apply(this, args); return result; } } class Validator { private static notNullValidatorMap: Map<any, Map<string, number[]>> = new Map(); //todo add more validator maps static registerNotNull(target: any, methodName: string, paramIndex: number): void { let paramMap: Map<string, number[]> = this.notNullValidatorMap.get(target); if (!paramMap) { paramMap = new Map(); this.notNullValidatorMap.set(target, paramMap); } let paramIndexes: number[] = paramMap.get(methodName); if (!paramIndexes) { paramIndexes = []; paramMap.set(methodName, paramIndexes); } paramIndexes.push(paramIndex); } static performValidation(target: any, methodName: string, paramValues: any[]): boolean { let notNullMethodMap: Map<string, number[]> = this.notNullValidatorMap.get(target); if (!notNullMethodMap) { return true; } let paramIndexes: number[] = notNullMethodMap.get(methodName); if (!paramIndexes) { return true; } let hasErrors: boolean = false; for (const [index, paramValue] of paramValues.entries()) { if (paramIndexes.indexOf(index) != -1) { if (!paramValue) { console.error("method param at index " + index + " cannot be null"); hasErrors = true; } } } return !hasErrors; } } class Task { @validate run(@notNull name: string): void { console.log("running task, name: " + name); } } console.log("-- creating instance --"); let task: Task = new Task(); console.log("-- calling Task#run(null) --"); task.run(null); console.log("----------------"); console.log("-- calling Task#run('test') --"); task.run("test");
對應的輸出位:
param decorator notNull function invoked method decorator validate function invoked -- creating instance -- -- calling Task#run(null) -- method param at index 0 cannot be null validation failed, method call aborted: run ---------------- -- calling Task#run('test') -- running task, name: test
@validate
裝飾器把run
方法包裹在一個函數裏在調用原先的函數前驗證函數參數.
裝飾器工廠
裝飾器工廠真的也就是一個噱頭(造名詞)而已,其實也是工廠的概念哈,畢竟官方也是這麼號稱的。在實際項目開發中,咱們使用的也仍是挺多的
**裝飾器工廠就是一個簡單的函數,它返回一個表達式,以供裝飾器在運行時調用。**其實說白了,就是一個函數 return
一個 Decorator
。很是像 JavaScript
函數柯里化,我的稱之爲「函數式Decorator」~
import { logClass } from './class-decorator'; import { logMethod } from './method-decorator'; import { logProperty } from './property-decorator'; import { logParameter } from './parameter-decorator'; // 裝飾器工廠,根據傳入的參數調用相應的裝飾器 export function log(...args) { switch (args.length) { case 3: // 多是方法裝飾器或參數裝飾器 // 若是第三個參數是數字,那麼它是索引,因此這是參數裝飾器 if typeof args[2] === "number") { return logParameter.apply(this, args); } return logMethod.apply(this, args); case 2: // 屬性裝飾器 return logProperty.apply(this, args); case 1: // 類裝飾器 return logClass.apply(this, args); default: // 參數數目不合法 throw new Error('Not a valid decorator'); } } @log class Employee { @log private name: string; constructor(name: string) { this.name = name; } @log greet(@log message: string): string { return `${this.name} says: ${message}`; } }
加載順序
一個類中,不一樣位置聲明的裝飾器,按照如下規定的順序應用:
- 有多個參數裝飾器(
parameterDecorator
)時,從最後一個參數依次向前執行 - 方法(
methodDecorator
)和方法參數裝飾器(parameterDecorator
)中,參數裝飾器先執行 - 類裝飾器(
classDecorator
)老是最後執行。 - 方法(
methodDecorator
)和屬性裝飾器(propertyDecorator
),誰在前面誰先執行。由於參數屬於方法一部分,因此參數會一直牢牢挨着方法執行。
function ClassDecorator() { return function (target) { console.log("I am class decorator"); } } function MethodDecorator() { return function (target, methodName: string, descriptor: PropertyDescriptor) { console.log("I am method decorator"); } } function Param1Decorator() { return function (target, methodName: string, paramIndex: number) { console.log("I am parameter1 decorator"); } } function Param2Decorator() { return function (target, methodName: string, paramIndex: number) { console.log("I am parameter2 decorator"); } } function PropertyDecorator() { return function (target, propertyName: string) { console.log("I am property decorator"); } } @ClassDecorator() class Hello { @PropertyDecorator() greeting: string; @MethodDecorator() greet( @Param1Decorator() p1: string, @Param2Decorator() p2: string) { } }
輸出爲:
I am parameter2 decorator I am parameter1 decorator I am method decorator I am property decorator I am class decorator
實戰
因爲是業務代碼,與技術無關瑣碎,只截取部分代碼示意,非 Decorator 代碼,以截圖形式
這應該也是整理這篇文章最開始的緣由了。直接說說項目(rax1.0
+Decorator
)吧。
需求很簡單,就是是編寫一個頁面的容器。
部分項目結構:
pm-detail ├─ constants │ └─ index.ts //常量 ├─ index.css ├─ index.tsx // 入口文件 └─ modules // 模塊 └─ page-container // 容器組件 ├─ base //容器基礎組件 ├─ decorator // 裝飾器 ├─ index.tsx ├─ lib // 工具 └─ style.ts
重點看下以下幾個文件
- base.tsx
實際上是基礎功能的封裝
在此基礎上,咱們須要個能滾動的容器
- scrollbase.tsx
也是基於 Base.tsx
基礎上,封裝一些滾動容器具備的功能
- style decorator
import is from './util/is'; import map from './util/map'; const isObject = is(Object); const isFunction = is(Function); class Style { static factory = (...args) => new Style(...args); analyze(styles, props, state) { return map(v => { if (isFunction(v)) { const r = v.call(this.component, props, state); return isObject(r) ? this.analyze(r, props, state) : r; } if (isObject(v)) return this.analyze(v, props, state); return v; })(styles); } generateStyles(props, state) { const { styles: customStyles } = props; const mergedStyles = this.analyze(this.defaultStyles, props, state); if (customStyles) { Object.keys(customStyles).forEach(key => { if (mergedStyles[key]) { if (isObject(mergedStyles[key])) { Object.assign(mergedStyles[key], customStyles[key]); } else { mergedStyles[key] = customStyles[key]; } } else { mergedStyles[key] = customStyles[key]; } }); } return { styles: mergedStyles, }; } constructor(defaultStyles = {}, { vary = true } = {}) { const manager = this; this.defaultStyles = defaultStyles; return BaseComponent => { const componentWillMount = BaseComponent.prototype.componentWillMount; const componentWillUpdate = BaseComponent.prototype.componentWillUpdate; BaseComponent.prototype.componentWillMount = function() { manager.component = this; Object.assign(this, manager.generateStyles(this.props, this.state)); return componentWillMount && componentWillMount.apply(this, arguments); }; if (vary) { BaseComponent.prototype.componentWillUpdate = function(nextProps, nextState) { Object.assign(this, manager.generateStyles(nextProps, nextState)); return componentWillUpdate && componentWillUpdate.apply(this, arguments); }; } return BaseComponent; }; } } export default Style.factory;
而後咱們須要一個錯誤的兜底功能,可是這個自己應該不屬於容器的功能。因此咱們封裝一個 errorDecorator
- withError.txs
function withError<T extends IConstructable>(Wrapped: T) { const willReceiveProps = Wrapped.prototype.componentWillReceiveProps; const didMount = Wrapped.prototype.componentDidMount; const willUnmount = Wrapped.prototype.componentWillUnmount; return class extends Wrapped { static displayName: string = `WithError${getDisplayName(Wrapped)}·`; static defaultProps: IProps = { isOffline: false, isError: false, errorRefresh: () => { window.location.reload(true); } }; private state: StateType; private eventNamespace: string = ""; constructor(...args: any[]) { super(...args); const { isOffline, isError, errorRefresh, tabPanelIndex } = this.props; this.state = { isOffline, isError, errorRefresh }; if (tabPanelIndex > -1) { this.eventNamespace = `.${tabPanelIndex}`; } } triggerErrorHandler = e => {...}; componentWillReceiveProps(...args) { if (willReceiveProps) { willReceiveProps.apply(this, args); } const [nextProps] = args; const { isOffline, isError, errorRefresh } = nextProps; this.setState({ isOffline, isError, errorRefresh }); } componentDidMount(...args) { if (didMount) { didMount.apply(this, args); } const { eventNamespace } = this; emitter.on( EVENTS.TRIGGER_ERROR + eventNamespace, this.triggerErrorHandler ); } componentWillUnmount(...args) { if (willUnmount) { willUnmount.apply(this, args); } const { eventNamespace } = this; emitter.off( EVENTS.TRIGGER_ERROR + eventNamespace, this.triggerErrorHandler ); } render() { const { isOffline, isError, errorRefresh } = this.state; if (isOffline || isError) { let errorType = "system"; if (isOffline) { errorType = "offline"; } return <Error errorType={errorType} refresh={errorRefresh} />; } return super.render(); } }; }
而後咱們進行整合導出
import { createElement, PureComponent, RaxNode } from 'rax'; import ScrollBase from "./base/scrollBase"; import withError from "./decorator/withError"; interface IScrollContainerProps { spmA:string; spmB:string; renderHeader?:()=>RaxNode; renderFooter?:()=>RaxNode; [key:string]:any; } @withError class ScrollContainer extends PureComponent<IScrollContainerProps,{}> { render() { return <ScrollBase {...this.props} />; } } export default ScrollContainer;
使用以下:
學習交流
最後附一張,本文思惟導圖。
公衆號回覆:【xmind1】 獲取思惟導圖源文件
- 關注公衆號【全棧前端精選】,每日獲取好文推薦
- 添加微信號:is_Nealyang(備註來源) ,入羣交流
公衆號【全棧前端精選】 | 我的微信【is_Nealyang】 |
---|---|
![]() |
![]() |
參考文獻
學習交流
關注公衆號: 【全棧前端精選】 每日獲取好文推薦。還能夠入羣,一塊兒學習交流呀~~