徐帥武,微醫雲服務團隊前端工程師。一個愛折騰、愛作菜的前端程序猿前端
不少小夥伴使用 Koa
或 Egg
之類框架寫接口時必定碰到過下面這種使人頭大的寫法,每次咱們定義一個路由寫完 Controller 方法還要去 router 文件中再次定義一遍,很是的繁瑣麻煩。webpack
這種寫法當然很是誘人,可是爲了一個寫法去切換框架的代價是很是大的,那麼咱們在 Koa
或 Egg
中可使用這種方法嗎?或者說能夠保持原有寫法的同時漸進加強的使用裝飾器來定義路由嗎?答案是確定的,裝飾器路由寫法的實現其實很是簡單,各位看官且往下看。git
裝飾器實際上是一種語法糖,定義爲一個普通的函數,調用時寫成 @ + 函數名
。它能夠放在類和類方法的定義前面。類與類方法的參數有所不一樣。github
類的裝飾器能夠用來裝飾整個類:web
@testable
class MyTestableClass { // ... } function testable(target) { target.isTestable = true; } MyTestableClass.isTestable // true // 此處代碼來自阮一峯 ES6 入門教程 複製代碼
傳入的 target 參數就是類自己,若是裝飾器函數有返回值就用返回值替換這個類。基本行爲就是下面這樣:正則表達式
@decorator
class A {} // 等同於 class A {} A = decorator(A) || A; 複製代碼
類方法裝飾器其實和 Object.defineProperty
方法很是像,三個參數分別是:npm
target
: 類對象其實就是 "類" 的 prototype 對象,它上面有個
constructor
屬性指向類自己
name
:裝飾屬性的名稱
descriptor
:屬性的描述符,這個和
Object.defineProperty
方法是一致的
屬性描述符的具體屬性和描述能夠看這裏 descriptor。前端工程師
function foo(target, name, descriptor){
} 複製代碼
裝飾器須要傳參時咱們能夠建立一個高階函數,這個函數返回一個裝飾器函數就能夠實現傳參的目的了。app
function foo (url) {
return function (target, name, descriptor) { console.log(url) } } class Bar { @foo('/test') baz () { } } 複製代碼
Reflect(反射)是 ES6 爲了操做對象而提供的新 API,這裏咱們要用到 MetaData 相關的 API 來爲對象綁定一些路由數據,用來生成最終的路由文件。這個 API 目前尚未進入正式版本須要引入 reflect-metadata
這個庫來支持相關 API,詳情參見這裏 reflect-metadata,咱們主要使用到下面兩個 API:框架
// 設置元數據
Reflect.defineMetadata(metadataKey, metadataValue, target); // 獲取設置的值 let result = Reflect.getMetadata(metadataKey, target); 複製代碼
最終目標就是要生成 Koa
或 Egg
所須要的 router 配置,這裏咱們拿 Koa 舉例,須要的就是相似於下面這樣一份配置,因此咱們的目標就是拿到建立下面這樣一份文件,因此思路就是在 裝飾器函數中能夠拿到類方法 ,經過 reflect-metadata 能夠在每一個方法上寫入 路徑、請求類型 等元數據,因此只須要統一對外提供一個註冊的方法就能夠把使用裝飾器設置的路徑和函數註冊在 router 對象上,這樣就完成了路由自動註冊的過程。
const app = new Koa();
const router = new Router(); router.get('/user/info', UserInfoController); router.get('/user/list', UserListController); router.post('/user/create', UserCreateController); app.use(router.routes()) 複製代碼
通常來講某個 Class 對應的接口都會有一個統一的前綴,因此咱們定義一個 Controller 方法用來存儲公共路徑。
/** * Controller 裝飾器 * 用來裝飾 Controller 類 * * @param {string} [baseUrl=''] 類的公共前綴 * @returns * @memberof Decorator */ Controller (baseUrl = '') { return (target) => { Reflect.defineMetadata(BASE_URL, baseUrl, target) } } 複製代碼
由於 koa-router 中註冊 Get、Post 之類的方法參數都相同,因此裝飾器能夠註冊一個通用的方法來生成各個方法的裝飾器,代碼以下:
/** * 用來生成各類方法裝飾器的工具函數 * * @param {*} method * @memberof Decorator */ createMethodDecorator (method) { return (url) => { return (target, name, decorator) => { // target 爲裝飾方法所在的類 // 由於類方法的裝飾器會比類的裝飾器先執行, 在這個階段拿不到 Controller 類的公共前綴 // 因此要存下 target 後面再根據所存的信息生成 router this.controllerList.add(target) // decorator.value 爲裝飾的函數自己 Reflect.defineMetadata(METHOD, method, decorator.value) // 沒有指定請求的 url 就是用函數名做爲 url Reflect.defineMetadata(METHOD_URL, url || name, decorator.value) } } } 複製代碼
有了路由信息後就須要將全部的路由信息註冊到 router 對象上來完成路由的註冊,咱們遍歷存儲起來的全部 controller 類,而後獲取到方法上面對應的路由信息來進行註冊:
/** * 註冊路由 * * @param {*} router Koa Router 對象 * @memberof Decorator */ registerRouter (router) { for (const controller of this.controllerList) { // 獲取類構造函數,就是類裝飾器中的 target 參數 const controllerCtor = controller.constructor const baseUrl = Reflect.getMetadata(BASE_URL, controllerCtor) || '' // 獲取類對象上的全部屬性 const allProps = Object.getOwnPropertyNames(controller) for (const props of allProps) { const handle = controller[props] // 遍歷全部屬性中是函數 且存在路由信息的函數 if (typeof handle !== 'function') { continue } const method = Reflect.getMetadata(METHOD, handle) const url = Reflect.getMetadata(METHOD_URL, handle) if (method && url && router[method]) { // 由於是 demo 暫時不校驗和轉換各個 url 的格式 // 實際使用中須要將三個路徑拼接爲合法的 url 格式 const completeUrl = this.prefix + baseUrl + url // 把接口路徑和函數註冊到 router 對象上 router[method](completeUrl, handle) } } } } 複製代碼
最後咱們還要將全部的 Controller 文件加載進來,爲了不手寫,咱們建立一個 load 函數來自動加載全部的 Controller 文件。
這裏咱們用到了 requireContext
函數,使用 webpack 打包的話這個方法爲 require.context()
是默承認用的。若是沒有使用 webpack 的話就須要手動引入 require-context
這個 npm 包來使用。
requireContext
這個方法有三個參數分別爲:搜索的目錄、是否搜索子文件夾、匹配文件的正則表達式。使用這個方法能夠獲取全部符合條件的模塊。
import requireContext from 'require-context'
export const load = function (path) { const ctx = requireContext(path, true, /\.js$/) ctx.keys().forEach(key => ctx(key)) } // 使用:傳入 controller 函數所在文件夾 load(path.resolve(__dirname, './controller')) 複製代碼
裝飾器的功能很是強大,除了上面的自動註冊路由外還能夠作更多的事情,好比路由的鑑權、中間件、依賴注入、參數校驗、日誌等等。
綜上咱們實現了一個基於 Koa Router 裝飾器路由,Express,Egg 之類的其餘框架的實現也是一個道理,不一樣框架根據路由註冊方法的區別對 registerRouter
函數略加改造便可完成。對應已經存在已久的老項目也可以使用這種方式對新的路由進行裝飾器的寫法,本身定製 registerRouter
的實現達到漸進加強的效果。
本文實現的代碼在這裏 【Koa-Decorator-Demo】