egg.js路由的優雅改造

引言

在使用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 裝飾器

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

元數據 Metadata

元數據簡單理解起來就是,修飾數據的數據,好比說的身材,身材魁梧,身材苗條,這裏身材爲元數據的項目,魁梧/苗條爲元數據的內容。一我的的描述正是由衆多的元數據組成的,(長相,身高,年齡,學歷等等數據)typescript

Annotation 註解

以前我對於註解和裝飾器的概念常常搞混,如今知道這是兩個不一樣的概念:express

  • 註解 僅提供附加元數據支持,並不能實現任何操做。
  • 裝飾器 僅提供定義劫持,可以對類及其方法的定義可是沒有提供任何附加元數據的功能。

因此對於decorator來言,是沒法直接進行元數據的操做的,想要對元數據進行操做,還須要藉助於好比Object或者Reflect-metadata來實現babel

JavaScript中須要反射Reflect

反射這個名詞是用於描述可以檢查同一系統中的其餘代碼的代碼,程序在運行時可以獲取自身的信息。

固然咱們也可使用for...in或者是Object.getOwnPropertyDescriptor等來反射獲取某個類或者類屬性的信息。可是原有的各類方法調用方式較爲複雜。

ES6中已經有了一個Reflect對象,在MDN中的定義爲:

Reflect 是一個內置的對象,它提供攔截 JavaScript 操做的方法。這些方法與處理器對象的方法相同。Reflect不是一個函數對象,所以它是不可構造的。你不能將其與一個new運算符一塊兒使用,或者將Reflect對象做爲一個函數來調用。Reflect的全部屬性和方法都是靜態的(就像Math對象)。

Reflect對象的設計目的有:
  • 將Object對象的一些明顯屬於語言內部的方法(好比Object.defineProperty),放到Reflect對象上
  • 修改某些Object方法的返回結果,讓其變得更合理。
  • 讓Object操做都變成函數行爲。某些Object操做是命令式,好比name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)讓它們變成了函數行爲。
  • Reflect對象的方法與Proxy對象的方法一一對應,只要是Proxy對象的方法,就能在Reflect對象上找到對應的方法。

Reflect對象可以將實現反射機制的方法都彙總於一個地方,而且用法上更簡單。讓咱們操做Object時更加方便簡潔。

Reflect Metadata

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';

egg.js controller層改造

//改造後的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改造後的插件能夠進行使用,雖然使用的方式各有迥異,可是其中的實現原理都是大致相同。

參考

修飾器-阮一峯

反射-阮一峯

深刻理解 TypeScript- Reflect Metadata

TypeScript 中的 Decorator & 元數據反射:從小白到專家

相關文章
相關標籤/搜索