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 & 元数据反射:从小白到专家

相关文章
相关标签/搜索