typescript 装饰器和元数据的一次实践

77 阅读7分钟

前言

本文作者之前在学些nestjs 和 ts 相关知识,后面自己想着能不能实现一个类似,同时也能巩固之前所学的知识,所以就有了这个项目,现在只实现了简单的部分,后续可能会慢慢追加新功能。 此项目是基于typescript + express 写的装饰器风格的后台接口项目,代码结构模仿了nestjs,使用装饰器和reflect-metadata实现了相关内容。目前完成了以下几个功能

  • 装饰器风格的路由器语法
  • 利用构造器注入的方式实现了依赖注入
  • 基于模块化的IOC 容器,模块可以有自己的Provider 也可以导入其他模块

当前项目的最后形态是这样的

.
├── const.ts
├── decorator
│   ├── http.decorator.ts
│   └── validator.decorator.ts
├── dto
│   └── pagation.dto.ts
├── index.ts
├── init.ts
├── lib
│   └── ioc.ts
└── modules
	├── app
	│   ├── app.container.ts
	│   ├── app.controller.ts
	│   └── app.module.ts
	└── users
		├── user.container.ts
		├── user.controller.ts
		├── user.module.ts
		└── user.service.ts

controller 写法是这个样子

import { Body, Get, Post } from '../../decorator/http.decorator'
import { PagationDto } from '../../dto/pagation.dto'
import { ValidateDto } from '../../decorator/validator.decorator'
import { UserService } from './user.service'
import { Controller } from './user.container'

@Controller('/user')
export class UserController {
    constructor(
        private userService: UserService
    ) { }

    @Post("/findall")
    @ValidateDto(PagationDto)
    async findAll(@Body() body: PagationDto) {
        const result = await this.userService.findAll(body)
        return {
            code: 0,
            data: result,
            message: "success"
        }
    }
}

项目的大概情况介绍完成了,在我们一步一步的实现它

具体实现

实现路由

关于路由的设置有两部分,一个是http.decorator 里设置元数据 ,然后在init.ts 里获取元数据,组装成路由,下面是init.ts 的核心部分,就是把Controller 路由类传进来,获取当中的元数据拼装成路由路径,最终设置到express 的路由里。

设置的部分 http.decorator.ts

import "reflect-metadata";
import { PATH_METADETA, METHOD_METADETA, PARAMS_METADETA } from "../const";

export function Controller(path: string): ClassDecorator {
    return (target) => {
        //给当前类添加一个元数据
        Reflect.defineMetadata(PATH_METADETA, path, target);
    }
}

// 创建各类请求装饰器的工厂函数
function createdMethod(methods: string) {
    return (path: string): MethodDecorator => {
        return (target: any, key: string | symbol, descriptor: any) => {
            //给类的属性添加属性,描述当前属性的路径
            Reflect.defineMetadata(PATH_METADETA, path, target, key);

            //添加方法元数据,描述当前方法为什么方法
            Reflect.defineMetadata(METHOD_METADETA, methods, target, key)
        }
    }
}

function createParams(param: string) {

    return (field?: string): ParameterDecorator => {
        return (target: any, name: string, index: number) => {
            // 取出原有的数据
            let params =
                Reflect.getMetadata(PARAMS_METADETA, new target.constructor(), name) || [];
            // 添加新的并加进去
            params.push({ key: param, index: index, field: field });
            Reflect.defineMetadata(PARAMS_METADETA, params, target, name);
        };
    };
}

// 使用@Get 它设置了两个元数据,一个是自带的METHOD_METADETA 只是'get',另一就是使用的时候设置的path
export const Get = createdMethod('get')
export const Post = createdMethod('post')

export const Body = createParams("body");

组装的部分 init.ts

export function moduleInit(app: any, controllers: Array<any>) {

    const router: any = Router()
    
    controllers.forEach((ctrl: any) => {

        // 获取类上的路径(Controller 装饰器里参数),有类装饰器的target 就是类本身,所以直接取,不需要实例化
        const classPath = Reflect.getMetadata(PATH_METADETA, ctrl)

        // 获取实例属性上面的元数据
        const ctrlInstance = new ctrl()

        // 获取实例里所有的路由函数
        const functionNames = getMethodsOfClassInstance(ctrlInstance)

        for (const key of functionNames) {

            //取实例上的方法 Post("/list") 当中的post
            const attrMethod: string = Reflect.getMetadata(METHOD_METADETA, ctrlInstance, key);

            //取实例上的方法 Post("/list") 当中的 /list
            const attrPath: string = Reflect.getMetadata(PATH_METADETA, ctrlInstance, key);

            // 取路由函数里的参数,像body,request 这些字符串
            let attrParams = Reflect.getMetadata(PARAMS_METADETA, ctrlInstance, key);

            attrParams = attrParams.sort((a: any, b: any) => a.index - b.index)

            // 路由的完整路径 项目公共前缀 + 类上公共路径 + 自己路由函数上的路径
            const routerUrl = routerPrefix + classPath + attrPath

            // 最终执行业务部分,路由处理函数
            let fn: any = (req: Request, res: Response, next: Function) => {

                // 取到实际的参数,例如实际body 值,query值等
                const params = getParameters(req, res, attrParams)
				// 执行原流程的业务
                const result = ctrlInstance[key].apply(ctrlInstance, params) 

                if (result instanceof Promise) {
                    result.then(val => {
                        !res.headersSent && res.send(val);
                    }).catch(err => {
                        next(err)
                    })
                } else {
                    !res.headersSent && res.send(result);
                }
            }
            router[attrMethod].apply(router, [routerUrl, fn])
        }
    })

    app.use(router)

}

总结下就是 各种装饰器给类以及方法设置元数据,接下来有统一取数的地方把之前设置的元数据进行组装,并设置到路由上。

验证

这里就是使用class-validator 做了一个验证中间件,如果验证失败直接抛出错误,验证成功才会走路由逻辑 ,中间件需要的参数就是个普通的Dto 类

import { validate, ValidationError } from 'class-validator';
import { plainToInstance } from 'class-transformer';

export function ValidateDto<T>(dtoClass: new () => T): MethodDecorator {
    return function (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {

        const originalMethod = descriptor.value;
        descriptor.value = async function (...args: any[]) {
            
            // 假设需要验证的参数在第一个位置
            const dto = plainToInstance<T, object>(dtoClass, args[0]);

            // 获取验证错误
            const errors: ValidationError[] = await validate(dto, { stopAtFirstError: true, whitelist: true });

            // 如果有错误直接抛出,不执行原函数的逻辑
            if (errors.length > 0) {
                const validationErrors = errors.map(error => {
                    if (error.constraints) {
                        return Object.values(error.constraints)
                    } else {
                        return [error.property + "字段不合法"]
                    }
                }).join(', ');
                throw new Error(validationErrors);
            }

            //将实例化后的参数替换原参数的位置
            args[0] = dto;
            //开始执行路由函数逻辑
            return originalMethod.apply(this, args);
        };
    };
}

src/dto/pagation.dto.ts


import {IsNumber, Min } from "class-validator"
export class PagationDto {
    @IsNumber()
    @Min(1)
    readonly pageNum: number;

    @IsNumber()
    @Min(1)
    readonly pageSize: number;
}

依赖注入

这里主要实现的就是依赖注入的容器,这里为了实现简便写了全局的容器,内部方法都静态的,下面一节会把他变成实例化的版。 src/lib/ioc.ts

export type ClassType<T = any> = new (...args: any[]) => T;
export type ServiceKey<T = any> = string | ClassType<T> | Function;
import "reflect-metadata";


export class Container {
    // 所有service
    private static services: Map<ServiceKey, ClassType> = new Map() 
    // 缓存的service 单例对象
    private static pools: Map<ClassType, any> = new Map()
  
    public static setService(key: ServiceKey, value: ClassType): void {
        Container.services.set(key, value)
    }

    public static get<T = any>(key: ServiceKey): T | undefined {

        let targetServices = Container.services.get(key)
        if (!targetServices) {
            if (typeof key === 'function') {
                targetServices = key as ClassType;
            } else {
                throw new Error(`No service found for key: ${String(key)}`);
            }
        }
        
        return Container.createInstance(targetServices)

    }

    public static createInstance<T = any>(impl: ClassType): T | undefined {
    
        if (Container.pools.has(impl)) {
            return Container.pools.get(impl)
        }
		
        const paramTypes = Reflect.getMetadata('design:paramtypes', impl) || [];
        const params = paramTypes.map((paramType: ClassType) => Container.get(paramType));
        const instance = new impl(...params);
        this.pools.set(impl, instance)
        
        return instance
    }
}

export function Injectable(): ClassDecorator {
    return (target) => {
        Container.setService(target, target as unknown as ClassType);
    };
}

先说下用法,我们在service类上加上Injectable() 这样就会被注册进容器里


@Injectable()
export class UserService {
    async findAll(body: PagationDto) {
        return body
    }
}

获取controller 类实例的地方是则init.ts里的moduleInit ,我们把

const ctrlInstance = new ctrl()
// 替换成
const ctrlInstance = Container.get(ctrl)

其他不用动,本次修改就是增加了一个作为容器的类,改变了下路由实例的获取方法。

整体流程(可以参照上面的Controller 一起看)

  • 通过 @Injectable 把当前类注册进容器
  • 通过容器获取某个类的实例,如果容器已缓存当前类实例则直接返回
  • 容器在实例化某个类的时候
    • 通过design:paramtypes 获取其构造函数的的参数的类型
    • 再次通过容器获取当前所有参数的实例
    • 把上面所有实例作为传入类impl构造函数的参数,new 一个实例出来,并把实例缓存

完成之后,Controller 这个类就拿到了 userService 的实例,可以正常使用了。

模块化

最终形态来了,当前版本单独实例化一个容器 src/lib/ioc.ts

... // 相同省略
import { PATH_METADETA, IOC_PROVIDER, IOC_CONTROLLER } from "../const";

export class Container {
    // 相同省略
    private controllers: Map<ServiceKey, ClassType> = new Map(); // 所有controller
    private modules: Map<ClassType, Set<ClassType>> = new Map(); // 模块依赖关系
    
    public setController(key: ServiceKey, value: ClassType): void {
        this.controllers.set(key, value);
    }

    public registerModule(module: ClassType, imports: ClassType[] = []): void {

        const dependencies = new Set<ClassType>(imports);
        this.modules.set(module, dependencies);

        // 注册导入模块的提供者和控制器
        for (const importedModule of imports) {
        
            const importedProviders = Reflect.getMetadata(IOC_PROVIDER, importedModule) || [];
            
            for (const provider of importedProviders) {
                this.setService(provider, provider);
            }
            
        }
    }

    public getControllersForModule(module: ClassType): ClassType[] {

        const controllers: ClassType[] = [];
        const visited = new Set<ClassType>();

        const collectControllers = (mod: ClassType) => {
            if (visited.has(mod)) 
	            return;
            visited.add(mod);

            const moduleControllers = Reflect.getMetadata(IOC_CONTROLLER, mod) || [];
            controllers.push(...moduleControllers);
            const dependencies = this.modules.get(mod) || new Set();

            for (const dep of dependencies) {
                collectControllers(dep);
            }
        };
        collectControllers(module);
        return controllers;
        
    }
}

export interface ModuleType {

    new(...args: any[]): any;
    getContainer(): Container;
    [key: string]: any;
    
}

  

export function createDecorators(container: Container) {

    return {
        Injectable(): ClassDecorator {
            return (target) => {
                container.setService(target, target as unknown as ClassType);
            };
        },

        Controller(path: string): ClassDecorator {
            return (target) => {
                container.setController(target, target as unknown as ClassType);
                Reflect.defineMetadata(PATH_METADETA, path, target);
            };
        },

        Module(options: { imports?: ClassType[], providers?: ClassType[], controllers?: ClassType[] }): ClassDecorator {
            return (target) => {
                const { imports = [], providers = [], controllers = [] } = options;
                Reflect.defineMetadata(IOC_PROVIDER, providers, target);
                Reflect.defineMetadata(IOC_CONTROLLER, controllers, target);
                container.registerModule(target as unknown as ClassType, imports);
            };
        }
    };
}

新版本的变化如下

  • 把原类里的static 去掉
  • 增加 controllersmodules 属性
  • 更新 Injectable,Controller ,增加 Module ,都是用工厂模式绑定一个container 实例
  • 增加 getControllersForModule 用递归的方式去获取所有模块的路由,用于注册在express 上

src/init.ts 少许改变

export function moduleInit(app: Express, moduleClass: ClassType) {

    const router: any = Router()
    const module = new moduleClass();
    const container = module.getContainer();
    const controllers = container.getControllersForModule(moduleClass);
    controllers.forEach((ctrl: any) => {
		// ...省略
        const ctrlInstance = container.get(ctrl)
       // ...省略
    })
 }

用法是这样的 src/modules/app/app.module.ts

import { Module, containerInstance } from "./app.container";
import { UserModule } from "../users/user.module";

@Module({
    imports: [UserModule],
})

export class AppModule {
    constructor() { }
    getContainer() {
        return containerInstance
    }
}

src/modules/app/app.container.ts

import { Container, createDecorators } from "../../lib/ioc";
const container = new Container();
const { Injectable, Controller, Module } = createDecorators(container);

export {
    Injectable,
    Controller,
    Module,
    container as containerInstance
}

每个module 都会实例化一个container , 并且基于它提供 Injectable, ControllerModule 装饰器。 我们看到 AppModule 导入了 UserModule , 这个就通过 registerModuleUserModule 的Service 导进来了,并同时生成了依赖关系,getControllersForModule 可以顺藤摸瓜找到 User的路由,最终一起注册在 express router 上。

src/index.ts

let express = require('express');
import { moduleInit } from './init';
import { AppModule } from './modules/app/app.module';

let app = express()
async function main() {
    moduleInit(app, AppModule)
    const port = 3000
    var server = app.listen(port, function () {
        const message = "app listen on port:" + port
        console.log(message)
    })
}

main()

这样这个给链路就串联起来了。

总结

整个小项目的就是从最基础的开始做一点点往上加,经过这么一个小项目,控制反转、依赖注入的思想、装饰器与元数据的应用又深刻了一些,还是挺有意思的。也希望读者能够在这篇文章上找到自己想要的知识,有任何想讨论的,欢迎在评论去留言