一种在_NestJS_框架中处理副作用的方法
几个月前,我发表了第一篇文章,探讨了如何将_Effect_与_NestJS_集成。尽管它意外地得到了积极的反馈,但我感觉文章没有为读者提供足够的指导来全面拥抱_Effect_生态系统。
我已经带来了完整且可以直接执行的解决方案,旨在简化你们的采用流程。
@nestjs-effect - 一个基于 NestJS 的效果库Effectul 是 NestJS 中的第一个库。
它提供了即插即用的功能
- 一个专门的运行时,用于在您的应用边界执行 Effect 脚本或功能,且 无需任何手动设置(支持 HTTP、RPC 和 WebSocket 协议等)
- 自动检测并集成您的 Effect 服务到 NestJS 依赖注入系统
- 与不使用 Effect 的标准 NestJS 核心包和库 完全兼容
- 通过管道机制,对 DTO 进行根据 Effect 架构的验证
本文将通过实际演示和提出实施建议来探讨解决方案的所有功能。
模块和运行时环境库最重要的特性之一是在应用程序的边界能够自动运行Effect,这能够大大减少在_Nestjs_中使用_Effect_时的样板代码和困惑。
它通过提供一个自定义的 EffectRuntimeInterceptor
实现,该拦截器会运行效果,如果需要的话会映射值/错误,并提供 NestJS 服务上下文中的 Effect(如有),这可能包括 Effect 或非 Effect 类型的服务。
我们来举一个例子。
首先让我们来安装核心模块
npm i @nestjs-effect/core
注:npm
是 Node.js 的包管理器,用于安装和管理软件包。
然后在应用程序模块中设置模块。
// app.module.ts
// 引入Effect模块
import { EffectModule } from '@nestjs-effect/core';
@Module({
// 注意这里
imports: [EffectModule.forRoot()],
controllers: [AppController],
providers: [],
})
export class AppModule {}
EffectModule 的所有选项 可以在处找到
最后在你想让效果自动运行的地方添加 EffectRuntimeInterceptor
。它可以全局设置在 AppModule 中,也可以局部设置在控制器或方法内部。
由控制者:
// app.controller.ts
import { EffectRuntimeInterceptor } from '@nestjs-effect/core';
@Controller()
@UseInterceptors(EffectRuntimeInterceptor) // 此处
export class AppController {
constructor() {}
@Get()
@UseInterceptors(EffectRuntimeInterceptor) // 或者此处
getHello(): Effect.Effect<number> {
return Effect.succeed(1)
}
}
按模块分类
// app.module.ts
import { EffectModule, EffectRuntimeInterceptor } from '@nestjs-effect/core';
@Module({
imports: [EffectModule.forRoot()],
controllers: [AppController],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: EffectRuntimeInterceptor, // 这里
},
],
})
export class AppModule {}
就这样。行了。你现在可以在控制器里返回一个 Effect 了。
我们来把控制器改为返回更有意思的Effect。
@Controller()
export class AppController {
constructor() {}
@Get()
getHello(): Effect.Effect<{ ok: boolean; data: string }> { // 获取问候信息的方法
return Effect.succeed({ // 表示一个效果,包含成功状态和数据
ok: true,
data: 'Hello World', // 输出"你好,世界"
});
}
}
如果你调用这个端点,你会得到
{
"ok": true,
"data": "你好,世界!"
}
这也有效,即使你发送失败的消息,就像下面这样。
@Controller()
export class AppController {
constructor() {}
@Get()
getHello(): Effect.Effect<never, string> {
return Effect.fail('返回错误');
}
}
会返回
{
"statusCode": 500,
"message": "服务器错误"
}
默认情况下不会显示错误消息,以防止泄露给用户。
为了映射所发送的错误(或值),我们可以使用模块中的选项:
mapValue
(映射值)mapError
(映射错误)
NestJS 和 Effect 的一个重要方面是它们的依赖注入(DI),虽然它们不能完全无缝协作,但我们可以在旁边使用它们。
我们来看一个例子,在这个例子中,我们需要在我们的_Effect_中提供一个自定义服务。
// 随机服务的 TypeScript 代码示例
@Injectable() // 注解,表示此服务可以被依赖注入
export class Random extends Context.Tag('MyRandomService')< // 扩展 Context.Tag
Random, // 随机类
{ readonly next: Effect.Effect<number> } // next 是一个返回数字的 Effect 类型
> () {}
// app.module.ts
@Controller()
export class AppController {
constructor() {}
@Get()
// 注意返回类型中包含`Random`的需求
getHello(): Effect.Effect<{ ok: boolean; data: number }, never, Random> {
// 注意这里的返回类型是`Effect.Effect<{ ok: boolean; data: number }, never, Random>`
return Effect.gen(function* () {
const random = yield* Random;
// 从`Random`源生成随机数
return {
ok: true,
data: yield* random.next,
// 返回一个对象,包含成功标志`ok`和从`random.next`获取的数值`data`
};
});
}
}
如果我们现在就这样运行这个效果,会出现一个 Nest JS 控制台上的错误:
错误信息:找不到服务:MyRandomService
那么使用_NestJS_是如何提供功能的呢?这种方式和使用无副作用的服务是否完全一样呢?
我们需要为 NestJS 提供一个使用我们服务的提供者
// random.service.ts
@Injectable()
// 服务标记/接口
export class Random extends Context.Tag('MyRandomService')<
Random,
{ readonly next: Effect.Effect<number> }
>() {}
// 实现服务
export const RandomLive = Layer.succeed(
Random,
Random.of({ next: Effect.succeed(Math.random()) }),
);
// app.module.ts
@Module({
imports: [
EffectModule.forRoot({
// autoServiceDiscovery 需要设为 `true`,才能使用 NestJS 的内建 DI
// 来为 Effect 服务注入
autoServiceDiscovery: true,
}),
],
controllers: [AppController],
providers: [
{
provide: Random, // 提供 Context.Tag 作为标记
useValue: RandomLive, // 使用 Layer 实现 Context.Tag 的功能
},
{
provide: APP_INTERCEPTOR,
useClass: EffectRuntimeInterceptor,
},
],
})
export class AppModule {}
如果我们现在再访问这个端点一次
{
"ok": true,
"信息": 0.2959038884446399
}
模式验证注意:使用 NestJS 的 DI 并不是提供服务的唯一方法。参见文档 了解更多。
一个关键功能是通过一个 NestJS 管道来验证 Effect-Schema。配置过程非常简单——只需在你选择的验证层级(无论是模块、控制器还是方法级别)添加 EffectValidationPipe
,然后在控制器中导入你的架构。
// 定义一个用户数据传输对象,包含用户ID和名字。
export class UserDTO extends Schema.Class<UserDTO>('UserDTO')({
id: Schema.Number,
name: Schema.NonEmptyString,
}) {}
// user.controller.ts
@Controller('user')
@UsePipes(EffectValidationPipe)
export class UserController {
constructor() {}
@Post()
addUser(@Body() dto: UserDTO) {
// 注释
}
}
当向API端点发出请求时,请求中的数据会先经过验证,只有在验证成功后才会被接收并传递给函数处理。
默认,验证失败会返回一个通用错误信息。
{
"message": "验证失败",
"error": "错误请求",
"statusCode": 400
}
为了自定义错误信息,你可以使用 EffectModule
中的 customError
选项。这需要创建一个能够接收 ParseError
参数的自定义错误类,该参数会自动传递给你的自定义错误处理程序。
// user.error.ts
import { HttpException } from '@nestjs/common';
import { ParseError } from 'effect/ParseResult';
// 您的自定义错误需要是一个类,可以接收一个类型为 ParseError 的错误
// 这个错误会被自动传递给您的自定义错误
export class UserValidationError extends HttpException {
constructor(error: ParseError) {
super('用户验证失败', 400);
}
}
// app.module.ts
@Module({
imports: [
EffectModule.forRoot({ // 效果模块的初始化配置
validation: {
customError: UserValidationError, // 用户验证错误
},
}),
],
controllers: [UserController], // 控制器
providers: [], // 提供者
})
export class AppModule {}
现在,如果出错,它会返回
{
"statusCode": 400,
"message": "用户验证失败"
}
另外,可以通过开启 strict
选项来强制仅使用 Effect Schema,拒绝任何无法验证的值。这种拒绝可能是因为没有提供有效的 Effect Schema,或者是因为验证失败。当启用严格模式时,在控制器中使用普通的 TypeScript 类型而非 Effect Schema 类型将会触发验证错误。
// app.module.ts
@Module({
imports: [
EffectModule.forRoot({
validation: {
customError: UserValidationError,
strict: true
},
}),
],
controllers: [UserController],
providers: [],
})
export class AppModule {}
// user.controller.ts
@Controller('user')
@UsePipes(EffectValidationPipe)
export class UserController {
constructor() {}
@Post()
addUser(@Body() dto: { id: number; name: string }) {
//
}
}
这是一个用户控制器类,用于处理用户相关的请求。它使用了@Controller
装饰器来定义控制器处理的路径,@UsePipes
装饰器来应用数据验证管道EffectValidationPipe
,以及@Post
装饰器来定义处理POST请求的方法。addUser
方法接受一个包含用户ID和名称的对象dto
作为参数。
返回
{
"statusCode": 400,
"message": "用户验证失败"
}
因为我们没有给 Body 提供效果模式,而是提供了一个普通的 TS 类型,而是采用了严格的 TS 模式。
最终的话语这个库是我个人的一个项目,源于我在这些技术中的实际经验。它还处于初级阶段,目前还不适合用于生产环境,但这是尝试将这两个优秀的框架和谐结合的一个努力。
如果你能试用一下就太好了,遇到问题时标出来,并分享你的改进建议!你的反馈将帮助我们更好地开发它。
仓库:https://github.com/nestjs-effect/nestjs-effect
npm包:https://www.npmjs.com/package/@nestjs-effect/core
我的上一篇文章:关于NestJS的效果 https://medium.com/@yatogamii/effect-with-nestjs-3ee4a4da54f3
这里有一些资源- [Effect (Effect)] 文档
- [Nest (Nest)] 文档
如果你遇到任何问题或 bug,在评论区留言告诉我。
你也可以用这些方式联系我。
共同學(xué)習(xí),寫下你的評(píng)論
評(píng)論加載中...
作者其他優(yōu)質(zhì)文章