NestJS 用户注入详细介绍

Nest.JS 是最流行的 Node.js 框架。 它利用 TypeScript 的全部功能使开发过程快速而简单。 在本文中,我将分享非常酷且有用的技巧“用户注入(User Injection)”,它可以大大简化我们的 Nest.JS 项目的代码。


为什么我们需要这个?

这是任何开发人员在开始新事物之前应该问的第一个问题。 让我们想象一下,我们有一个 API 服务器,并且我们需要限制对某些数据的访问,这是一个非常常见的场景。 例如,让我们为一个有订单和客户的在线商店创建一个简单的 API。 客户必须只能访问他们的订单,而不能访问其他客户的订单。 OrdersController 将如下所示:

import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Post,
  UseGuards,
} from '@nestjs/common';
import { OrdersService } from './orders.service';
import { ICustomer } from '../customers/customer.model';
import { Customer } from '../common/customer.decorator';
import { IOrder } from './order.model';
import { AuthGuard } from '../common/auth.guard';

@Controller('orders')
@UseGuards(AuthGuard)
export class OrdersController {
  constructor(private readonly ordersService: OrdersService) {}

  @Get('/')
  getOrders(@Customer() customer: ICustomer): Promise<IOrder[]> {
    return this.ordersService.getOrders(customer);
  }

  @Get('/:id')
  getOrder(
    @Param('id') id: string,
    @Customer() customer: ICustomer,
  ): Promise<IOrder> {
    return this.ordersService.getOrder(id, customer);
  }

  @Post('/')
  create(
    @Body() data: Partial<IOrder>,
    @Customer() customer: ICustomer,
  ): Promise<IOrder> {
    return this.ordersService.create(data, customer);
  }

  @Delete('/:id')
  cancel(
    @Param('id') id: string,
    @Customer() customer: ICustomer,
  ): Promise<void> {
    return this.ordersService.cancel(id, customer);
  }
}
view raw

这个控制器用 AuthGuard 封装,@Customer 用于检索当前用户。 这是 OrdersService 的代码:

import {
  ForbiddenException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { ICustomer } from 'src/customers/customer.model';
import { IOrder } from './order.model';
import { OrdersRepository } from './orders.repository';

@Injectable()
export class OrdersService {
  constructor(private readonly ordersRepository: OrdersRepository) {}

  async create(data: Partial<IOrder>, customer: ICustomer): Promise<IOrder> {
    const order = { ...data, customerId: customer.id };
    return this.ordersRepository.create(order);
  }

  async cancel(orderId: string, customer: ICustomer): Promise<void> {
    const order = await this.ordersRepository.findOne(orderId);
    if (!order) {
      throw new NotFoundException();
    }

    if (order.customerId !== customer.id) {
      throw new ForbiddenException();
    }

    await this.ordersRepository.deleteOne(orderId);
  }

  async getOrders(customer: ICustomer): Promise<IOrder[]> {
    return this.ordersRepository.find({ customerId: customer.id });
  }

  async getOrder(orderId: string, customer: ICustomer): Promise<IOrder> {
    return this.ordersRepository.findOne({
      id: orderId,
      customerId: customer.id,
    });
  }
}

如大家所见,customer 被传递给 OrdersService 的每个方法。 这只是一项服务。 如果我们有数百个服务,每个服务至少有 10 种方法怎么办? 使用 customers 数据传递额外的参数变得非常烦人。

解决方法

幸运的是,Nest.JS 提供了非常灵活的依赖注入机制,Nest.JS 从第 6 版开始支持请求注入范围 。 这意味着我们可以将当前请求注入到我们的服务中。 如果我们可以注入请求,我们就可以注入存储在请求中的用户数据。 让我们写一个 customer 提供者:

import { FactoryProvider, Scope } from '@nestjs/common'
import { REQUEST } from '@nestjs/core'

export const customerProvider: FactoryProvider = {
  provide: 'CUSTOMER',
  scope: Scope.REQUEST,
  inject: [REQUEST],
  useFactory: (req) => req.user,
}

现在尝试用它向 OrdersService 注入 customer :

@Injectable()
export class OrdersService {
  constructor(
    @Inject('CUSTOMER')
    private readonly customer,
    private readonly ordersRepository: OrdersRepository,
  ) {
    console.log('Current customer:', this.customer);
  }
}

现在 Nest.JS 在每个请求上创建一个新的 OrdersService 实例,让我们发出一个请求,看看注入了什么:

Current customer: undefined

它应该可以工作,但实际上,undefined 已注入 OrdersService 。 这种意外行为的原因是 Nest.JS 依赖注入机制。 它会在收到新请求后立即创建所有提供者,此时 req 对象不包含有关提出此请求的客户的信息。 它需要一些时间来验证授权令牌/cookie 并从数据库接收用户数据。

解决方法 #2

我花了一些时间想出一个优雅的解决方案。 如果在注入的那一刻不可用,我们如何注入一些东西? 但是解决方案很明显,如果用户的数据还不可用,让我们将其包装为 getter:

import { Inject, Injectable, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { ICustomer } from './customer.model';

@Injectable({ scope: Scope.REQUEST })
export class CustomerProvider {
  get customer(): ICustomer {
    return this.req.user;
  }

  constructor(@Inject(REQUEST) private readonly req) {}
}

现在我们可以在 OrdersService 中使用它:

import {
  ForbiddenException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { ICustomer } from 'src/customers/customer.model';
import { IOrder } from './order.model';
import { OrdersRepository } from './orders.repository';
import { CustomerProvider } from '../customers/customer.provider';

@Injectable()
export class OrdersService {
  private get customer(): ICustomer {
    return this.customerProvider.customer;
  }

  constructor(
    private readonly customerProvider: CustomerProvider,
    private readonly ordersRepository: OrdersRepository,
  ) {}

  async create(data: Partial<IOrder>): Promise<IOrder> {
    const order = { ...data, customerId: this.customer.id };
    return this.ordersRepository.create(order);
  }

  async cancel(orderId: string): Promise<void> {
    const order = await this.ordersRepository.findOne(orderId);
    if (!order) {
      throw new NotFoundException();
    }

    if (order.customerId !== this.customer.id) {
      throw new ForbiddenException();
    }

    await this.ordersRepository.deleteOne(orderId);
  }

  async getOrders(): Promise<IOrder[]> {
    return this.ordersRepository.find({ customerId: this.customer.id });
  }

  async getOrder(orderId: string): Promise<IOrder> {
    return this.ordersRepository.findOne({
      id: orderId,
      customerId: this.customer.id,
    });
  }
}

在 OrdersController 中:

import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Post,
  UseGuards,
} from '@nestjs/common';
import { OrdersService } from './orders.service';
import { ICustomer } from '../customers/customer.model';
import { Customer } from '../common/customer.decorator';
import { IOrder } from './order.model';
import { AuthGuard } from '../common/auth.guard';

@Controller('orders')
@UseGuards(AuthGuard)
export class OrdersController {
  constructor(private readonly ordersService: OrdersService) {}

  @Get('/')
  getOrders(): Promise<IOrder[]> {
    return this.ordersService.getOrders();
  }

  @Get('/:id')
  getOrder(@Param('id') id: string): Promise<IOrder> {
    return this.ordersService.getOrder(id);
  }

  @Post('/')
  create(@Body() data: Partial<IOrder>): Promise<IOrder> {
    return this.ordersService.create(data);
  }

  @Delete('/:id')
  cancel(@Param('id') id: string): Promise<void> {
    return this.ordersService.cancel(id);
  }
}

如大家所见,代码变得更加简洁。 不再需要将 customer 传递给每个方法,它在 OrdersService 中可用。

你可能会问,当某个方法被调用时,我们如何确定 customer 已经被初始化了? 这是一个公平的问题。 如果在控制器类中添加@UseGuards(AuthGuard),Nest.JS 会先执行 guard 的canActivate 方法。 此方法检查当前授权数据并将其添加到请求对象中。 换句话说,如果使用授权保护,我们可以确定客户数据在运行 OrdersService 中的某些方法之前已经被初始化。


下一步做什么?

如果我们可以注入一个用户,我们可以以同样的方式注入它的权限。 它可以很容易地用 CASL 实现。 我们甚至可以更进一步,将用户注入存储库,并根据用户在数据库级别的权限过滤数据。 这种技术的潜在用例仅受我们的想象力限制。


缺点

任何方法都有优缺点,“用户注入”也不例外。

  • 性能 : 这种方法基于请求范围注入,这意味着将在每个传入请求上创建所有必需服务的新实例。 使用默认注入范围时,所有服务都会在应用程序启动时初始化。 但是,实例创建仍然是一个相当便宜的操作。 我想添加一些真实的性能比较,但用例多种多样。 性能测试结果只会显示我的项目的差异,对于我们的项目,它们可能会显示完全不同的画面。
  • 测试 : 为请求范围的服务编写测试比具有默认注入范围的服务要难一些。 但另一方面,我们可以在一个地方模拟用户数据,而不是将模拟传递给每个服务的方法。

总结

Nest.JS 是一个灵活且功能强大的框架。 其依赖注入机制的潜力几乎是无限的。 用户注入是许多技巧之一,可以与 Nest.JS 一起使用。 我并不是说这是组织代码的唯一正确方法。 但我希望这种方法对你有用,并能帮助大家重新思考自己的代码结构。