在使用 express,koa, 或者是 egg.JS 进行 node server 开发的过程中, 我们的路由基本上都是定义在 controller 层的, 框架对于 node 原生路由都会进行一层封装, 一版都会封装到一个 router 对象, 提供 http 的 method 对应的方法, 然后在回调函数的入参中封装请求对象和响应对象.
- //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 来进行尝试, 改造后的形式如下:
- // 改造后的 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, 元数据等基本概念.
Decorator 装饰器
decorator 即是装饰器, 在不侵入类的原有代码的情况下在编译时给类添加行为或者修改行为的表现. 目前还在 es7 草案阶段, JS 中使用还需要 babel 装嘛, 但是在 TypeScript 目前通过配置 tsconfig 可以使用 decorator.
先来简单看下 decorator 作用在类和类的方法上的简单用法
- // 类的修饰
- @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 类型的定义:
- 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.
元数据 Metadata
元数据简单理解起来就是, 修饰数据的数据, 比如说的身材, 身材魁梧, 身材苗条, 这里身材为元数据的项目, 魁梧 / 苗条为元数据的内容. 一个人的描述正是由众多的元数据组成的,(长相, 身高, 年龄, 学历等等数据)
Annotation 注解
之前我对于注解和装饰器的概念经常搞混, 现在知道这是两个不同的概念:
注解 仅提供附加元数据支持, 并不能实现任何操作.
装饰器 仅提供定义劫持, 能够对类及其方法的定义但是没有提供任何附加元数据的功能.
所以对于 decorator 来言, 是无法直接进行元数据的操作的, 想要对元数据进行操作, 还需要借助于比如 Object 或者 Reflect-metadata 来实现
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 改造后的插件可以进行使用, 虽然使用的方式各有迥异, 但是其中的实现原理都是大体相同.
来源: https://www.cnblogs.com/blackgan/p/10192359.html