在使用express,koa, 或者是egg.js進行node server開發的過程當中,咱們的路由基本上都是定義在controller層的,框架對於 node 原生路由都會進行一層封裝,一版都會封裝到一個router對象,提供http的method對應的方法,而後在回調函數的入參中封裝請求對象和響應對象。javascript
//koa 中koa-router中的router.js router.get('/home',async (ctx:{req,res},next)=>{ let reqParams = req.query; res.body = {a:1} }) //egg.js app/router.js module.exports = app => { const { router, controller } = app; router.get('/user/:id', controller.user.info); };
相似上邊爲koa-router和egg.js中設置的路由。路由的設置並非特別明顯直觀。此次的路由改造示例,是使用egg.js來進行嘗試,改造後的形式以下:html
//改造後的controller @prefix('/page') export default class PageController extends Controller { @get('/example') public async index():Promise<void> { const {ctx} = this; ctx.body={a:1}; ctx.status = 200; } }
在進行改造的過程當中,是在TypeScript環境中使用Decorator+Reflect-metadata來對egg.js的controller進行改造,主要須要瞭解的概念有:Decorator,註解,Reflect,元數據等基本概念。java
decorator便是裝飾器,在不侵入類的原有代碼的狀況下在編譯時給類添加行爲或者修改行爲的表現。目前還在es7草案階段,js中使用還須要babel裝嘛,可是在 TypeScript 目前經過配置tsconfig可使用decorator。node
先來簡單看下decorator做用在類和類的方法上的簡單用法git
//類的修飾 @setHelloDecorator class oneClass {} function setHelloDecorator(target){ target.hello = true; } //類的方法的修飾 class towClass { @nonenumerable someMethod(){} } function nonenumerable(target, key, descriptor){ //target 修飾方法的類的原型對象 //key 修飾方法名 //descriptor 修飾方法的描述對象 descriptor.writable = false; }
在TypeScript的源碼中能夠找到支持Decorator類型的定義:es6
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void; declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void; declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void; declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;
能夠看到decorator能夠用來修飾class,property,method,parameter。github
元數據簡單理解起來就是,修飾數據的數據,好比說的身材,身材魁梧,身材苗條,這裏身材爲元數據的項目,魁梧/苗條爲元數據的內容。一我的的描述正是由衆多的元數據組成的,(長相,身高,年齡,學歷等等數據)typescript
以前我對於註解和裝飾器的概念常常搞混,如今知道這是兩個不一樣的概念:express
因此對於decorator來言,是沒法直接進行元數據的操做的,想要對元數據進行操做,還須要藉助於好比Object或者Reflect-metadata來實現babel
反射這個名詞是用於描述可以檢查同一系統中的其餘代碼的代碼,程序在運行時可以獲取自身的信息。
固然咱們也可使用for...in或者是Object.getOwnPropertyDescriptor等來反射獲取某個類或者類屬性的信息。可是原有的各類方法調用方式較爲複雜。
ES6中已經有了一個Reflect對象,在MDN中的定義爲:
Reflect 是一個內置的對象,它提供攔截 JavaScript 操做的方法。這些方法與處理器對象的方法相同。Reflect不是一個函數對象,所以它是不可構造的。你不能將其與一個new運算符一塊兒使用,或者將Reflect對象做爲一個函數來調用。Reflect的全部屬性和方法都是靜態的(就像Math對象)。
Reflect對象可以將實現反射機制的方法都彙總於一個地方,而且用法上更簡單。讓咱們操做Object時更加方便簡潔。
ES6提供的Refelct並不知足修改元數據,咱們要額外引入一個庫reflect-metadata。而且Decorator中也沒法進行元數據的獲取和修改。
Reflect Metadata 是 ES7 的一個提案,它主要用來在聲明的時候添加和讀取元數據。TypeScript 在 1.5+ 的版本已經支持它。目前JS中的裝飾器更多仍是對類或者類的屬性進行一些操做,經過Reflect Metadata來反射獲取類或者方法上的修飾器的信息
使用reflect-metadata來自定義類上的元數據,在註冊路由的時候取出使用。
function classDecorator(): ClassDecorator { return target => { // 在類上定義元數據,key 爲 `classMetaData`,value 爲 `a` Reflect.defineMetadata('classMetaData', 'a', target); }; } function methodDecorator(): MethodDecorator { return (target, key, descriptor) => { // 在類的原型屬性 'someMethod' 上定義元數據,key 爲 `methodMetaData`,value 爲 `b` Reflect.defineMetadata('methodMetaData', 'b', target, key); }; }
在類和類的方法上自定義完,使用 getMetadata 來取出所定義的元數據
@classDecorator() class oneClass{ @methodDecorator() otherMethod(){} } Reflect.getMetadata('classMetaData',oneClass); //返回 'a'; Reflect.getMetadata('methodMetaData',new OneClass(),'otherMehod'); //返回 'b';
//改造後的controller @prefix('/page') export default class PageController extends Controller { @get('/example') public async index():Promise<void> { const {ctx} = this; ctx.body={a:1}; ctx.status = 200; } }
prefix裝飾器爲
const CONTROLLER_PREFIX_METADATA = 'CONTROLLER_PREFIX_METADATA'; export function prefix(pathPrefix: string = ''): ClassDecorator { return (targetClass) => { //將 path的前綴路徑添加到賦值到class的元數據 Reflect.defineMetadata(CONTROLLER_PREFIX_METADATA, pathPrefix, targetClass); }; }
get裝飾器爲
export const controllerMap = new Map<typeof Controller, typeof Controller>(); export function get(path: string = '/get') { return (target, _key, descriptor) => { //將有裝飾器的controller添加到controllerMap controllerMap.set(target, target); Reflect.defineMetadata('PATH_METADATA', path, descriptor.value); Reflect.defineMetadata('METHOD_METADATA','get', descriptor.value); }; }
以此類推,能夠寫出POST,DELETE,PUT,HEAD,OPTIONS等http請求
將獲取到的controller和路由進行註冊
export enum RequestMethod { ALL = 'all', GET = 'get', POST = 'post', PUT = 'put', DELETE = 'delete', PATCH = 'patch', OPTIONS = 'options', HEAD = 'head', } export default function RouteShell(app: Application) { const { router, config } = app; controllerMap.forEach((controller: typeof Controller) => { const controllerPrefix = Reflect.getMetadata(CONTROLLER_PREFIX_METADATA, controller.constructor) || ''; Object.getOwnPropertyNames(controller).filter( (pName: string) => pName !== 'constructor' && pName !== 'pathName' && pName !== 'fullPath', ).forEach((pName: string) => { const path = Reflect.getMetadata('PATH_METADATA', controller[pName]); const method = Reflect.getMetadata('METHOD_METADATA', controller[pName]) as RequestMethod; const wrap: (this: Context, ...args: any[]) => void = async function (...args) { return new (controller.constructor as any)(this)[pName](...args); }; //路由註冊 router[method](controllerPrefix + path, wrap); }); }); }
通過此次對於controller層的改造,讓我更加深刻了解了ts中Decorator的編譯產物,以及Decorator針對類的修飾和類的方法的修飾不一樣,進行參數傳遞的不一樣。同時使用Decorator+Reflect的方式在裝飾器中可以方便簡單的進行元數據的操做。而且對於egg.js的controller有了更加深刻的瞭解,固然,如今也已經有了好幾個egg-controller改造後的插件能夠進行使用,雖然使用的方式各有迥異,可是其中的實現原理都是大致相同。