從 JavaScript 到 TypeScript 5 - 路由進化

weixin_33763244發表於2017-10-12

隨著應用的龐大,專案中 JavaScript 的程式碼也會越來越臃腫,這時候許多 JavaScript 的語言弊端就會愈發明顯,而 TypeScript 的出現,就是著力於解決 JavaScript 語言天生的弱勢:靜態型別。

前端開發 QQ 群:377786580

這篇文章首發於我的個人部落格 《聽說》,系列目錄:

在上一篇文章 《從 JavaScript 到 TypeScript 4 - 裝飾器和反射》 我們介紹了裝飾器和反射,在這篇文章中,我們會把這兩個特性引入,並且在 express 上,實現一層全新的路由封裝。

express 路由

首先我們來看一個簡單的 express 路由 (router):

// 對網站首頁的訪問返回 "Hello World!"
app.get('/', function (req: Request, res: Reponse) {
  res.send('Hello World!')
})


app.post('/user', function (req: Request, res: Reponse) {
  res.send(`User Id ${req.query.id}`)
})

在上面的路由程式碼我們演示了一個普通流水線式的路由。

基於上一篇文章中我們學到的裝飾器和反射的知識,我們將要實現 路由的配置通過裝飾器實現,並且實現一層路由邏輯的封裝。

路由進化

基於裝飾器和反射,我們要實現的路由最終效果是這樣的:

class Home {
  @path('/user')
  @httpGet
  user (id: string) {
    return `User Id ${id}`
  }
}
GET  HTTP/1.1
Host: /user?id=tasaid.com

這段程式碼相比傳統的路由配置,優點如下:

  • 將路由的配置抽離成為了裝飾器,讓整個 router 函式內部只需要處理業務邏輯即可,路由配置簡單明瞭
  • 隱藏 reqres,每個 router 直接返回結果即可,無需自己再輸出結果

裝飾器: HTTP Method

我們先編寫 HTTP Method 的裝飾器,我們將實現兩個裝飾器,分別叫做 httpGethttpPost,對應 HTTP Method 的 GET/POST

原理上,我們會將 router 配置的資料都掛到使用裝飾器的方法上。

import 'reflect-metadata'

export const symbolHttpMethodsKey = Symbol("router:httpMethod")

export const httpGet = function (target: any, propertyKey: string) {
  // 掛載到呼叫裝飾器的方法上
  Reflect.defineMetadata(symbolHttpMethodsKey, 'get', target, propertyKey)
}

export const httpPost = function (target: any, propertyKey: string) {
  Reflect.defineMetadata(symbolHttpMethodsKey, 'post', target, propertyKey)
}

裝飾器: path

有了上面 HTTP Method 裝飾器的實現,我們再實現 path 裝飾器將會很簡單。

當然,我們還可以在 path 中實現對原方法的封裝:隱藏 reqres,並對 router 的輸出結果進行封裝。

注意這裡使用的是裝飾器工廠:

import 'reflect-metadata'

export const symbolPathKey = Symbol.for('router:path')

export let path = (path: string): Function => {
  return function (target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<Function>) {

    Reflect.defineMetadata(symbolPathKey, path, target, propertyKey)

    if (!descriptor.value) return
    // 覆蓋掉原來的 router method,在外層做封裝
    let oldMethod = descriptor.value
    descriptor.value = function (req: Request, res: Response) {
      const params = Object.assign({}, req.body, req.query)
      let methodResult = oldMethod.call(this, params)
      // 輸出返回結果
      res.send(methodResult)
    }
  }
}

Router? Controller!

現在,我們需要將所有的 Router 按照自己的業務規則/或者自定義的其他規則進行歸類 —— 然後提取出對應的 Class,例如下面的 User Class 就是把使用者資訊所有的 router 都歸類在一起:

class User {
  @httpPost
  @path('/user/login')
  login() { }

  @httpGet
  @path('/user/exit')
  exit() { }
}

然後在 express 配置的入口邏輯那裡,把 class 對應的方法遍歷一遍,然後使用 reflect-metadata 反射對應的 router 配置即可:

import 'reflect-metadata'
// 裝飾器掛載資料的 key
import { symbolHttpMethodsKey, symbolPathKey } from './decorators'

const createController = (app: Express) => {
  let user = new User()
  for (let methodName in user) {
    let method = user[methodName]
    if (typeof method !== 'function') break
    // 反射得到掛載的資料
    let httpMethod = Reflect.getMetadata(symbolHttpMethodsKey, user, methodName)
    let path = Reflect.getMetadata(symbolPathKey, user, methodName)

    // app.get('/', () => any)
    app[httpMethod](path, method)
  }
}

至此,我們的 express 路由進化完畢,效果如下:

clipboard.png

完整的例子可以參考我的 Github

結語

裝飾器目前在 ECMAScript 新提案中的 建議徵集的第二階段(Stage 2),由於裝飾器在其他語言中早已實現,例如 Java 的註解(Annotation) 和 C# 的特性(Attribute),所以納入 ECMAScript 規範只是時間問題了。

裝飾器來裝飾路由,並且封裝 router 操作的的思路緣起 .NET MVC 架構:

clipboard.png

angular 2.x 使用也引入了裝飾器作為核心開發,隨著規範的推進,相信裝飾器進入大家視野,應用的場景也會越來越多。

在下一篇文章 《從 JavaScript 到 TypeScript 6 - Vue 引入 TypeScript》 中,我們將介紹如何在 Vue 中引入 TypeScript。

 

TypeScript 中文網:https://tslang.cn/

TypeScript 視訊教程:《TypeScript 精通指南

相關文章