TS從裝飾器到注解到元編程

先看一段代碼

import {Controller, Path, GET, POST, PathParam, BodyParam} from 'iwinter'

@Path('/api/orders')
class OrdersController extends Controller {

    @GET
    @Path('/:name/:id', (ctx, next)=> ~~ctx.params.id > 20)
    getAllOrders(@PathParam('id') id: number, @PathParam('name') name: string){
        return [{
            id: id, name, content: 'test', author: 'test', comments: []
        }];
    }

    @POST
    @Path('/add')
    addPost(@BodyParam('order') order: object){
        return order
    }
}

export default OrdersController

這是一段TypeScript上 koa 路由類的寫(xiě)法,注意到在其中,使用了@Paht @Get的寫(xiě)法, 并且在入?yún)⒅幸灿?code>@PathParam('id') id: number這樣的寫(xiě)法。這就是裝飾器。其中 @Path('/api')中的API是這個(gè)裝飾器的入?yún)ⅲ谶@里是注解,因?yàn)檫@個(gè)框架通過(guò)Reflect.defineMetadata將這個(gè)入?yún)?xiě)入到了該方法中。在搞清這些復(fù)雜的概念之前,我們先弄明白兩個(gè)最基礎(chǔ)的概念

裝飾器和注解

  • 裝飾器(Decorator) 僅提供定義劫持,能夠?qū)︻惣捌浞椒ā⒎椒ㄈ雲(yún)ⅰ傩缘亩x并沒(méi)有提供任何附加元數(shù)據(jù)的功能。
  • 注解(Annotation) 僅提供附加元數(shù)據(jù)支持,并不能實(shí)現(xiàn)任何操作。需要另外的 Scanner 根據(jù)元數(shù)據(jù)執(zhí)行相應(yīng)操作。

注意到裝飾器是對(duì)類及其方法、入?yún)ⅰ傩孕袨榈男薷模⒔庵皇翘砑釉獢?shù)據(jù),不能修改行為。
在實(shí)際的開(kāi)發(fā)過(guò)程中,我們通過(guò)注解添加元數(shù)據(jù),裝飾器再獲取這些元數(shù)據(jù)完成對(duì)類或者方法的修改,下面開(kāi)始對(duì)修改一個(gè)類

實(shí)際操作

class A {

}

首先我們聲明了一個(gè)什么也沒(méi)有類,接著我們聲明第一個(gè)修改器方法,并且作用在類上

@modifyClass
class A {

}

function modifyClass(target: any) {
  target.prototype.extraProp = 'decorator'
}

在裝飾器的方法中,入?yún)⑹莟arget,作用于class A上就是 A ,我們知道,在ES中一切都是對(duì)象,class 是ES6以后的一個(gè)面向?qū)ο蟮恼Z(yǔ)法糖,在這里的A本質(zhì)也就是一個(gè)function,在新建實(shí)例的時(shí)候作為構(gòu)造函數(shù)調(diào)用。這里通過(guò)target.prototype我們也能獲得這個(gè)類的原型。這樣我們就可以對(duì)這個(gè)類進(jìn)行修改了。值得注意的是,

裝飾器是在編譯期間發(fā)生的,這個(gè)時(shí)候類的實(shí)例還沒(méi)有生成,因此裝飾器無(wú)法直接對(duì)類的實(shí)例進(jìn)行修改。但是可以間接的通過(guò)修改類的原型影響實(shí)例

這樣的修飾器意義不大,我們要應(yīng)對(duì)更多的情況,因此可以給修飾器加上參數(shù),或者叫做'注解'

@modifyClass('param')
class A {

}

function modifyClass(param) {
  return target => {
      target.prototype.extraProp = param
  }
}

之所以要給注解添加引號(hào),是因?yàn)樽⒔獾母拍钍且M(jìn)行元數(shù)據(jù)的修改,而這里僅僅是動(dòng)態(tài)改變?cè)蜕系膶傩浴RM(jìn)行元數(shù)據(jù)的修改,我們需要利用反射Reflect。 ES6提供的Refelct并不滿足修改元數(shù)據(jù),我們要額外引入一個(gè)庫(kù)reflect-metadata

import 'reflect-metadata'

@modifyClass('param')
class A {

}

function modifyClass(param) {
  return target => {
      Reflect.defineMetadata(Symbol.for('META_PARAM'), param, target.prototype)
  }
}

這個(gè)時(shí)候就是真正的注解了,我們通過(guò)裝飾器和Reflect對(duì)要修飾的類注入了元數(shù)據(jù),注意我們這里是注入到target.prototype,類的實(shí)例上。因?yàn)椴煌膶?shí)例是獲得的不同的數(shù)據(jù),因此不能注入到target上。
Reflect.defineMetadata方法第一個(gè)入?yún)⒖梢允莝tring 類型或者 symbol 類型。建議使用symbol類型,這樣避免被覆蓋掉。而這里的param可以是任意類型。我們不僅能在類上定義元數(shù)據(jù),也可以在類的屬性上定義
Relect.defineMetadata(metadataKey: any, metadataValue: any, target: any, propertyKey)

接下來(lái)我們就可以在任意的地方通過(guò)Reflect.getMetadata(metadataKey, target)獲取元數(shù)據(jù)了。

反射給了我們?cè)陬惣捌鋵傩浴⒎椒ā⑷雲(yún)⑸洗鎯?chǔ)讀取數(shù)據(jù)的能力

類及其實(shí)例并不能感知或者修改存取在類上元數(shù)據(jù),但是我們可以通過(guò)裝飾器和注解在編譯時(shí)動(dòng)態(tài)的修改它們的行為,即我們寫(xiě)了一個(gè)函數(shù)去修改函數(shù),我們把這樣的行為稱作元編程,接下來(lái)我們看看裝飾器作用在屬性、方法、入?yún)⑸?/p>

@modifyClass('new Prop')
class A {

  @modifyProp type: string
  name: string

  constructor (name) {
    this.name = name
  }
  
  @modifyMethod
  say (@modifyParam word) {
    let str = Reflect.getMetadata(key, this)
    console.log(str)
  }
}


// 在裝飾類的裝飾器上獲得target(類)是類本身
// 在裝飾屬性、方法、入?yún)⑸汐@得的target的是類的原型target(屬性、方法、入?yún)? === target(類).prototype
function modifyClass (name) {
  return (target) => {
    target.prototype.extra = name
  }
}
 
function modifyProp (target, propertyKey) {
  // 修改屬性
  console.log(target)
  console.log(propertyKey)
  target[propertyKey] = 'modfiyed by decorator'
}

// 我們?cè)?ts 版本的 vuex 裝飾器中看到的 @state('key') key 等價(jià)于
// function state (key) {
//   return (target, propertyKey) => {
//     target[propertyKey] = target.$store.state[key]
//   }
// }

// 修飾方法
// descriptor對(duì)象原來(lái)的值如下
// {
//   value: specifiedFunction,
//   enumerable: false,
//   configurable: true,
//   writable: true
// };
function modifyMethod (target, propertyKey, descriptor) {
  Reflect.defineMetadata(key, 'Hello Reflect',target)
  const fun = descriptor.value
  descriptor.value = function () {
    console.log(this) // 運(yùn)行時(shí)確定因此這里是的 this 指向?qū)嵗摹H绻@里是箭頭函數(shù),this則指向undefined
    return fun.apply(this, arguments)
  }
}

// 修飾入?yún)?// index 是這個(gè)參數(shù)的順序
function modifyParam (target, propertyKey, index) {
  console.log(target)
  console.log(propertyKey)
  console.log(index)
}

通過(guò)裝飾器實(shí)現(xiàn)一個(gè)切面

class A {
    say()
}

function AOP(type, func) {
  return (target, propetyKey, descriptor) => {
      let oldMethod = descriptor.value
      if (type == 'BEFORE') {
          descripotor.value = function () {
              fun(...arguments)
              return oldMethod.apply(this, arguments)
          }
      } else if (type == 'AFTER') {
          descripotor.value = function () {
              let result = oldMethod.apply(this, arguments)
              fun(...arguments)
              return result
          }
      }
  }
}

注意,這里的切點(diǎn)只能是同步函數(shù),如果要異步的函數(shù)需要進(jìn)行額外的處理,是否為異步函數(shù)同樣可以通過(guò)注解表明。這里也只能用函數(shù)表達(dá)式,不能使用箭頭函數(shù),否則會(huì)造成this的丟失。

總結(jié)

  1. 裝飾器提供了對(duì)類的屬性、方法、入?yún)⑿薷牡哪芰Γ菃为?dú)靠裝飾器是不夠的,還要通過(guò)注解配合,這樣才能動(dòng)態(tài)的修改原來(lái)的表現(xiàn)行為。因此我們可以封裝一些常用的裝飾器方法,達(dá)到復(fù)用的能力。但要切記,裝飾器的行為是發(fā)生在編譯時(shí)
  2. 這里的裝飾器修飾是在TS上完成的,在不涉及Reflec時(shí)TS和ES的目前表現(xiàn)一致。那么在涉及Reflect時(shí)的表現(xiàn)是什么樣的呢?我也不知道啊o_O。并且TS和ES的裝飾器是有不同的,未來(lái)的版本可能也會(huì)發(fā)生根本的改變。

以上都是我瞎編的

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 本文章涉及代碼已放到github上annotation-study 1.Annotation為何而來(lái) What:A...
    zlcook閱讀 29,255評(píng)論 15 116
  • 前言,本來(lái)只是想研究一下注解的,不過(guò)發(fā)現(xiàn),要懂注解先得懂反射,別問(wèn)我為什么,你可以自己試試 JAVA反射 主要是指...
    justCode_閱讀 1,232評(píng)論 2 9
  • 前言 人生苦多,快來(lái) Kotlin ,快速學(xué)習(xí)Kotlin! 什么是Kotlin? Kotlin 是種靜態(tài)類型編程...
    任半生囂狂閱讀 26,267評(píng)論 9 118
  • 今天: 1.前橋 1.1設(shè)備調(diào)試工作計(jì)劃 1.2現(xiàn)場(chǎng)工作梳理 1.3代碼:底層,返修代碼;底層,下線代碼;OPC通...
    先胖不算胖閱讀 66評(píng)論 0 0
  • 度量品優(yōu)比月明,無(wú)疆父愛(ài)堪星辰。 精言暖語(yǔ)入我心,高德致行化我思。 萬(wàn)千心念伴我長(zhǎng),珍時(shí)惜間陪您老。 得此父如獲至...
    萬(wàn)愛(ài)閱讀 195評(píng)論 0 1