原文地址:observable-other-typehtml
控制反转和依赖注入
是常见一种设计模式,在先后端均有很深的应用场景,不了解的小伙伴能够先看下资料:wiki/设计模式_(计算机),wiki/控制反转node
若是以前有过 Angular 开发经历,那么确定用过 Injectable
和 Component
等常见的装饰器,其做用就是完成控制反转和依赖注入
git
对于 node 后端,也一样有不少以 IoC
和 DI
这套思想为主打的库,好比:NestJs,InversifyJs 等es6
今天主要聊聊这些依赖注入框架下的装饰器的使用原理与简单实现,还不了解装饰器的小伙伴看这里:ES6 入门:装饰器github
express 开发中,常常能看到这样的代码。为获取核心数据去写一些与业务逻辑无关的代码,数据一多的话,代码就会很冗杂typescript
const app = express(); app.use('/users', (req, res) => { const id = req.query.id; const uid = req.cookies.get('uid'); const auth = req.header['authorization']; // 业务逻辑... res.send(...); }); 复制代码
有了 nest 强力的装饰器,咱们能够这样写。把路由抽成一个类,从而更专一和具体的维护;路由参数使用装饰器捕获,直接拿到核心数据,这样代码可读性也变强了express
@Controller('/users') export class UserController { constructor(private userService: UserService) {} @Get('/') getUserById(@Query('id') id: string, @Headers('authorization') auth: string) { return this.userService.getUserBtyId(id, auth); } } 复制代码
Reflect Metadata 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据json
在 Angular 2+ 的版本中,控制反转与依赖注入即是基于此实现后端
NestJs 在创做时也是吸取了 Angular 的依赖注入思想设计模式
使用很简单,请参考:reflect metadata api
在项目中配置:
// 下载 yarn add reflect-metadata // tsconfig.json 中添加 "compilerOptions": { "types": ["reflect-metadata", "node"], "emitDecoratorMetadata": true, } // index.ts 根文件中引入 import 'reflect-metadata'; 复制代码
设计了两套实现方案,第一套是利用全局变量记录装饰对象完成,供学习使用;第二套是用 reflect metadata 实现
在 reflect metadata 还没推出以前,node 中的依赖注入是怎么作的呢
其实就是维护一个全局的 list,经过初始化 controller 时装饰器的调用进行依赖的收集,在客户端请求资源时截获并修改数据
这里只是简单的收集依赖,并不作什么处理
export const controllerList: ControllerType[] = []; export function Controller(path = ''): ClassDecorator { // target: controller 类,不是实例 return (target: object) => { controllerList.push({ path, target }); }; } 复制代码
根据 http 方法又封装了一层而已
export type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch'; export const routeList: RouteType[] = []; export function createMethodDecorator(method: HttpMethod = 'get') { return (path = '/'): MethodDecorator => // target:当前类实例,name:当前函数名,descriptor:当前属性(函数)的描述符 (target: object, name: string, descriptor: any) => { routeList.push({ type: method, target, name, path, func: descriptor.value }); }; } // 使用 export const Get = createMethodDecorator('get'); export const Post = createMethodDecorator('post'); 复制代码
根据参数封装了一层
export type Param = 'params' | 'query' | 'body' | 'headers' | 'cookies'; export const paramList: ParamType[] = []; export function createParamDecorator(type: Param) { return (key?: string): ParameterDecorator => // target:当前类实例,name:当前函数名,index:当前函数参数顺序 (target: object, name: string, index: number) => { paramList.push({ key, index, type, name }); }; } // 使用 export const Query = createParamDecorator('query'); export const Body = createParamDecorator('body'); export const Headers = createParamDecorator('headers'); 复制代码
这类装饰器属于优化,用法同 Query 等装饰器
export type Parse = 'number' | 'string' | 'boolean'; export const parseList: ParseType[] = []; export function Parse(type: Parse): ParameterDecorator { return (target: object, name: string, index: number) => { parseList.push({ type, index, name }); }; } 复制代码
controller 的遍历,配置全部根路由
route 的遍历,配置当前根路由下的子路由
param 和 parse 的遍历,配置当前路由函数中的各个参数
const router = express.Router(); // 初始化路由 controllerList.forEach(controller => { const { path: basePath, target: cTarget } = controller; routeList // 取出当前根路由下的 route .filter(({ target }) => target === cTarget.prototype) .forEach(route => { const { name: funcName, type, path, func } = route; // handler 即咱们常见的 (res, req, next) => {} const handler = handlerFactory( func, // 取当前路由函数下装饰的参数列表 paramList.filter(param => param.name === funcName), parseList.filter(parse => parse.name === funcName), ); // 配置 express router router[type](basePath + path, handler); }); }); // 将装载好的 router 放到 express 中 app.use('/', router); 复制代码
export function handlerFactory(func: (...args: any[]) => any, paramList: ParamType[], parseList: ParseType[]) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
// 获取路由函数的参数
const args = extractParameters(req, res, next, paramList, parseList);
const result = await func(...args);
res.send(result);
} catch (err) {
next(err);
}
};
}
复制代码
export function extractParameters( req: Request, res: Response, next: NextFunction, paramArr: ParamType[] = [], parseArr: ParseType[] = [], ) { if (!paramArr.length) return [req, res, next]; const args = []; // 进行第三层遍历 paramArr.forEach(param => { const { key, index, type } = param; // 获取相应的值,如 @Query('id') 则为 req.query.id switch (type) { case 'query': args[index] = key ? req.query[key] : req.query; break; case 'body': args[index] = key ? req.body[key] : req.body; break; case 'headers': args[index] = key ? req.headers[key.toLowerCase()] : req.headers; break; // ... } }); // 小优化,处理参数类型 parseArr.forEach(parse => { const { type, index } = parse; switch (type) { case 'number': args[index] = +args[index]; break; case 'string': args[index] = args[index] + ''; break; case 'boolean': args[index] = Boolean(args[index]); break; } }); args.push(req, res, next); return args; } 复制代码
接来下就是愉快的使用时间 😏
@Controller('/') // 装饰 controller export default class Index { @Get('/') // 装饰 route index(@Parse('number') @Query('id') id: number) { // 装饰参数 return { code: 200, id, message: 'success' }; } @Post('/login') login( @Headers('authorization') auth: string, @Body() body: { name: string; password: string }, @Body('name') name: string, @Body('password') psd: string, ) { console.log(body, auth); if (name !== 'lawler' || psd !== '111111') { return { code: 401, message: 'auth failed' }; } return { code: 200, token: 't:111111', message: 'success' }; } } 复制代码
除了代码书写的不一样,思想彻底同样
export const CONTROLLER_METADATA = 'controller'; export const ROUTE_METADATA = 'method'; export const PARAM_METADATA = 'param'; export function Controller(path = ''): ClassDecorator { return (target: object) => { Reflect.defineMetadata(CONTROLLER_METADATA, path, target); }; } export function createMethodDecorator(method: HttpMethod = 'get') { return (path = '/'): MethodDecorator => (target: object, name: string, descriptor: any) => { Reflect.defineMetadata(ROUTE_METADATA, { type: method, path }, descriptor.value); }; } export function createParamDecorator(type: Param) { return (key?: string): ParameterDecorator => (target: object, name: string, index: number) => { // 这里要注意这里 defineMetadata 挂在 target.name 上 // 但该函数的参数有顺序之分,下一个装饰器定义参数后覆盖以前的,因此要用 preMetadata 保存起来 const preMetadata = Reflect.getMetadata(PARAM_METADATA, target, name) || []; const newMetadata = [{ key, index, type }, ...preMetadata]; Reflect.defineMetadata(PARAM_METADATA, newMetadata, target, name); }; } 复制代码
const router = express.Router(); const controllerStore = { index: new IndexController(), user: new UserController(), }; Object.values(controllerStore).forEach(instance => { const controllerMetadata: string = Reflect.getMetadata(CONTROLLER_METADATA, instance.constructor); const proto = Object.getPrototypeOf(instance); // 拿到该实例的原型方法 const routeNameArr = Object.getOwnPropertyNames(proto).filter( n => n !== 'constructor' && typeof proto[n] === 'function', ); routeNameArr.forEach(routeName => { const routeMetadata: RouteType = Reflect.getMetadata(ROUTE_METADATA, proto[routeName]); const { type, path } = routeMetadata; const handler = handlerFactory( proto[routeName], Reflect.getMetadata(PARAM_METADATA, instance, routeName), Reflect.getMetadata(PARSE_METADATA, instance, routeName), ); router[type](controllerMetadata + path, handler); }); }); 复制代码
如何运行:yarn && yarn start
测试 url,建议使用 postman
body: { name: 'lawler', password: '111111' }
headers: { authorization: 't:111111' }
body: { name: 'lawler', password: '111111' }
headers: { authorization: 't:111111' }
body: { name: 'lawler', password: '111111', gender: 0 }
喜欢的记得点 ❤️哦~