Typescript Decorators 介紹與使用情境範例

      在〈Typescript Decorators 介紹與使用情境範例〉中尚無留言

有寫過 Java Spring Framework 應該對於 @Annotation 用法並不陌生, 其常常用來實現 Decorator Pattern, 只要在函數或屬性上標記就能把功能附上或注入值, 巧妙的讓程式碼變的更加簡潔, 之後有些套件也是使用 Annotation 方式處理 (例如:Lombok), 是一個非常好用的語法特性, 而這種語法在近幾年也開始有其他程式語言開始支援, 例如:Python, Dart…等, 這語法特性在 JS 也有提出 proposal, 而在 Typescript 上已經優先支援此語法並且使用在 Angular, Vue3, NestJS…等一些框架上, 本篇將介紹一些 Decorator 的實際情境實作範例。

語法特性文件

Javascript Decorator Proposal: https://github.com/tc39/proposal-decorators

英文:https://www.typescriptlang.org/docs/handbook/decorators.html

中文:https://typescript.bootcss.com/decorators.html

示範使用情境

使用 Decorator 注入 property 資料範例

往往我們都會在 .env 或是環境變數設定一些 key 或是設定, 下面的範例是使用 Decorator 注入的方式替換掉取 jwtKey 變數時的值

// 定義一個回傳 Property 的函數
function config (name: string) {
  // 相關設定可能來自 .env 或是系統環境變數, 這邊使用一個 MAP 示範
  const map = new Map()
  map.set('jwt-key', 't37XJOl8Ayxd2y3JphVLBOCWbYqxWcTk')

  return (target: any, key: string) => {
    Object.defineProperty(target, key, {
      configurable: false,
      get: function (this: { [name: string]: any}) {
        return map.get(name)
      },
      set: function (value) {
        // do nothing. 不可更改
      }
    })
  }
}

class Encrypter {
    @config('jwt-key')
    jwtKey = '';
}

const encrypter = new Encrypter();
console.log(encrypter.jwtKey); // 輸出 `t37XJOl8Ayxd2y3JphVLBOCWbYqxWcTk`

使用 Decorator 替換函數實作範例

下面的範例是抄至 Spring Data JPA 的使用情境, 使用 Decorator 中已定義的查詢功能進行查詢替換掉函數實際功能, 只需要在函數上定義 SQL 就可以查詢, 節省部分重複的程式碼

function query (sql: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.value = async function (...args: any[]): Promise<any[]> {
      for (const i in args) {
        sql = sql.replaceAll('$' + (Number(i) + 1), args[i])
      }

      // 執行查詢...
      const rows = [] as any[]
      let conn
      try {
        const conn = createDBConn()
        const [rows] = await conn.execute(sql)
      } finally {
        if (conn !== undefined) {
          conn.end()
        }
      }

      return rows
    }
  }
}

class StudentRepository {
  @query('SELECT * FROM Student WHERE Name = "$1"')
  findByName (name: string): any { /* do nothing. 由 Decorator 實作 */ }

  @query('SELECT * FROM Student WHERE ID = $1')
  findByID (id: number): any { /* do nothing. 由 Decorator 實作 */ }
}

使用 Decorator 實現 AOP 方式的 Cache method data

有些時候為了節省效能會使用到 Cache, 而往往讀取與寫入 Cache 的程式碼片段會遍佈在不同程式中, 這時可以針對某些函數成功的回傳值進行 Cache

function cacheData () {
  let cacheDatas = {} as { [key: string]: any; };

  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalFunc = descriptor.value;
    descriptor.value = function (...args: any[]): any {
        // 建立 cache 所用的 key, 依據自己情況改變 key 組成方式
        let key = originalFunc.toString() + '-' + JSON.stringify(args);
        if (cacheDatas.hasOwnProperty(key)) {
            return cacheDatas[key]
        }

        let returnVal = originalFunc(...args);
        if (returnVal instanceof Promise) {
          // 如果是 promise 成功才會 cache
          returnVal.then((value) => {
            cacheDatas[key] = Promise.resolve(value);
            return value;
          })
        } else {
          cacheDatas[key] = returnVal;
        }

        return returnVal;
    }
  }
}

class MyClass {
  @cacheData()
  async foo (name: string, id: number): Promise<number> {
    return (new Date()).getTime();
  }
}

async function main() {
  let obj = new MyClass();
  console.log('Bob', await obj.foo("Bob", 1));
  console.log('Alice', await obj.foo("Alice", 2));
  console.log('Jake', await obj.foo("Jake", 3));
  console.log('Bob', await obj.foo("Bob", 1)); // 這邊會是使用 cache 的結果
  console.log('Bob', await obj.foo("Bob", 3));
}
main();

指定函數忽略 Exception (Error) 並回傳預設值 (AOP 應用)

有時使用第三方或自己寫功能時, 會遇到當其失敗會丟出 Exception 情境, 而如果想要忽略掉此錯誤, 會需要寫一個 try catch 來包住該函數功能, 但是這樣在程式碼可能會變複雜, 且可能會有許多 try catch 產生, 此時可以用 Decorator 方式忽略掉該函數中可能丟出的錯誤, 以替代的值返回

function ignoreError (defaultVal: any) {
  let resolvePrmise = async (promise: Promise<any>, defaultValue: any) => {
    try {
      return await promise
    } catch (e) {
      // do nothing
    }
    return defaultValue
  }

  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalFunc = descriptor.value;
    descriptor.value = function (...args: any[]): any {
        try {
          let returnVal = originalFunc(...args);
          if (!(returnVal instanceof Promise)) {
            return returnVal
          }

          return resolvePrmise(returnVal, defaultVal)
        } catch (e) {
          // do nothing
        }

        return defaultVal;
    }
  }
}

class MyClass {
  @ignoreError('default value')
  async foo (name: string): Promise<number> {
    // do something
  }

  @ignoreError('default value2')
  async foo2 (name: string): Promise<number> {
    // do something
  }
}

async function main() {
  let obj = new MyClass();
  console.log('Bob', await obj.foo("Bob"));
  console.log('Alice', await obj.foo2("Alice"));
}

這幾個一些小小的情境應用, 還能作非常多的用途 (例如:對函數賦予 Lock 功能, 資料庫連線管理… 等), 有許多的框架用來作了不少好用的功能, 相信學習此語法特性會對於開發會有很多幫助

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。