如何使用类装饰器将装饰器应用于所有类方法

5

我正在使用实验性的TypeScript装饰器来管理Express中的访问控制。

class AccountController extends Controller {
  
  login(req: Request, res: Response) {
    const { email, password } = req.body;
    const token = await this.model.login(email, password);
    return res.json({
      token
    });
  }

  @hasRole('ADMIN')
  list(req: Request, res: Response) {
    res.json({
      data: await this.model.findAll()
    });
  }
}

hasRole方法装饰器运行良好,我很满意。

Controller实现REST方法:

class Controller {
  list(req: Request, res: Response) { // impl }
  get(req: Request, res: Response) { // impl } 
  create(req: Request, res: Response) { // impl }
  update(req: Request, res: Response) { // impl }
  delete(req: Request, res: Response) { // impl }
}

问题在于,我需要将同样的修饰器应用到大多数其他控制器中,这让我感到非常重复。例如,StockController 应该只允许 MERCHANT 角色访问,我必须像以下这样做:
class StockController extends Controller {
  @hasRole('MERCHANT')
  list(req: Request, res: Response) { 
    return super.list(req, res);
  }
  @hasRole('MERCHANT')
  get(req: Request, res: Response) { 
    return super.get(req, res);
  } 
  @hasRole('MERCHANT')
  create(req: Request, res: Response) { 
    return super.create(req, res);
  }
  @hasRole('MERCHANT')
  update(req: Request, res: Response) { 
    return super.update(req, res);
  }
  @hasRole('MERCHANT')
  delete(req: Request, res: Response) { 
    return super.delete(req, res);
  }
}

这种方法不仅枯燥重复而且不安全,因为如果我向Controller添加一个方法,但意外忘记在子控制器中添加该方法,则会允许不必要的访问。

我想通过类装饰器来解决这个问题,使用以下代码:

@requireRole('MERCHANT')
class StockController extends Controller {}

不过,从我在文档中所看到的内容:

类装饰器应用于类的构造函数,并可用于观察、修改或替换类定义。

据我所理解,在类装饰器中无法实现“方法钩子”。有什么建议吗?

供参考,hasRole装饰器如下:

export function hasRole(role: string) {
  return function(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function(req: Request, res: Response) {
      const user = res.locals.user;
      if (user && user.hasRole(role)) {
        originalMethod.apply(this, [req, res]);
      } else {
        res.status(403).json({});
      }
    }
  }
}


一个方法装饰器将方法包装成一个新函数,该函数调用原始方法并且在此之前或之后还可以执行其他操作。一个类装饰器则包装构造函数。编写一个能够满足需求的类装饰器是完全可行的。 - axiac
@axiac 感谢您的建议。请通过一个例子来指导我,我有点迷失了。。 - glinda93
1个回答

7

通过覆盖类方法,这是可能的。

function AttachToAllClassDecorator<T>(someParam: string) {
    return function(target: new (...params: any[]) => T) {
        for (const key of Object.getOwnPropertyNames(target.prototype)) {
            // maybe blacklist methods here
            let descriptor = Object.getOwnPropertyDescriptor(target.prototype, key);
            if (descriptor) {
                descriptor = someDecorator(someParam)(key, descriptor);
                Object.defineProperty(target.prototype, key, descriptor);
            }
        }
    }
}

基本上是遍历所有方法(可能需要添加一些逻辑来对某些方法进行白名单/黑名单处理),并使用方法装饰器包装新的方法覆盖旧的方法。

以下是方法装饰器的基本示例。

function someDecorator(someParam: string): (methodName: string, descriptor: PropertyDescriptor) => PropertyDescriptor {
    return (methodName: string, descriptor: PropertyDescriptor): PropertyDescriptor => {
        let method = descriptor.value;

        descriptor.value = function(...args: any[]) {
            console.warn(`Here for descriptor ${methodName} with param ${someParam}`);

            return method.apply(this, args);
        }

        return descriptor;
    }
}

TS Playground

解释:这段HTML代码包含一个p标签和一个a标签,a标签链接指向“TS Playground”网站。

只是为了明确起见,Object.defineProperty(target.prototype, key, descriptor);。这行代码会覆盖附加到方法的装饰器。正确吗? - glinda93
这将覆盖类中当前方法(没有装饰器的方法),使用附加了方法装饰器的方法。descriptor = someDecorator... 将装饰器附加到方法。 - noitse
谢谢你的好回答。顺便说一下,你的代码有一个错误:如果一个类方法接收到一个参数,return method.apply(this, ...args); 将会抛出 CreateListFromArrayLike called on non-object 错误。你需要移除 ...,即 method.apply(this, args) - glinda93
确实,我只是凭感觉写的,并没有进行充分测试 :) - noitse
同时更新游乐场网址 - noitse
1
请注意:此方法不适用于扩展方法。请参见此处的答案:https://dev59.com/jVYN5IYBdhLWcg3wT2xa#47621528 - glinda93

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接