原文地址:ts-decoratorhtml
控制反轉和依賴注入
是常見一種設計模式,在先後端均有很深的應用場景,不瞭解的小夥伴能夠先看下資料: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 }
喜歡的記得點 ❤️哦~