前言
本文作者之前在学些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 去掉
- 增加
controllers
,modules
属性 - 更新
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
, Controller
,Module
装饰器。
我们看到 AppModule
导入了 UserModule
, 这个就通过 registerModule
将 UserModule
的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()
这样这个给链路就串联起来了。
总结
整个小项目的就是从最基础的开始做一点点往上加,经过这么一个小项目,控制反转、依赖注入的思想、装饰器与元数据的应用又深刻了一些,还是挺有意思的。也希望读者能够在这篇文章上找到自己想要的知识,有任何想讨论的,欢迎在评论去留言