[NestJS 带你飞!] DAY14 - Custom Decorator

装饰器 (Decorator) 是一种设计模式,有些程序语言会直接将此设计模式实作出来,TypeScript 与 JavaScript 在近年也添加了此功能,而 Nest 将装饰器发挥到淋漓尽致,透过装饰器就可以很轻易地套用功能,不论是针对开发速度、易读性等都很有帮助。
https://ithelp.ithome.com.tw/upload/images/20210428/20119338usaYZmi3VS.png

Custom Decorator

Nest 有提供许多装饰器,但在某些情况下内建的装饰器可能没办法很有效地解决问题,於是 Nest 提供了 自订装饰器 (Custom Decorator) 的功能,其分成下方三种:

参数装饰器

有些资料可能无法透过内建装饰器直接取得,比如:授权认证机制所带入的资料。如果对 Express 不陌生的话应该看过下方的写法,为什麽会有自订义的资料放在请求物件中呢?主要是透过 Middleware 进行扩充,在授权认证机制是非常常见的:

const user = req.user;

试想,如果要透过内建装饰器要如何取得该资料?必需要使用 @Request 装饰器先取得请求物件,再从请求物件中提取,这样的方式并不是特别理想,於是可以自行设计参数装饰器来取得,而 Decorator 可以透过 CLI 产生:

$ nest generate decorator <DECORATOR_NAME>

注意<DECORATOR_NAME> 可以含有路径,如:decorators/user,这样就会在 src 资料夹下建立该路径并含有 Decorator。

这边我建立一个 Userdecorators 资料夹下:

$ nest generate decorator decorators/user

src 底下会看见一个名为 decorators 的资料夹,里面有 user.decorator.ts
https://ithelp.ithome.com.tw/upload/images/20210428/20119338fvDNGSKp8A.png

建立出来的装饰器骨架如下,会发现是一个回传 SetMetadata 的函式:

import { SetMetadata } from '@nestjs/common';

export const User = (...args: string[]) => SetMetadata('user', args);

不过参数装饰器并不是使用 SetMetadata,而是使用 createParamDecorator,透过 createParamDecorator 来产生参数装饰器,并使用 Callback 里面的 ExecutionContext 来取得请求物件再从中取得要取出的资料。下方为修改後的 user.decorator.ts

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);

在设计完 User 装饰器後,要设计一个 Middleware 来添加 user 到请求物件中。透过 CLI 产生 AddUserMiddleware

$ nest generate middleware middlewares/add-user

add-user.middleware.ts 的内容修改如下:

import { Injectable, NestMiddleware } from '@nestjs/common';

@Injectable()
export class AddUserMiddleware implements NestMiddleware {
  use(req: any, res: any, next: () => void) {
    req.user = { name: 'HAO' };
    next();
  }
}

接着,在 AppModule 中套用 AddUserMiddleware,修改 app.module.ts 如下:

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AddUserMiddleware } from './middlewares/add-user.middleware';
@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    AppService
  ]
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(AddUserMiddleware).forRoutes('');
  }
}

这样就能够将 user 添加到请求物件里了,接下来就要使用 User 装饰器来将 user 内容取出并返回给客户端,这里我们修改 app.controller.ts

import { Controller, Get } from '@nestjs/common';
import { User } from './decorators/user.decorator';

@Controller()
export class AppController {
  constructor() {}

  @Get()
  getHello(@User() user: any): string {
    return user;
  }
}

透过浏览器查看 http://localhost:3000 会看到下方结果,表示有成功将 user 取出:
https://ithelp.ithome.com.tw/upload/images/20210501/20119338PSDSnlWpfs.png

那如果想要像 @Param('id') 一样只取出特定资料的话该如何设计呢?createParamDecorator 中的 Callback 里面除了 ExecutionContext 之外,还有一个 data,这个 data 事实上就是带到装饰器中的参数,所以要运用 data 来取出 user 中的资料。这里修改一下 user.decorator.ts

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data: string, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const user = request.user;
    return data ? user[data] : user;
  },
);

修改 app.controller.ts 来指定取出 user 中的 name

import { Controller, Get } from '@nestjs/common';
import { User } from './decorators/user.decorator';

@Controller()
export class AppController {
  constructor() {}

  @Get()
  getHello(@User('name') name: string): string {
    return name;
  }
}

透过浏览器查看 http://localhost:3000 会得到下方结果:
https://ithelp.ithome.com.tw/upload/images/20210501/20119338lCSX5HbfpZ.png

自订 Metadata 装饰器

有时候需要针对某个方法设置特定的 Metadata,比如:角色权限控管,透过设置 Metadata 来表示该方法仅能由特定角色来存取。这里来实作一个简单的角色权限控管功能,透过 CLI 产生 Roles

$ nest generate decorator decoractors/roles

建立出来的骨架即为自订 Metadata 装饰器的格式,SetMetadata 即产生自订 Metadata 的装饰器:

import { SetMetadata } from '@nestjs/common';

export const Roles = (...args: string[]) => SetMetadata('roles', args);

这个范例的意思为:Roles 即为装饰器,透过 @Roles('admin')admin 字串带入装饰器中,SetMetadata 指定 roleskey 值,并令 ['admin'] 为其值,最後设置为 Metadata。

接着来设置一个 RoleGuard 模拟角色权限管理的效果:

$ nest generate guards/role

为了要取得 Metadata 的内容,必须透过 Nest 提供的 Reflector 来取得,其引入方式即透过依赖注入,并呼叫 get(metadataKey: any, target: Function | Type<any>) 来取得指定的 Metadata,其中 metadataKey 即要指定的 Metadata Key,而 target 则为装饰器装饰之目标,经常会使用 ExecutionContext 里面的 getHandler 来作为 target 的值。将 role.guard.ts 的内容修改如下:

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';

@Injectable()
export class RoleGuard implements CanActivate {

  constructor(private readonly reflector: Reflector) {}

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return this.matchRoles(roles, user.roles);
  }

  private matchRoles(resources: string[], target: string[]): boolean {
    return !!resources.find(x => target.find(y => y === x));
  }
}

在设置好 RolesRoleGuard 之後,就来调整一下 AddUserMiddleware 的内容,添加角色 staffuser 里面:

import { Injectable, NestMiddleware } from '@nestjs/common';

@Injectable()
export class AddUserMiddleware implements NestMiddleware {
  use(req: any, res: any, next: () => void) {
    req.user = { name: 'HAO', roles: ['staff'] };
    next();
  }
}

最後,调整一下 app.controller.ts 的内容,指定 getHello 只有 admin 身份可以存取:

import { Controller, Get, UseGuards } from '@nestjs/common';
import { User } from './decorators/user.decorator';
import { Roles } from './decorators/roles.decorator';
import { RoleGuard } from './guards/role.guard';

@Controller()
export class AppController {
  constructor() {}

  @UseGuards(RoleGuard)
  @Roles('admin')
  @Get()
  getHello(@User('name') name: string): string {
    return name;
  }
}

透过浏览器查看 http://localhost:3000 会发现无法存取:
https://ithelp.ithome.com.tw/upload/images/20210502/201193388eQA15a0cu.png

整合装饰器

有些装饰器它们之间是有相关的,比如:授权验证需要使用 Guard、添加自订 Metadata 等,每次要实作都要重复将这些装饰器带入,会使得重复性的操作变多,於是 Nest 还有设计一个叫 applyDecorators 的函式来将多个装饰器整合成一个装饰器,每当要实作该功能就只要带入整合装饰器即可。下方会简单模拟授权验证的整合装饰器,先透过 CLI 产生 Auth Decorator:

$ nest generate decorators/auth

接着,Auth 需包含 UseGuardsRoles 这两个装饰器的功能,在设计整合装饰器之前需要先透过 CLI 产生 AuthGuard 以便後续使用:

$ nest generate guard guards/auth

注意:本节主要是将焦点放在整合功能上,所以这里就不特别去改 AuthGuard 的内容了,让它们回传 true 即可。

产生完 AuthGuard 之後,来修改一下 auth.decorator.ts 的内容,透过 applyDecoratorsUseGuardsRoles 整合成一个装饰器:

import { applyDecorators, UseGuards } from '@nestjs/common';
import { RoleGuard } from '../guards/role.guard';
import { AuthGuard } from '../guards/auth.guard';
import { Roles } from './roles.decorator';

export const Auth = (...roles: string[]) => applyDecorators(
    Roles(...roles),
    UseGuards(AuthGuard, RoleGuard)
);

最後来调整一下 app.controller.ts,套用 Auth 装饰器并指定 getHello 只有 staff 可以存取:

import { Controller, Get } from '@nestjs/common';
import { Auth } from './decorators/auth.decorator';
import { User } from './decorators/user.decorator';

@Controller()
export class AppController {
  constructor() {}

  @Auth('staff')
  @Get()
  getHello(@User('name') name: string): string {
    return name;
  }
}

透过浏览器查看 http://localhost:3000 会得到下方结果:
https://ithelp.ithome.com.tw/upload/images/20210502/20119338N46rj0EuVN.png

小结

Custom Decorator 可以补足 Nest 内建装饰器不足的部分,且具有相当大的弹性,是非常实用的功能。这里附上今天的懒人包:

  1. Custom Decorator 可以实作:参数装饰器、自订 Metadata 装饰器、整合装饰器。
  2. 参数装饰器是使用 createParamDecorator 来产生。
  3. 自订 Metadata 装饰器可以说是 SetMetadata 的扩展。
  4. 整合装饰器是使用 applyDecorators 来产生。

Nest 各元件的基本功能与使用方式皆介绍完毕,下一篇开始将会进入到 进阶功能 单元,敬请期待!


<<:  Day 17 正面的鼓励与掌声...

>>:  CSS微动画 - 图片不裁切,纯css实现分格淡出

DAY3 MongoDB 连线与 IDE

DAY3 MongoDB 连线与 IDE MongoDB 的连线方式主要有三种,分别是: legac...

EXCEL VBA SQL 将资料 汇出 到dBASEIII .dbf档案

EXCEL VBA SQL 将资料 汇出 到dBASEIII .dbf档案 PS. : Proper...

演算法 Fizz Buzz

##让我们来学习演算法吧,此为阅读[https://pjchender.blogspot.com/2...

我与程序的距离-Day2

不难发现,问题在於该用什麽标准来做决定呢?梁晓声曾讲过,友谊,好比一瓶酒,封存的时间越长,价值则越高...

Day25【Web】TCP 连线与断线:三次握手、四次挥手

TCP 是一种要求资料正确性的传输方式, 这表示它需要一些特殊机制, 来确保传输的数据不会出错。 其...