Nestjs🐺⚡ | Nodejs 框架(下)| 模块、循环依赖、Guard

2025-06-07

Nestjs🐺⚡ | Nodejs 框架(下)| 模块、循环依赖、Guard

如果你还没有读过第一部分,请先读一下,否则你会觉得给出的信息不合上下文

在第二部分中,我将讨论 Nestjs 模块、循环依赖和 Guards

1. 模块

在第一部分中,对模块进行了简单的描述。Nestjs 中的模块并非全局的,而是有深度的,但也可以跨其他模块共享。虽然它像 Angular 一样支持全局模块,但更建议将服务/控制器保留在 Nestjs 中最常用的模块中。

大多数情况下,模块将通过 NestCLI 生成,并且在该模块上下文中生成的提供程序/控制器将由 CLI 自动添加。这些被称为功能模块

这是一个模块示例:

////// hi.module.ts //////
import {Module} from "@nestjs/common"
import HiService from "./hi.service";
import HiController from "hi.controller";

@Module({
  providers: [HiService],
  controllers: [HiController],
  exports: [HiService]
})
export class HiModule{
}

////// hello.module.ts//////
import {Module} from "@nestjs/common"
import HelloService from "./hello.service";
import HelloController from "hello.controller";
import HiModule from "../hi/hi.module"

@Module({
    imports: [HiModule],
  providers: [HelloService],
  controllers: [HelloController],
  exports: [HelloService]
})
export class HelloModule{
}
Enter fullscreen mode Exit fullscreen mode

装饰@Module器的controllers数组属性用于模块使用的所有控制器@Controller或所有用装饰器装饰的类。该providers属性用于service或用装饰器装饰的类@Injectable。请记住,任何可注入的东西都是提供程序,您必须将其放入providers字段中才能注入/使用它。

exports属性用于导出/暴露可与其他模块共享的提供程序。将您想要注入/在其他模块中使用的任何提供程序放入

属性imports与 完全相反exports。为了能够在另一个模块的提供程序/控制器中使用/注入任何外部提供程序,您必须在imports另一个模块的字段中添加该导出提供程序的模块。

2. 循环依赖

很多时候,你会想在另一个模块的提供程序中使用一个提供程序,并在该提供程序/控制器中使用另一个模块的提供程序。在这种情况下,就会产生循环依赖。在 Nest 中,模块之间以及提供程序之间都可能出现循环依赖。在 Nestjs 中,我们应该尽力避免循环依赖,但有时这并非易事。在这种情况下,forwardRef&@Inject参数装饰器对于位于同一模块上下文中的提供程序非常有用。

forwardRef使用来自同一模块的跨提供程序解决循环依赖的示例:

///// bye.service.ts /////
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { HelloService } from './hello.service';

@Injectable()
export class ByeService {
  constructor(
        // injecting HelloService
    @Inject(forwardRef(() => HelloService))
    private helloService: HelloService,
  ) {}

  getBye(arg: string) {
    return `bye bye, ${arg}`;
  }

    // it uses `helloService` & is within same module
  helloServiceUsingMethod() {
    return this.helloService.getHello('bye');
  }
}

///// hello.service.ts /////
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { ByeService } from './bye.service';

@Injectable()
export class HelloService {
    // ...other stuff

  constructor(
        // injecting ByeService
    @Inject(forwardRef(() => ByeService))
    private byeService: ByeService,
  ) {}

  getHello(arg: string) {
    return `hello for ${arg}`;
  }

  byeServiceUsingMethod() {
    return this.byeService.getBye('hello');
  }

    // ....other stuff
}
Enter fullscreen mode Exit fullscreen mode

让我们在/hello模块或字段ByeService中添加新创建的HelloModuleproviders

////// hello.module.ts //////
// import stuff
import {ByeService} from "./bye.service"

@Module({
  providers: [HelloService, ByeService], // new bye-service added
  controllers: [HelloController],
  exports: [HelloService]
})
export class HelloModule{
}
Enter fullscreen mode Exit fullscreen mode

那么,来自外部模块的提供程序怎么办呢?不用担心,只需像上面一样对提供程序进行操作,并在两个模块的字段forwardRef中使用imports,即可在各自的上下文中导入彼此的提供程序。

跨模块转发外部提供商的引用的示例:

////// hi.module.ts //////
import { forwardRef, Module } from '@nestjs/common';
import HiService from "./hi.service";
import HiController from "hi.controller";
import HelloModule from "../hello/hello.module";

@Module({
  imports: [forwardRef(() => HelloModule)], // importing HelloMoule using forwardRef
  providers: [HiService],
  controllers: [HiController],
  exports: [HiService] // exporting hi-service for using in hello-service
})
export class HiModule{
}

////// hello.module.ts//////
import {Module, forwardRef} from "@nestjs/common"
import HelloService from "./hello.service";
import HelloController from "hello.controller";
import HiModule from "../hi/hi.module";
import ByeService from "./bye.service";

@Module({
    imports: [forwardRef(() => HiModule)],
  providers: [HelloService, ByeService],
  controllers: [HelloController],
  exports: [HelloService] // exporting hello-service for using in hi-service
})
export class HelloModule{
}
Enter fullscreen mode Exit fullscreen mode

现在两个模块的提供程序都可以在彼此的范围内使用,让我们forwardRef在它们的提供程序中使用HelloService&HiService来解决它们的循环依赖:

///// hello.service.ts //////
import {Injectable, Inject, forwardRef} from "@nestjs/common"
import HiService from "../hi/hi.service"

@Injectable()
export class HelloService{
  // .... other properties/methods

    constructor(
        // just like provider-scoped circular dependency
        @Inject(forwardRef(()=>HiService))
        private hiService: HiService
     ){
    }

    getHello(arg: string){
        return `hello for ${arg}`
    }

    // a method that uses `hiService`
  hiServiceUsingMethod(){
        return this.hiService.getHi("hello");
  }
  // .... other properties/methods
}

///// hi.service.ts /////
import {Injectable, Inject, forwardRef} from "@nestjs/common"
import HelloService from "../hello/hello.service"

@Injectable()
export class HelloService{
  // .... other properties/methods

    constructor(
        @Inject(forwardRef(()=>HelloService)) private helloService: HelloService
     ){
    }

    getHi(arg: string){
        return `hi for ${arg}`
    }

    // a method that uses `helloService`
  helloServiceUsingMethod(){
        return this.helloService.getHello("hi");
  }
  // .... other properties/methods
}
Enter fullscreen mode Exit fullscreen mode

确保您的代码不依赖于首先调用哪个构造函数,因为这些提供程序类的实例化顺序是不确定的

有一个高级替代方案forwardRef。该类ModuleRef由提供,@nestjs/core用于动态实例化静态和作用域提供程序。它主要用于导航内部提供程序列表,并使用其注入令牌作为查找键来获取对任何提供程序的引用。ModuleRef可以以常规方式注入到类中。了解更多关于[ModuleRef](https://docs.nestjs.com/fundamentals/module-ref)

3. 警卫

根据 Nestjs 文档,Guard 只有一个职责。它的作用是根据特定条件(特别是用户定义的逻辑)确定请求是否由控制器处理。它对于身份验证/授权很有用,并且是 Nestjs 中处理身份验证/授权的推荐方法。虽然身份验证/权限等可以使用middleware& 来完成,但是它们在 express 或其他 HTTP 服务器中完成,因为它们没有连接的强上下文,并且不需要知道将使用哪种方法来处理请求。中间件只有这个next功能,没有其他功能,因此对于 Nestjs 来说有点笨。但 Guard 可以访问执行上下文。它的设计更像是异常过滤器、管道和拦截器。

防护装置在每个中间件之后但在任何拦截器或管道之前执行

Guards 是一种提供程序,因为它的类也需要用@Injectable装饰器进行注释,但它必须实现接口或在 JS 的情况下CanActivate提供方法canActivate

大多数情况下,身份验证将使用passport或类似的库来处理。Nestjs 文档中解释了如何Nestjs 中passport创建身份验证流程。虽然可以使用,但并非生产安全。仅用于演示目的。AuthGuard

例如AuthGaurd

////// auth.guard.ts /////

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

function validateToken(token: string): boolean{
    return true
}

@Injectable()
export class AuthGuard implements CanActivate {
    logger: Logger = new Logger(AuthGuard.name)  

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
        try{
          // gives access to the express/fastify request object
        const request = context.switchToHttp().getRequest();
            // jwt/any kind of token
        const token = request?.hearders?.["Authorization"]?.split(" ")[1]
            if(!token)return false; // no token no entry

          return validateToken(token)
        }
        catch(e){
            this.logger.error(e)
            return false
        }
  }
}
Enter fullscreen mode Exit fullscreen mode

就像异常过滤器/管道一样,你可以在方法作用域/控制器作用域中使用@UseGaurds()装饰器来使用 Guard。它可以接受任意数量的 Guard 作为参数。

方法范围的 Guard 示例:

////// hello.controller.ts ///////
// ... import stuff
import {UseGuards} from "@nestjs/commmon"
import {AuthGuard} from "../../guards/auth.guard"

@Controller()
export class HelloController{
  // ..... other stuff

    @Get("/restricted-data")
    @UseGuards(AuthGuard)      // or pass it already being instantated as `new AuthGuard()`                             
    async getRestrictedData(){ // if it doesn't require dependency injection
        // ... logic
        return {};
    }

    // ..... other stuff
}
Enter fullscreen mode Exit fullscreen mode

request顺便说一句,您可以为Guard 中的对象分配新属性,并可以通过@Req()任何 Controller 路由处理程序中的参数装饰器访问它,getRestrictedData例如HelloController

就像管道/异常过滤器一样,你可以在应用的useGlobalGaurds方法中全局使用 Guard。这样就无需@UseGaurds()在每个需要该 Guard 的控制器/处理程序上都使用 Guard。

全局警卫的示例:

///// main.ts /////
// ...import stuff
import {AuthGuard} from "./guards/auth.guard"

async function bootstrap(){
    // ...other stuff

    app.useGlobalGuards(new AuthGuard())

    // ...other stuff
}

bootstrap()
Enter fullscreen mode Exit fullscreen mode

但是,如果您在该 Guard 内部使用/注入其他提供程序,则会引发错误。但是,如果您想同时保留依赖注入和全局范围,那么通过全局提供它AppModule,然后将其设置为全局 Guard 即可。

具有 DI 能力的 Global Guard:

///// app.module.ts //////
// ...import other stuff
import {AuthGuard} from "./guards/auth.guard"

// unique key/id for selecting the gaurd from within the NestFactory instance
export const AUTH_GUARD = "unqiue-auth-guard";

@Module({
  // ...other stuff

    providers: [
        AppService,
        {provide: AUTH_GUARD, useClass: AuthGuard}
    ],

  // ...other stuff
})
export class AppModule{
}

///// main.ts /////
// ...import stuff
import {AUTH_GUARD} from "./app.module";

async function bootstrap(){
    // ...other stuff

    const authGuard = app.select(AppModule).get(AUTH_GUARD)

    app.useGlobalGuards(authGuard);

    // ...other stuff
}

bootstrap()
Enter fullscreen mode Exit fullscreen mode

现在,又出现了一个问题。如何让路由在这个 Guard 中公开/不受限制?这时就Reflector派上用场了。它是一个由 提供的特殊类,@nestjs/core可以在任何模块范围的提供程序/控制器中访问,或者简单地说,在任何未全局实例化的控制器/提供程序/Guard/异常过滤器/拦截器/管道中访问。

使用装饰Reflector@SetMetadata()自定义装饰器,我们可以简单地处理这种情况

@SetMetadata()是 & 提供的方法和类装饰器@nestjs/common,可用于key-value metadata为方法/类设置特殊 & 可以通过Reflector注入到每个@Injectable()&@Controller()上下文中AppModule可用的来访问

自定义装饰器示例:

///// public.decorator.ts /////

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

export const IS_PUBLIC_KEY = "THIS_ROUTE_IS_PUBLIC"

// decorators are functions inside function with access to extra metadata provided 
// by the JSVM (JavaScript Interpreter). So you can ovbiously call 
// a decorator in a function like normal functions but just remember to `return` it to
// let the decorator's inner function to access those metadata about the class/method/parameter/property
// its currently being applied to
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

// the decorator flow-> `Public` is a function which returns & calls `SetMetadata`
// function which also returns & calls an inner function within it. Its called
// **function-currying**
// More on Wikipedia: https://en.wikipedia.org/wiki/Currying
Enter fullscreen mode Exit fullscreen mode

现在,在AuthGuardcanActivate方法中,我们可以获取上下文中当前活动的类/方法的元数据:

////// auth.guard.ts /////
// ...import stuff
import { Reflector } from "@nestjs/core";
import {IS_PUBLIC_KEY} from "./decorators/public.decorator"

@Injectable()
export class AuthGuard implements CanActivate {
        // ...other stuff

        // just add the Reflector as a type
    constructor(private readonly reflector: Reflector){}

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
        try{
            // accessing the passed metadata with its unique key/id
            // within the current execution context
            const isPublic = this.reflector.getAllAndOverride<boolean>(
                        IS_PUBLIC_KEY,
                        [
              context.getHandler(),
              context.getClass(),
                  ]
                    );
            if(isPublic) return true;

          // ... other validation logic/stuff
        }
        catch(e){
            this.logger.error(e)
            return false
        }
  }
}
Enter fullscreen mode Exit fullscreen mode

现在我们只需要@Public()在路由中应用自定义方法/类装饰器,使其不受限制。如果你读过第一部分,就会知道HelloController(在第一部分中创建的)有一个 GET /hello 路由,它会在请求时返回 hello。但是对于AuthGuard,该路由将受到限制。但是究竟是什么原因导致有人收不到热情的 hello呢?!所以,让我们将它向所有人开放:

////// hello.controller.ts ///////
// ... import stuff
import {Public} from "../decorators/public.decorator"

@Controller()
export class HelloController{
  // ..... other stuff

    @Get("hello")
  @Public() // now everyone gets a hello ;)
    async replyHello(){
        // ... logic
    }

    // ..... other stuff
}
Enter fullscreen mode Exit fullscreen mode

这是今天更新的完整应用程序

更新后,除以下所有路线外,其他路线/hello均会返回

{"statusCode": 403,"message": "Forbidden resource", "error": "Forbidden"}
Enter fullscreen mode Exit fullscreen mode

Bearer <token>提供任何带有请求标头字段的这种格式的 jwt-tokenAuthorization将使受保护的路由暂时起作用

文章来源:https://dev.to/krtirtho/nestjs-the-framework-of-nodejs-part-2-5ek2
PREV
你不是 React Native 菜鸟
NEXT
如何在 React App 中流畅渲染图像?