本文章默认听众使用Koa框架。

在我们使用nodejs开发项目的时候,如果项目中有很多路由,那么我们需要写很多的类似router.get('xxx', () => {})这样的代码。

var Koa = require('koa');
var Router = require('koa-router');
var app = new Koa();
var router = new Router();
router
.get('/', (ctx, next) => {
ctx.body = 'Hello World!';
})
.post('/users', (ctx, next) => {
// ...
})
.put('/users/:id', (ctx, next) => {
// ...
})

熟悉Java的朋友应该都知道, 可以通过以下形式定义路由

@Slf4j
@Controller
@RequestMapping(value = "/user")
public class HomeController {
@RequestMapping(value = "/list", method = RequestMethod.GET)
@ResponseBody
...
@RequestMapping(value = "/update", method = RequestMethod.POST)
@ResponseBody
...
}

这样不仅简化、规范化了路由的写法,减少了代码的冗余和错误,还使代码含义一目了然,无需注释也能通俗易懂。那么Java中的注解功能对应到NodeJs,分别是一下两种工具:装饰器与元编程。我们是不是也可以通过装饰器来达到我们的目的呢?首先让我们先来了解一下他们。

Decorator(装饰器)

装饰器提供了一种为类声明和成员添加注释和元编程语法的方法。装饰器是JavaScriptstage-2 proposal,可作为 TypeScript 的实验性功能使用。

注意装饰器是一项实验性功能,可能会在未来版本中更改。

要启用对装饰器的实验性支持,必须要在tsconfig.json中配置打开experimentalDecorators这个编译属性

定义

interface TypedPropertyDescriptor<T> {
enumerable?: boolean;
configurable?: boolean;
writable?: boolean;
value?: T;
get?: () => T;
set?: (value: T) => void;
}
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;

执行顺序

  1. 不同类型的装饰器的执行顺序是明确定义的:
    1. 实例成员: 参数装饰器 -> 方法 / 访问器 / 属性 装饰器
    2. 静态成员: 参数装饰器 -> 方法 / 访问器 / 属性 装饰器
    3. 构造器: 参数装饰器
    4. 类装饰器
  2. 同一方法中不同参数的装饰器的执行顺序是相反的, 最后一个参数的装饰器会最先被执行
  3. 多个装饰器组合应用,他们的执行顺序是由上到下编译,由下到上执行(FILO),可以当做栈来理解
function f(key: string): any {
console.log("evaluate: ", key);
return function () {
console.log("call: ", key);
};}

@f("Class Decorator")
class C {
@f("Static Property")
static prop?: number;

@f("Static Method")
static method(@f("Static Method Parameter") foo) {}

constructor(@f("Constructor Parameter") foo) {}

@f("Instance Method")
method(@f("Instance Method Parameter") foo) {}

@f("Instance Property")
prop?: number;
}

reflect-metadata(元数据)

严格地说,元数据和装饰器是EcmaScript中两个独立的部分。 然而,如果你想实现像是反射这样的能力,你总是同时需要它们。
有了reflect-metadata的帮助, 我们可以获取编译期的类型。

使用

Reflect Metadata 是 ES7 的一个提案(stage-2),它主要用来在声明的时候添加和读取元数据。TypeScript 在 1.5+ 的版本已经支持它,你只需要:

  • npm i reflect-metadata --save
  • tsconfig.json 里配置 emitDecoratorMetadata 选项。
@Reflect.metadata('name', 'A')class A {
@Reflect.metadata('hello', 'world')
public hello(): string {
return 'hello world'
}}

Reflect.getMetadata('name', A) // 'A'
Reflect.getMetadata('hello', new A()) // 'world'

API

// define metadata on an object or property
Reflect.defineMetadata(metadataKey, metadataValue, target);
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);
// check for presence of a metadata key on the prototype chain of an object or property
let result = Reflect.hasMetadata(metadataKey, target);
let result = Reflect.hasMetadata(metadataKey, target, propertyKey);
// check for presence of an own metadata key of an object or property
let result = Reflect.hasOwnMetadata(metadataKey, target);
let result = Reflect.hasOwnMetadata(metadataKey, target, propertyKey);
// get metadata value of a metadata key on the prototype chain of an object or property
let result = Reflect.getMetadata(metadataKey, target);
let result = Reflect.getMetadata(metadataKey, target, propertyKey);
// get metadata value of an own metadata key of an object or property
let result = Reflect.getOwnMetadata(metadataKey, target);
let result = Reflect.getOwnMetadata(metadataKey, target, propertyKey);
// get all metadata keys on the prototype chain of an object or property
let result = Reflect.getMetadataKeys(target);
let result = Reflect.getMetadataKeys(target, propertyKey);
// get all own metadata keys of an object or property
let result = Reflect.getOwnMetadataKeys(target);
let result = Reflect.getOwnMetadataKeys(target, propertyKey);
// delete metadata from an object or property
let result = Reflect.deleteMetadata(metadataKey, target);
let result = Reflect.deleteMetadata(metadataKey, target, propertyKey);
// apply metadata via a decorator to a constructor
@Reflect.metadata(metadataKey, metadataValue)
class C {
// apply metadata via a decorator to a method (property)
@Reflect.metadata(metadataKey, metadataValue)
method() {
}
}

如何用装饰器打造路由?

  1. 提供需要注册的装饰器,通过metadata存储对应的数据信息。
    1. 类装饰器:@Controller()
    2. 方法装饰器:@Get()、@Post()、@Put()、@Delete()
    3. 参数装饰器:@Request()、@Response()、@Ctx()、@Body()、@Param()、@Session()等等。
  2. 创建一个依赖注入的容器,收集所有与路由相关的配置,并进行路由注册。
    1. 遍历文件夹中所有符合条件的Controller文件,并获取相应的Controller类
    2. 扫描每个Controller类的实例方法,检测是否需要路由注册,并根据metadata中的数据进行路由的组装与注册
  3. 绑定Controller实例方法到KoaRouter中,并注入相关参数。

总结

  • 在项目启动阶段,会创建一个依赖注入容器(Container),扫描所有用户代码(Controller)中的文件,将拥有@Controller装饰器的 Class,保存到容器中。
  • 这里的依赖注入容器内部维护了一个数组。用来存放类本身。
  • 在扫描时,会动态实例化这些 Class,并且绑定属性方法(参数)与路由的动态关系,并且注册到router中

以上就是路由器注入的核心过程。

场景扩展

  • Before/After钩子。
  • 监听属性改变或者方法调用。
  • 对方法的参数做转换。
  • 添加额外的方法和属性。
  • 运行时类型检查。
  • 依赖注入(dependency injection)

参考资料