装饰器(Decorators)是一种特殊的声明,可附加在类、方法、访问器、属性、参数声明上。

装饰器(Decorators)可以通过非侵入方式增强方法访问器属性参数的能力。可以将业务逻辑代码和特殊能力和服务的代码分割开来,保证代码的灵活性和扩展性,合理的利用装饰器可以极大的提高我们的开发效率。

注意: 同样的滥用装饰器也会使代码本身逻辑变得扑朔迷离,如果确定一段代码不会在其他地方用到,或者一个函数的核心逻辑就是这些代码,那么就没有必要将它取出来作为一个装饰器来存在。

为什么需要装饰器

例如: 我们想记录一个方法执行的耗时

没有使用装饰器方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Model1 {
getData() {
let start = new Date().valueOf()
try {
// 此处省略获取数据的逻辑
return [{
id: 1,
name: 'Niko'
}, {
id: 2,
name: 'Bellic'
}]
} finally {
let end = new Date().valueOf()
console.log(`start: ${start} end: ${end} consume: ${end - start}`)
}
}
}

// start: XXX end: XXX consume: XXX
console.log(new Model1().getData()) // [ { id: 1, name: 'Niko'}, { id: 2, name: 'Bellic' } ]
// start: XXX end: XXX consume: XXX
console.log(Model1.prototype.getData()) // [ { id: 1, name: 'Niko'}, { id: 2, name: 'Bellic' } ]

在上面代码中,getData方法中包含了一些非业务的逻辑代码。而这些代码还是不可复用的。

使用装饰器的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function log(target, descriptor, descriptor) {
Object.defineProperty(target, key, {
...descriptor,
value: function (...arg) {
let start = new Date().valueOf()
try {
return descriptor.value.apply(this, arg) // 调用之前的函数
} finally {
let end = new Date().valueOf()
console.log(`start: ${start} end: ${end} consume: ${end - start}`)
}
}
})
}

class Model1 {
@log
getData() {
return [{
id: 1,
name: 'Niko'
}, {
id: 2,
name: 'Bellic'
}]
}
}

上面的代码中,我们将增加了一个log用来记录方法执行的时间。通过装饰器我们将业务逻辑和非业务逻辑代码分割开来。

装饰器函数执行时机

装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。也就是说,修饰器本质就是编译时执行的函数。

  • 创建类和创建函数时立即执行装饰器
  • 如果装饰器函数返回一个function,这个function会在所有的装饰器函数运行完毕后,继续运行

如何启用

目前 装饰器(Decorators)处于ECMAscript提案第二阶段(Stage2 Draft)。目前Typescript已经支持,只需要在tsconfig.json做一下简单配置

1
2
3
4
5
6
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}

装饰器种类

Typescript中装饰器有五种类型:

装饰器/属性 类 装饰器 方法 装饰器 访问器 装饰器 属性 装饰器 参数 装饰器
位置 @foo class Bar {} @foo public bar() {} @foo get bar() @foo() bar: number bar(@foo para: string) {}
传入参数 constructor target,
propertyKey,
descriptor
target,
propertyKey,
descriptor
target, propertyKey target,
propertyKey,
parameterIndex
返回值 用返回值提供的构造函数来替换类的声明 返回值被用作方法的属性描述符 返回值被用作方法的属性描述符 被忽略 被忽略

参数释义:

  • constructor: 类构造函数
  • target: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • propertyKey: 成员的名字
  • descriptor: 成员的属性描述符
  • parameterIndex: 参数在函数参数列表中的索引

Typescript 中的 Decorator 签名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;

Class 装饰器

类装饰器的类型定义如下:

1
type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;

class 装饰器应用于类的构造器,可用于观测、修改、替换类定义。

类装饰器只有一个参数: constructorconstructor为类的构造函数。类装饰器的返回值可以为空,也可以是一个新的构造函数

扩展构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@name
class Person {
sayHi() {
console.log(`My name is: ${this.name}`)
}
}

// 创建一个继承自Person的匿名类
// 直接返回并替换原有的构造函数
function name(constructor) {
return class extends constructor {
name = 'Niko'
}
}

new Person().sayHi()

修改原有属性的描述符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@seal
class Person {
sayHi() {}
}

function seal(constructor) {
let descriptor = Object.getOwnPropertyDescriptor(constructor.prototype, 'sayHi')
Object.defineProperty(constructor.prototype, 'sayHi', {
...descriptor,
writable: false
})
}

Person.prototype.sayHi = 1 // 无效

使用闭包来增强装饰器的功能

因为@符号后边跟的是一个函数的引用,所以对于mixin的实现,我们可以很轻易的使用闭包来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A { say() { return 1 } }
class B { hi() { return 2 } }

@mixin(A, B)
class C { }

function mixin(...args) {
// 调用函数返回装饰器实际应用的函数
return function(constructor) {
for (let arg of args) {
for (let key of Object.getOwnPropertyNames(arg.prototype)) {
if (key === 'constructor') continue // 跳过构造函数
Object.defineProperty(constructor.prototype, key, Object.getOwnPropertyDescriptor(arg.prototype, key))
}
}
}
}

let c = new C()
console.log(c.say(), c.hi()) // 1, 2

覆盖旧的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function classDecorator<T extends { new (...args: any[]): {} }>( constructor: T) {
return class extends constructor {
newProperty = "new property";
hello = "override";
};
}

@classDecorator
class Greeter{
property = "property";

hello: string;

constructor(m: string) {
this.hello = m;
}
}
const greeter: Greeter = new Greeter("world");

console.log({ greeter }, greeter.hello);

/**
{
"greeter": {
"property": "property",
"hello": "override",
"newProperty": "new property"
}
}, "override"
*/

多个装饰器的应用

可以多个装饰器一起使用:

1
2
3
@decorator1
@decorator2
class { }

执行的顺序为decorator2 -> decorator1,离class定义最近的先执行。

Method 装饰器

方法装饰器的类型定义如下:

1
type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;

方法装饰器有 3 个参数 target 、 propertyKey 和 descriptor。

  • target静态方法是类的构造函数,实例方法是类的原型对象
  • propertyKey: 方法名
  • descriptor: 属性描述符 方法装饰器的返回值可以为空,也可以是一个新的属性描述符。

函数装饰是如果有返回值会默认作为属性的value描述符存在,如果返回值为undefined则会忽略,使用之前的descriptor引用作为函数的描述符。

方案一 修改现有描述符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const Log: MethodDecorator = (target: any, key: string, descriptor: PropertyDescriptor) => {
const className = target.constructor.name;
const oldValue = descriptor.value;
descriptor.value = function(...params) {
console.log(`调用${className}.${key}()方法`);
return oldValue.apply(this, params);
};
};

class MyClass {
private name: string;

constructor(name: string) {
this.name = name;
}

@Log
getName (): string {
return 'Tom';
}
}

const entity = new MyClass('Tom');
const name = entity.getName();
// 输出: 调用MyClass.getName()方法

@Log 是一个方法装饰器 ,使用时添加到方法声明前,用于自动输出方法的调用日志。方法装饰器的第 3 个参数是属性描述符,属性描述符的 value 表示方法的执行函数,用一个新的函数替换了原来值,新的方法还会调用原方法,只是在调用原方法前输出了一个日志。

方案二 返回新的value描述符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const Log: MethodDecorator = (target: any, key: string, descriptor: PropertyDescriptor) => {
const className = target.constructor.name;
const oldValue = descriptor.value;
return {
...descriptor,
value(...args) {
console.log(`调用${className}.${key}()方法`);
return oldValue.apply(this, args);
}
}
};

class MyClass {
private name: string;

constructor(name: string) {
this.name = name;
}

@Log
getName (): string {
return 'Tom';
}
}

const entity = new MyClass('Tom');
const name = entity.getName();

Property 装饰器

属性装饰器的类型定义如下:

1
type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;

属性装饰器有两个参数 target 和 propertyKey。

  • target静态属性是类的构造函数,实例属性是类的原型对象
  • propertyKey:属性名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface CheckRule {
required: boolean;
}
interface MetaData {
[key: string]: CheckRule;
}

const Required: PropertyDecorator = (target: any, key: string) => {
target.__metadata = target.__metadata ? target.__metadata : {};
target.__metadata[key] = { required: true };
};

class MyClass {
@Required
name: string;

@Required
type: string;
}

@Required 是一个属性装饰器,使用时添加到属性声明前,作用是在 target 的自定义属性 metadata 中添加对应属性的必填规则

上例添加装饰器后 target.metadata 的值为:{ name: { required: true }, type: { required: true } }。通过读取 __metadata 可以获得设置的必填的属性,从而对实例对象进行校验,校验相关的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function validate(entity): boolean {
// @ts-ignore
const metadata: MetaData = entity.__metadata;
if(metadata) {
let i: number,
key: string,
rule: CheckRule;
const keys = Object.keys(metadata);
for (i = 0; i < keys.length; i++) {
key = keys[i];
rule = metadata[key];
if (rule.required && (entity[key] === undefined || entity[key] === null || entity[key] === '')) {
return false;
}
}
}
return true;
}

const entity: MyClass = new MyClass();
entity.name = 'name';
const result: boolean = validate(entity);
console.log(result); // 输出结果:false

Parameter 装饰器

参数装饰器的类型定义如下:

1
type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;

参数装饰器有 3 个参数 target 、 propertyKey 和 descriptor。

  • target静态方法的参数是类的构造函数,实例方法的参数是类的原型对象
  • propertyKey:参数所在方法的方法名
  • parameterIndex:在方法参数列表中的索引值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const parseConf = {}
class Modal {
@parseFunc
addOne(@parse('number') num) {
return num + 1
}
}

// 在函数调用前执行格式化操作
function parseFunc (target, name, descriptor) {
return {
...descriptor,
value (...arg) {
// 获取格式化配置
for (let [index, type] of parseConf) {
switch (type) {
case 'number': arg[index] = Number(arg[index]) break
case 'string': arg[index] = String(arg[index]) break
case 'boolean': arg[index] = String(arg[index]) === 'true' break
}
}

return descriptor.value.apply(this, arg)
}
}
}

// 向全局对象中添加对应的格式化信息
function parse(type) {
return function (target, name, index) {
parseConf[index] = type
}
}

console.log(new Modal().addOne('10')) // 11

结合着函数装饰器来完成对函数参数的类型转换

访问符装饰器

访问符装饰器类型定义和 方法装饰器类型定义一样:

1
type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;

访问符装饰器的使用与方法装饰器一致,参数和返回值相同,只是访问符装饰器用在访问符声明之前。 需要注意的是,TypeScript 不允许同时装饰一个成员的 get 和 set 访问符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const Enumerable: MethodDecorator = (target: any, key: string, descriptor: PropertyDescriptor) => {
descriptor.enumerable = true;
};

class MyClass {
createDate: Date;
constructor() {
this.createDate = new Date();
}

@Enumerable
get createTime () {
return this.createDate.getTime();
}
}

const entity = new MyClass();
for(let key in entity) {
console.log(`entity.${key} =`, entity[key]);
}
/* 输出:
entity.createDate = 2020-04-08T10:40:51.133Z
entity.createTime = 1586342451133
*/

MyClass 类中有一个属性 createDate 为 Date 类型, 另外增加一个有 get 声明的 createTime 方法,就可以以 entity.createTime 方式获得 createDate 的毫秒值。 但是 createTime 默认是不可枚举的,通过在声明前增加 @Enumerable 装饰器可以使 createTime 成为可枚举的属性。

给属性设置前缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Modal {
_name = 'Niko'

@prefix
get name() { return this._name }
}

function prefix(target, name, descriptor) {
return {
...descriptor,
get () {
return `wrap_${this._name}`
}
}
}

console.log(new Modal().name) // wrap_Niko

静态类型和实例类型

默认Typescript属性是无法获取到的 descriptor,但 静态属性是可以手动拿到的,但实例属性不行。

什么是静态属性和实例属性
1
2
3
4
5
6
7
8
9
class Model {
// 实例成员
method1 () {} // 实例方法
method2 = () => {} //实例属性

// 静态成员
static method3 () {} // 静态方法
static method4 = () => {} // 静态属性
}

method1method2是实例成员,method1存在于prototype之上,而method2只在实例化对象以后才有。
作为静态成员的method3method4,两者的区别在于是否可枚举描述符的设置,所以可以简单地认为,上述代码转换为ES5版本后是这样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Model () {
// 成员仅在实例化时赋值
this.method2 = function () {}
}

// 成员被定义在原型链上
Object.defineProperty(Model.prototype, 'method1', {
value: function () {},
writable: true,
enumerable: false, // 设置不可被枚举
configurable: true
})

// 成员被定义在构造函数上,且是默认的可被枚举
Model.method4 = function () {}

// 成员被定义在构造函数上
Object.defineProperty(Model, 'method3', {
value: function () {},
writable: true,
enumerable: false, // 设置不可被枚举
configurable: true
})

可以看出,只有method2是在实例化时才赋值的,一个不存在的属性是不会有descriptor的,所以这就是为什么TS在针对Property Decorator不传递第三个参数的原因,至于为什么静态成员也没有传递descriptor.

就像上述的示例,我们针对四个成员都添加了装饰器以后,method1method2第一个参数就是Model.prototype,而method3method4的第一个参数就是Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Model {
// 实例成员
@instance
method1 () {}
@instance
method2 = () => {}

// 静态成员
@static
static method3 () {}
@static
static method4 = () => {}
}

function instance(target) {
console.log(target.constructor === Model)
}

function static(target) {
console.log(target === Model)
}

静态属性手动获取 descriptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Modal {
@prefix
static name1 = 'Niko'
}

function prefix(target, name) {
let descriptor = Object.getOwnPropertyDescriptor(target, name)

Object.defineProperty(target, name, {
...descriptor,
value: `wrap_${descriptor.value}`
})

return target
}

console.log(Modal.name1) // wrap_Niko

装饰器执行顺序

不同声明上的装饰器将按以下顺序执行:

  1. 实例成员的装饰器:参数装饰器 > 方法装饰器 > 访问符装饰器/属性装饰器
  2. 静态成员的装饰器:参数装饰器 > 方法装饰器 > 访问符装饰器/属性装饰器
  3. 构造函数的参数装饰器
  4. 类装饰器

如果同一个声明有多个装饰器,离声明越近的装饰器越早执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
function logClass1(params:string){
return function(target:any){
console.log('类装饰器1')
}
}

function logClass2(params:string){
return function(target:any){
console.log('类装饰器2')
}
}

function logAttribute1(params?:string){
return function(target:any,attrName:any){
console.log('属性装饰器1')
}
}

function logAttribute2(params?:string){
return function(target:any,attrName:any){
console.log('属性装饰器2')
}
}

function logMethod1(params?:string){
return function(target:any,attrName:any,desc:any){
console.log('方法装饰器1')
}
}
function logMethod2(params?:string){
return function(target:any,attrName:any,desc:any){
console.log('方法装饰器2')
}
}



function logParams1(params?:string){
return function(target:any,attrName:any,desc:any){
console.log('方法参数装饰器1')
}
}

function logParams2(params?:string){
return function(target:any,attrName:any,desc:any){
console.log('方法参数装饰器2')
}
}



@logClass1('http://www.loaderman.com/api')
@logClass2('xxxx')
class HttpClient{
@logAttribute1()
@logAttribute2()
public apiUrl:string | undefined;
constructor(){
}

@logMethod1()
@logMethod2()
getData(){
return true;
}

setData(@logParams1() attr1:any,@logParams2() attr2:any,){

}
}

var http:any=new HttpClient();

/**
[LOG]: "属性装饰器2"
[LOG]: "属性装饰器1"
[LOG]: "方法装饰器2"
[LOG]: "方法装饰器1"
[LOG]: "方法参数装饰器2"
[LOG]: "方法参数装饰器1"
[LOG]: "类装饰器2"
[LOG]: "类装饰器1"
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function f() {
console.log("f(): evaluated");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("f(): called");
}
}
function g() {
console.log("g(): evaluated");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("g(): called");
}
}
class C {
@f()
@g()
method() {}
}

结果:

1
2
3
4
f(): evaluated
g(): evaluated
g(): called
f(): called

装饰工厂🏭

由于每种装饰器都有它自身的调用签名,我们可以使用装饰器工厂来泛化装饰器调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { logClass } from './class-decorator';
import { logMethod } from './method-decorator';
import { logProperty } from './property-decorator';
import { logParameter } from './parameter-decorator';

// 装饰器工厂,根据传入的参数调用相应的装饰器
export function log(...args) {
switch (args.length) {
case 3: // 可能是方法装饰器或参数装饰器
// 如果第三个参数是数字,那么它是索引,所以这是参数装饰器
if typeof args[2] === "number") {
return logParameter.apply(this, args);
}
return logMethod.apply(this, args);
case 2: // 属性装饰器
return logProperty.apply(this, args);
case 1: // 类装饰器
return logClass.apply(this, args);
default: // 参数数目不合法
throw new Error('Not a valid decorator');
}
}

@log
class Employee {
@log
private name: string;

constructor(name: string) {
this.name = name;
}

@log
greet(@log message: string): string {
return `${this.name} says: ${message}`;
}
}

装饰器的应用

使用装饰器可以实现自动注册路由,通过给 Controller 层的类和方法添加装饰器来定义路由信息,当创建路由时扫描指定目录下所有 Controller,获取装饰器定义的路由信息,从而实现自动添加路由。

装饰器代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* src/common/decorator/controller.ts */
export interface Route {
propertyKey: string,
method: string;
path: string;
}

export function Controller(path: string = ''): ClassDecorator {
return (target: any) => {
Reflect.defineMetadata('basePath', path, target);
}
}

export type RouterDecoratorFactory = (path?: string) => MethodDecorator;

export function createRouterDecorator(method: string): RouterDecoratorFactory {
return (path?: string) => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
const route: Route = {
propertyKey,
method,
path: path || ''
};
if (!Reflect.hasMetadata('routes', target)) {
Reflect.defineMetadata('routes', [], target);
}
const routes = Reflect.getMetadata('routes', target);
routes.push(route);
}
}

export const Get: RouterDecoratorFactory = createRouterDecorator('get');
export const Post: RouterDecoratorFactory = createRouterDecorator('post');
export const Put: RouterDecoratorFactory = createRouterDecorator('put');
export const Delete: RouterDecoratorFactory = createRouterDecorator('delete');
export const Patch: RouterDecoratorFactory = createRouterDecorator('patch');

控制器代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* src/controller/roleController.ts */
import Koa from 'koa';
import { Controller, Get } from '../common/decorator/controller';
import RoleService from '../service/roleService';

@Controller()
export default class RoleController {

@Get('/roles')
static async getRoles (ctx: Koa.Context) {
const roles = await RoleService.findRoles();
ctx.body = roles;
}

@Get('/roles/:id')
static async getRoleById (ctx: Koa.Context) {
const id = ctx.params.id;
const role = await RoleService.findRoleById(id);
ctx.body = role;
}
}

路由器代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/* src/common /scanRouter.ts */
import fs from 'fs';
import path from 'path';
import KoaRouter from 'koa-router';
import { Route } from './decorator/controller';

// 扫描指定目录的Controller并添加路由
function scanController(dirPath: string, router: KoaRouter): void {
if (!fs.existsSync(dirPath)) {
console.warn(`目录不存在!${dirPath}`);
return;
}
const fileNames: string[] = fs.readdirSync(dirPath);

for (const name of fileNames) {
const curPath: string = path.join(dirPath, name);
if (fs.statSync(curPath).isDirectory()) {
scanController(curPath, router);
continue;
}
if (!(/(.js|.jsx|.ts|.tsx)$/.test(name))) {
continue;
}
try {
const scannedModule = require(curPath);
const controller = scannedModule.default || scannedModule;
const isController: boolean = Reflect.hasMetadata('basePath', controller);
const hasRoutes: boolean = Reflect.hasMetadata('routes', controller);
if (isController && hasRoutes) {
const basePath: string = Reflect.getMetadata('basePath', controller);
const routes: Route[] = Reflect.getMetadata('routes', controller);
let curPath: string, curRouteHandler;
routes.forEach( (route: Route) => {
curPath = path.posix.join('/', basePath, route.path);
curRouteHandler = controller[route.propertyKey];
router[route.method](curPath, curRouteHandler);
console.info(`router: ${controller.name}.${route.propertyKey} [${route.method}] ${curPath}`)
})
}
} catch (error) {
console.warn('文件读取失败!', curPath, error);
}

}
}

export default class ScanRouter extends KoaRouter {
constructor(opt?: KoaRouter.IRouterOptions) {
super(opt);
}

scan (scanDir: string | string[]) {
if (typeof scanDir === 'string') {
scanController(scanDir, this);
} else if (scanDir instanceof Array) {
scanDir.forEach(async (dir: string) => {
scanController(dir, this);
});
}
}
}

创建路由代码

1
2
3
4
5
6
7
8
9
/* src/router.ts */
import path from 'path';
import ScanRouter from './common/scanRouter';

const router = new ScanRouter();

router.scan([path.resolve(__dirname, './controller')]);

export default router;

Refer To

https://juejin.cn/post/6844903635168526343#heading-15

https://www.infoq.cn/article/ikvkdvq9haopv5yfppmj

https://www.typescriptlang.org/docs/handbook/decorators.html

https://yrq110.me/post/front-end/typescript-decorator-practice/