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{
}
装饰@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
}
让我们在/hello模块或的字段ByeService
中添加新创建的HelloModule
providers
////// 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{
}
那么,来自外部模块的提供程序怎么办呢?不用担心,只需像上面一样对提供程序进行操作,并在两个模块的字段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{
}
现在两个模块的提供程序都可以在彼此的范围内使用,让我们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
}
确保您的代码不依赖于首先调用哪个构造函数,因为这些提供程序类的实例化顺序是不确定的
有一个高级替代方案
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
}
}
}
就像异常过滤器/管道一样,你可以在方法作用域/控制器作用域中使用@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
}
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()
但是,如果您在该 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()
现在,又出现了一个问题。如何让路由在这个 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
现在,在AuthGuard
的canActivate
方法中,我们可以获取上下文中当前活动的类/方法的元数据:
////// 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
}
}
}
现在我们只需要@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
}
这是今天更新的完整应用程序
更新后,除以下所有路线外,其他路线/hello
均会返回
{"statusCode": 403,"message": "Forbidden resource", "error": "Forbidden"}
Bearer <token>
提供任何带有请求标头字段的这种格式的 jwt-tokenAuthorization
将使受保护的路由暂时起作用