先从一个简单的参数装饰器入手来了解下装饰器是怎么用的?
如何用参数装饰器进行参数验证?
参数装饰器是一个函数,它接收以下参数:
- 类的原型(如果是静态方法,则是类的构造函数)。
- 方法名称。
- 参数在函数参数列表中的索引。
js
function ParameterDecorator(target: any, methodName: string, parameterIndex: number) {
console.log("Parameter Decorator called");
console.log("Target:", target);
console.log("Method Name:", methodName);
console.log("Parameter Index:", parameterIndex);
}
class MyClass {
myMethod(@ParameterDecorator arg1: string, arg2: number) {
console.log(`Executing myMethod with args: ${arg1}, ${arg2}`);
}
}
const instance = new MyClass();
instance.myMethod("Hello", 42);
那它有什么用呢?可以用于实现参数的验证,你可以在方法执行前对参数进行验证。
以下是一个完整的示例,展示如何使用参数装饰器实现参数验证:
js
import 'reflect-metadata';
# 定义验证规则接口
interface ValidationRule {
validate(value: any): boolean;
message: string;
}
# 定义具体的验证规则
class IsNumber implements ValidationRule {
validate(value: any): boolean {
return typeof value === 'number';
}
message = '参数必须是数字';
}
class IsString implements ValidationRule {
validate(value: any): boolean {
return typeof value === 'string';
}
message = '参数必须是字符串';
}
# 参数装饰器工厂函数
function Validate(rule: ValidationRule) {
return function (target: any, methodName: string, parameterIndex: number) {
// 获取已有的验证规则
const rules = Reflect.getMetadata('validationRules', target, methodName) || [];
// 添加新的验证规则
rules.push({ index: parameterIndex, rule });
// 存储验证规则
Reflect.defineMetadata('validationRules', rules, target, methodName);
};
}
# 方法装饰器,用于拦截方法调用并验证参数
function ValidateParameters(target: any, methodName: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
// 获取验证规则
const rules = Reflect.getMetadata('validationRules', target, methodName) || [];
// 验证每个参数
for (const { index, rule } of rules) {
if (!rule.validate(args[index])) {
throw new Error(`参数 ${index} 验证失败: ${rule.message}`);
}
}
// 如果验证通过,调用原始方法
return originalMethod.apply(this, args);
};
}
// 示例类
class Example {
@ValidateParameters
greet(@Validate(new IsString()) name: string, @Validate(new IsNumber()) age: number) {
console.log(`Hello, ${name}! You are ${age} years old.`);
}
}
// 测试
const example = new Example();
example.greet('Alice', 30); // 正常执行
example.greet('Bob', '30'); // 抛出错误:参数 1 验证失败: 参数必须是数字
example.greet(123, 30); // 抛出错误:参数 0 验证失败: 参数必须是字符串
验证规则:
定义了 ValidationRule 接口和具体的验证规则类(如 IsNumber 和 IsString)。每个规则类实现了 validate 方法和 message 属性。
参数装饰器:
Validate 是一个参数装饰器工厂函数,用于将验证规则与参数关联。使用 Reflect.defineMetadata 将验证规则存储到元数据中。
方法装饰器:
ValidateParameters 是一个方法装饰器,用于拦截方法调用。在方法执行前,读取元数据中的验证规则,并验证参数。如果验证失败,抛出错误;否则,调用原始方法。
如何用参数装饰器进行依赖注入?
js
import "reflect-metadata";
# 定义参数装饰器
function Inject(token: string) {
return function (target: any, methodName: string, parameterIndex: number) {
Reflect.defineMetadata("inject", token, target, `${methodName}_${parameterIndex}`);
};
}
# 定义服务类
class MyService {
doSomething() {
console.log("MyService is doing something");
}
}
# 定义需要注入的类
class MyClass {
# 使用装饰器Inject注入MyService
constructor(@Inject("MyService") private myService: MyService) {}
doSomething() {
this.myService.doSomething();
}
}
# 定义依赖注入容器
class Container {
private instances = new Map<string, any>();
# 注册
register(token: string, instance: any) {
this.instances.set(token, instance);
}
resolve<T>(token: string): T {
const instance = this.instances.get(token);
if (!instance) {
throw new Error(`No instance found for token: ${token}`);
}
return instance;
}
# 依赖注入
inject<T>(target: any): T {
const instance = new target();
# 在 TypeScript 里,`design:paramtypes` 是一种由 TypeScript 编译器自动生成的元数据,
# 借助反射机制,它能让我们在运行时获取构造函数参数的类型。
# 这里会返回构造函数参数类型数组,即[MyService]
# [MyService]
const paramTypes = Reflect.getMetadata("design:paramtypes", target) || [];
for (let i = 0; i < paramTypes.length; i++) {
# 获取 Reflect.defineMetadata注入的元数据
const token = Reflect.getMetadata("inject", target.prototype, `constructor_${i}`);
if (token) {
# 获取MyService实例
const dependency = this.resolve(token);
# 把MyService实例赋值给MyClass的属性myService上,这样就完成了依赖注入
instance[i] = dependency;
}
}
return instance;
}
}
# 使用依赖注入
const container = new Container();
container.register("MyService", new MyService());
# 通过container.inject实例化MyClass
const myClassInstance = container.inject(MyClass);
myClassInstance.doSomething(); // 输出: MyService is doing something
首先要知道,所有的类的实例化都是在容器中进行的(控制反转)。至于怎么实现的控制反转,这就是通过装饰器和元数据实现的。
【代码解释】
-
在需要注入依赖的地方,通过参数装饰器注入元数据,这个元数据就是类的一个标志key(MyService),
constructor(@Inject("MyService") private myService: MyService) {}
-
通过容器注册实例,
container.register("MyService", new MyService());
-
在实例化
MyClass
时,就不是通过new
的方式了,而是通过容器container.inject(MyClass)
,在这个方法中取出元数据,然后获取到元数据所对应类的实例,然后把实例赋值给MyClass。
利用装饰器改写express成nestjs的样子
我们先看看改写之后的样子:
js
# 中间件:验证用户是否登录
const checkLogin = (req: Request, res: Response, next: NextFunction): void => {
const isLogin = !!req.session?.isLogin
if (isLogin) {
next()
} else {
res.json(getResponseResult(null, 'please login'))
}
}
# 定义父路由路径
@controller('/api')
export class CrowllerController {
# 定义方法路由
@get('/getData')
# 注册中间件
@use(checkLogin)
getData(req: Request, res: Response): void {
...
}
@get('/showData')
@use(checkLogin)
showData(req: Response, res: Response): void {
...
}
}
这就很像nestjs
的代码了。那它是怎么实现的呢?
首先看看装饰器get post
实现逻辑:
js
# get post 装饰器
enum Methods {
get = 'get',
post = 'post'
}
function getRequestDecorator(type: Methods) {
return function (path: string) {
// target就是类的原型对象
return function (target: LoginController, key: string) {
Reflect.defineMetadata('path', path, target, key)
Reflect.defineMetadata('method', type, target, key)
}
}
}
export const get = getRequestDecorator(Methods.get)
export const post = getRequestDecorator(Methods.post)
这段代码很简单,就是定义了两个get、post两个装饰器,在装饰器里面通过元数据Reflect.defineMetadata在LoginController的方法 login 和 logout 上添加了 path 和 method 两个元数据,例如,login方法上的元数据为:
js
{
path: '/login',
method: 'post'
}
那什么时候获取到元数据呢?那就是类的装饰器controller
。
这里需要知道,方法的装饰器是先于类的装饰器之前执行,所以,能在类的装饰器上获取到在方法的装饰器上定义的元数据。
js
# 定义类的装饰器controller
export function controller(root: string) {
# target就是类的构造函数,通过target.prototype获取类的原型
return function (target: new (...args: any[]) => any) {
for (let key in target.prototype) {
# 获取路由
const path: string = Reflect.getMetadata('path', target.prototype, key)
# 获取请求方法
const method: Methods=Reflect.getMetadata('method',target.prototype,key)
# 获取对应的处理函数
const handle = target.prototype[key]
# 获取中间件
const middleware: RequestHandler = Reflect.getMetadata(
'middleware',target.prototype,key)
# 拼接路由
if (path && method) {
let fullpath = ''
if (root === '/') {
if (path === '/') {
fullpath = '/'
} else {
fullpath = path
}
} else {
fullpath = `${root}${path}`
}
# 绑定router
if (middleware) {
# 这里才是执行express逻辑的地方
router[method](fullpath, middleware, handle)
} else {
router[method](fullpath, handle)
}
}
}
}
}
上面还缺一个中间件的装饰器:
js
# 定义中间件装饰器
export function use(middleware: RequestHandler) {
return function (target: any, key: string) {
Reflect.defineMetadata('middleware', middleware, target, key)
}
}
# 获取中间件
const middleware: RequestHandler = Reflect.getMetadata('middleware', target.prototype, key)
理解了上面的代码,对于理解nestjs
的代码是比较容易的。
因为,nestjs
的实现原理就是通过装饰器给 class 或者对象添加元数据,然后初始化的时候取出这些元数据,进行依赖的分析,然后创建对应的实例对象就可以了。