原文地址: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 }
喜歡的記得點 ❤️哦~