1. 程式人生 > 其它 >TypeScript裝飾器Decorators學習

TypeScript裝飾器Decorators學習

目錄

TypeScript裝飾器Decorators學習

裝飾器與繼承的區別

裝飾器可以給程式碼提供功能。
現實生活當中張三想有一輛車,他可以通過繼承的方式讓父親給他一輛車。
裝飾器是現在比如說張三有一輛車,他想換個內飾,方向盤,或者輪骨這樣的。
繼承是在父子類之間進行的,父類有的功能子類可以拿來用,當然有的情況下父類設定為private子類就無法使用了,裝飾器可以對功能進行裝飾,裝飾類的方法、屬性甚至整個類等等。
裝飾器更加的靈活,繼承的話就是拿來就用。

配置TS裝飾器環境

需要我們有tsconfig.json的配置檔案。

終端命令:tsc --init

然後我們需要將裝飾器所需要的配置項開啟:

{
  "compilerOptions": {
  	"target": "es2016",
    "experimentalDecorators": true, 
  	"emitDecoratorMetadata": true,
    ......
  }
}

然後是兩個比較常用的終端命令:

tsc 1.ts -w // 監視某一個ts檔案
tsc -w // 根據配置檔案監視整個專案的檔案

當然也可以在選單欄使用終端-執行任務-typescript-監視來實現對整個專案檔案的監視。

類裝飾器decorator的基本使用

可以在原型鏈上無限增加內容,讓其擁有新的屬性啊,方法呀什麼的。像vue中的混入(mixin),裝飾器就是這樣的特性,開放封閉原則。

const moveDecorator: ClassDecorator = (target: Function) => {
    target.prototype.name = 'bleak'
    target.prototype.getPosition = () : {x: number, y: number} => {
        return {x: 100, y: 200}
    }
}

@moveDecorator
class Tank {
    // public getPosition() {}
}

const t = new Tank()
console.log((t as any).getPosition()) // { x: 100, y: 200 }
console.log((<any>t).getPosition()) // { x: 100, y: 200 }
console.log((t as any).name) // bleak

@moveDecorator
class Player {
    public getPosition() {}
}

const p = new Player()
console.log(p.getPosition()) // { x: 100, y: 200 }

裝飾器decorator語法糖

@符號的方式就是語法糖的表現形式, 我們在例項化物件new Function來建立物件, 為了與其他語言相似, ES6推出了class類的概念, 其實class內部還是通過建構函式的方式來進行操作, 歸根到底還是原型的概念.

const moveDecorator: ClassDecorator = (target: Function) => {
    target.prototype.name = 'bleak'
    target.prototype.getPosition = () : {x: number, y: number} => {
        return {x: 100, y: 200}
    }
}

// @moveDecorator
class Tank {}
moveDecorator(Tank)
const t = new Tank()
console.log((<any>t).getPosition()) // { x: 100, y: 200 }

我們會發現我們不使用裝飾器@語法的情況下, 直接使用該函式傳入類與使用裝飾器的效果相同, 只是裝飾器是自動的幫我們執行了一下, 不用我們再去寫一行程式碼去執行.

ts裝飾器疊加

在ts中裝飾器是可以疊加的,比如我們可以像如下程式碼一樣疊加多個類裝飾器:

const moveDecorator: ClassDecorator = (target: Function) => {
    target.prototype.name = 'bleak'
    target.prototype.getPosition = () : {x: number, y: number} => {
        return {x: 100, y: 200}
    }
}

const MusicDecorator: ClassDecorator = (target: Function) => {
    target.prototype.playMusic = (): void => {
        console.log('播放音樂')
    }
}


@moveDecorator
@MusicDecorator
class Tank {}

const t = new Tank()
console.log((t as any).getPosition()); // { x: 100, y: 200 }
(<any>t).playMusic() // 播放音樂

我們可以通過使用多個裝飾器來給類新增多個不同的功能,比如上面新增的一個是獲取位置和名字的功能,一個是播放音樂的功能。

通過TS裝飾器實現統一訊息迴應

我們可以通過裝飾器給多個類新增統一的功能,當然我們也可以通過繼承來實現。

const MessageDecorator:ClassDecorator = (target:Function) => {
    target.prototype.message = (content: string) => {
        console.log(content)
    }
}

@MessageDecorator
class LoginController {
    public login() {
        console.log("登入業務處理")
        ;(this as any).message("恭喜你,登入成功了")
    }
}

new LoginController().login() // 登入業務處理 恭喜你,登入成功了

@MessageDecorator
class ArticleController {
    public store() {
        (this as any).message("文章新增成功")
    }
}
new ArticleController().store() // 文章新增成功

裝飾器工廠在TS中的使用

我們可以根據需要使用裝飾器工廠返回不同的裝飾器, 根據傳入引數的不同,我們可以返回不同的裝飾器,雖然我們下面只是根據一個引數,但是我們也用多個引數來區分要返回的裝飾器。

const MusicDecoratorFactory = (type: string): ClassDecorator => {
    switch(type) {
        case 'Tank':
            return (target:Function) => {
                target.prototype.playMusic = (): void => {
                    console.log('播放戰爭音樂')
                }
            }
        default:
            return (target:Function) => {
                target.prototype.playMusic = (): void => {
                    console.log('播放電音')
                }
            }
    }
    
}

@MusicDecoratorFactory('Tank')
class Tank {}

const t = new Tank()
;(<any>t).playMusic() // 播放戰爭音樂


@MusicDecoratorFactory('Player')
class Player {}

(new Player() as any).playMusic() // 播放電音

當然,方法裝飾器呀,屬性裝飾器呀等裝飾器同樣可以使用裝飾器工廠。

方法裝飾器

const showDecorator:MethodDecorator = (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor): PropertyDescriptor | void => {
    console.log(target)
    console.log(propertyKey)
    console.log(descriptor)
    target.name = 'bleak'
}

class User {
    @showDecorator
    public show() {
        console.log("It's my show time")
    }
}

console.log((new User() as any).name)
/* 結果
{}
show
{
  value: [Function: show],
  writable: true,
  enumerable: false,
  configurable: true
}
bleak
*/
  • 方法裝飾器的第一個引數target,如果我們是給靜態方法新增的方法裝飾器,那麼target就是建構函式,如果是普通方法,那麼target就是原型物件。
  • 方法裝飾器的第二個引數propertyKey,是我們方法的名稱
  • 方法裝飾器的第三個引數descriptor,是對方法屬性的描述,包括其函式體的具體內容value,其可寫性writable,可列舉性(迭代性)enumerable和可配置性configurable.
    我們可以像如下方式一樣修改方法體:
const showDecorator:MethodDecorator = (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor): PropertyDescriptor | void => {
    console.log(target)
    console.log(propertyKey)
    console.log(descriptor);
    descriptor.value = () => {
        console.log("Now it's bleak's show time")
    }
}

class User {
    @showDecorator
    public show() {
        console.log("It's my show time")
    }
}

new User().show() // Now it's bleak's show time

靜態方法裝飾器與writable

const showDecorator:MethodDecorator = (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor): PropertyDescriptor | void => {
    descriptor.value = () => {
        console.log("Now it's bleak's show time")
    }
}

class User {
    @showDecorator
    public static show() {
        console.log("It's my show time")
    }
}

User.show() // Now it's bleak's show time

無論是靜態方法還是普通方法,呼叫裝飾器的時候第三個引數都是對方法屬性的描述。
如果我們把writable設定為false,那麼我們就無法再對方法進行重寫。

const showDecorator:MethodDecorator = (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor): PropertyDescriptor | void => {

    descriptor.writable = false
}

class User {
    @showDecorator
    public static show() {
        console.log("It's my show time")
    }
}

User.show() // Now it's bleak's show time
// × Error
User.show = () => {
    console.log('show method changed.')
}

使用裝飾器實現文字高亮

const highlightDecorator:MethodDecorator = (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor): PropertyDescriptor | void => {
    const method = descriptor.value 
    descriptor.value = () => {
        return `<div style='color:red;'>${method()}</div>`
    }
}

class User {
    @highlightDecorator
    public static show() {
        return "It's my show time"
    }
}


console.log(User.show()) // <div style='color:red;'>It's my show time</div>
  1. 首先把原來的方法儲存
  2. 重新寫一個新的方法
  3. 在新的方法中使用原來的方法,即可實現文字的高亮效果。

延遲執行在裝飾器中的實現

const SleepDecorator:MethodDecorator = (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor): PropertyDescriptor | void => {
    const method = descriptor.value
    descriptor.value = () => {
        setTimeout(() => {
            method()
        }, 2000)
    }
}

class User {
    @SleepDecorator
    public show() {
        console.log("It's my show time")
    }
}
new User().show()

使用裝飾器工廠控制延遲時間

const SleepDecoratorFactory = 
    (times: number):MethodDecorator => 
    (...args: any[]) => {
        const [ , ,descriptor] = args
        const method = descriptor.value
        descriptor.value = () => {
            setTimeout(() => {
                method()
            }, times)
        }
    }

class User {
    @SleepDecoratorFactory(500)
    public show() {
        console.log("It's my show time")
    }
}
new User().show()

裝飾器全域性異常管理

const ErrorDecorator:MethodDecorator = (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
    const method = descriptor.value
    descriptor.value = () => {
        try {
            method()
        } catch(error: any) {
            console.log(`%cbleak發現錯誤了`, 'color:green;font-size:30px;')
            console.log(`%c${error.message}`, 'color:red; font-size:16px')
        }
    }

}

class User {
    @ErrorDecorator
    find() {
        throw new Error("您查詢的使用者不存在")
    }

    @ErrorDecorator
    create() {
        throw new Error("建立使用者失敗")
    }
}

new User().create()

裝飾器工廠自定義 console.log

我們可以通過裝飾器工廠來實現列印錯誤時自定義訊息內容:

const ErrorDecoratorFactory = (title: string='bleak發現錯誤了', titleFontSize: number = 20):MethodDecorator => {
    return (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
        const method = descriptor.value
        descriptor.value = () => {
            try {
                method()
            } catch(error: any) {
                console.log(`%c${title}`, `color:green;font-size:${titleFontSize}px;`)
                console.log(`%c${error.message}`, 'color:red; font-size:16px;')
            }
        }
    
    }
}

class User {
    @ErrorDecoratorFactory()
    find() {
        throw new Error("您查詢的使用者不存在")
    }

    @ErrorDecoratorFactory('Bleak Find Error https://www.cnblogs.com/bleaka/', 12)
    create() {
        throw new Error("建立使用者失敗")
    }
}

new User().create()

使用者登入驗證在TS裝飾器中的實現

const user = {
    name: 'Bleak',
    isLogin: false
}

const AccessDecorator: MethodDecorator =  (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
    const method = descriptor.value
    descriptor.value = () => {
        if(user.isLogin === true) {
            return method()
        } else {
            alert('請登陸後操作')
            location.href = 'login.html'
        }

    }
}

class Article {
    show() {
        console.log('顯示文章')
    }
    @AccessDecorator
    store() {
        console.log('儲存文章')
    }
}

new Article().store() // 跳轉到登入頁面

資料許可權控制訪問方法

type userType = {
    name: string,
    isLogin: boolean, 
    permissions: string[]
}
const user:userType = {
    name: 'Bleak',
    isLogin: true,
    permissions: ["store"]
}

const AccessDecoratorFactory = (keys: string[]): MethodDecorator => {
    return (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
        const method = descriptor.value

        // 定義一個方法來檢測有效性
        const validate = () => 
            keys.every (k => {
                return user.permissions.includes(k)
            })

        descriptor.value = () => {
            if(user.isLogin === true && validate()) {
                alert('驗證通過')
                method()
            } else {
                alert('驗證失敗')
            }
    
        }
    }
}  

class Article {
    show() {
        console.log('顯示文章')
    }
    @AccessDecoratorFactory(['store'])
    store() {
        console.log('儲存文章')
    }
}

new Article().store()

使用裝飾器模擬超快速的網路請求

const RequestDecorator = (url: string): MethodDecorator => {
    return (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
        const method = descriptor.value
        // axios.get(url).then()
        new Promise<any[]>(resolve => {
            setTimeout(() => {
                resolve([{name:'Bleak'}, {name: 'Chris'}])
            }, 2000)
        }).then(users => {
            method(users)
        })
    }
}


class Uesr {
    @RequestDecorator('https://www.baidu.com')
    public all(users: any[]) {
        console.log(users)
    }
}

我們可以通過方法裝飾器工廠來實現非同步網路請求,這樣的好處是我們以後想要請求得時候就會變得非常簡單,我們使用裝飾器就自動請求了,自動注入到我們的引數裡面,我們直接用就可以了。

屬性修飾器和引數修飾器

const PropDecorator: PropertyDecorator = (target: Object, propertyKey: string | symbol): void => {
    console.log(target)
    console.log(propertyKey)
}

class Hd {
    @PropDecorator
    public name: string | undefined
}

  • 屬性裝飾器的第一個引數target,如果我們是給靜態屬性新增的屬性裝飾器,那麼target就是建構函式,如果是普通屬性,那麼target就是原型物件。
  • 屬性裝飾器的第二個引數propertyKey是屬性名稱。
const PropDecorator: PropertyDecorator = (target: Object, propertyKey: string | symbol): void => {
    // console.log(target)
    // console.log(propertyKey)
}

const ParamsDecorator: ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number): void => {
    console.log(target)
    console.log(propertyKey)
    console.log(parameterIndex)
}


class Hd {
    @PropDecorator
    public title: string | undefined

    public show(id: number = 1, compouted: boolean ,@ParamsDecorator content:string) {

    }
}
  • 引數裝飾器的第一個引數target就是原型物件。
  • 引數裝飾器的第二個引數propertyKey是引數名稱。
  • 引數裝飾器的第三個引數parameterIndex是引數所在的位置。

屬性訪問器動態轉換物件屬性

// Object.defineProperty() 方法會直接在一個物件上定義一個新屬性,或者修改一個物件的現有屬性,並返回此物件。
// Object.defineProperty(obj, prop, descriptor)
// obj 要定義屬性的物件。
// prop 要定義或修改的屬性的名稱或 Symbol。
// descriptor 要定義或修改的屬性描述符。
const LowerDecorator: PropertyDecorator = (target: Object, propertyKey: string | symbol): void => {
    let value: string
    Object.defineProperty(target, propertyKey, {
        get: () => {
            return value.toLowerCase()
        },
        set: v => {
            value = v
        }
    })
}


class Hd {
    @LowerDecorator
    public title : string | undefined

}

const obj = new Hd()
obj.title = 'Bleak'
console.log(obj.title) // bleak

使用ts的屬性裝飾器建立隨機色塊

const RandomColorDecorator: PropertyDecorator = (target: Object, propertyKey: string | symbol): void => {
    const color:string[] = '0123456789abcdef'.split('')
    Object.defineProperty(target, propertyKey, {
        get: ()=> {
            let res = '#'
            for(let i = 0; i < 6; i++) {
                res += color[Math.floor(Math.random() * color.length)]
            }
            return res
        }
    })
}

class Hd {
    @RandomColorDecorator
    public color: string | undefined

    public draw() {
        document.body.insertAdjacentHTML('beforeend'
            ,`<div style="height:200px;width:200px;background-color:${this.color};">Bleak</div>`
        )
    }

}

console.log(new Hd().draw())

元資料reflect-metadata的使用

元資料(metadata):即資料的資料,可以在資料中儲存資訊,我們可以根據元資料來在類上、類原型的屬性新增元資料。

  1. 首先我們需要安裝這個庫:pm install reflect-metadata --save
  2. 然後我們使用這個庫中的Reflect.getMetadataReflect.defineMetadata
import 'reflect-metadata'


// metadata元資料:資料的資料
let dreamCode = {
    name: 'bleak'
}

Reflect.defineMetadata('bleak',{url:'https://www.cnblogs.com/bleaka/'}, dreamCode, 'name')

console.log(Reflect.getMetadata('bleak', dreamCode, 'name')) // { url: 'https://www.cnblogs.com/bleaka/' }

關於兩個重要的apiReflect.getMetadataReflect.defineMetadata的引數解釋:

Reflect.getMetadata

其可以接收兩個引數或者是三個引數:

  • 當接收兩個引數的時候,是去找'類'所對映的對應關係使用,第一個引數是建立對映時候的'key',第二個 引數是這個'類'。
  • 當接收三個引數的時候,是去找'類中屬性'對應的對映關係時候,三個引數,第一個引數是建立對映時候的'key' ,第二個引數是這個'例項',第三個是例項中所對應的具體'屬性',其實可以很容易理解這裡 為什麼用的是例項,因為有了例項才有了屬性。

Reflect.defineMetadata

其可以給'類'和'屬性'增加自定義對映關係。

  • 當只有這三個引數'metadataKey,metadataValue, target'修飾類時,第一個引數代表對映的'key',第二個引數代表對映'key'對應的'value',第三個引數代表的需要對映對應的類。
  • 當有四個引數''metadataKey,metadataValue, target, propertyKey'修飾屬性時,第一個引數代表對映的'key',第二個引數代表對映'key'對應的'value',第三個引數代表的需要對映對應的類或例項,第四個引數代表例項上的屬性。

使用Reflect-metadata的defineMetadata和getMetadata配置驗證資料

import 'reflect-metadata'


const RequiredDecorator:ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number): void => {
    let requiredParams: number[] = Reflect.getMetadata('validate',target,propertyKey) || []
    requiredParams.push(parameterIndex)
    Reflect.defineMetadata('validate',requiredParams, target, propertyKey)
}

const validateDecorator:MethodDecorator = (target: Object, propertyKey: string | symbol, descriptor:PropertyDescriptor): PropertyDescriptor | void => {
    const method = descriptor.value
    descriptor.value = function() {
        let requiredParams:number[] = Reflect.getMetadata('validate',target,propertyKey) || []
        requiredParams.forEach(index => {
            if(index > arguments.length || arguments[index] === undefined) {
                throw new Error('請傳遞必要的引數')
            } 
        })
        return method.apply(this, arguments)
    }
    
}

class User {
    @validateDecorator
    find(@RequiredDecorator name:string, @RequiredDecorator id: number) {
        console.log(id)
    }
}

 new User().find('sds', 2)

以上,如果傳遞引數少於2個的話就會報錯,提示請傳遞必要的引數。