Effect(以前称为 Effect-TS)通过提供自定义类型和函数,增强 Typescript 的功能,这些类型和函数从如 Scala 中的 ZIO 这样的框架中的函数式编程(FP)获得启发。尽管它最初受到了 FP 的启发,但 Effect 旨在解决 Typescript 实际中存在的问题,并弥补标准库中的缺失功能,这些功能可以在像 NestJS 这样的面向对象编程框架中使用,就像我们在本文中将看到的那样。
说明:本文假定读者已经有一定 NestJS 和 Effect 的基础。我们将重点放在如何有效整合这些技术,而不是深入探讨特定框架的细节。
除了本文中的示例外,整个源代码也可以在这里找到 here。
设置首先,我们创建一个新的项目来试试这个。
使用 nest CLI 来创建一个项目
$ nest new my-nest-effect-app
然后用你喜欢的包管理器安装 effect
$ npm install effect
运行此命令来安装effect模块
那么,让我们修改 TypeScript 配置文件内容,如果还没有设置的话,添加 strict: true
这一行。
// tsconfig.json
{
"compilerOptions": {
// ...其他属性
"strict": true // 这很重要
}
}
最后,我们可以删除今天暂时用不到的文件。
test
目录,app.controller.spec.ts
文件,app.service.ts
文件,app.controller.ts
文件,
我们将先使用嵌套模式创建一个基本的过程,接着使用Effect进行重构该过程。
要做到这一点,我们需要在 src
文件夹中创建一个名为 modules
的文件夹,然后在 modules
文件夹内创建一个名为 cat
的文件夹。
...其他文件...
main.ts
package.json
src/
- modules/ // 新增的文件夹
- cat/ // 新的文件夹
然后我们将创建 cat.module.ts
和 cat.controller.ts
文件,如下。
// cat.controller.ts
@Controller('cats') // 控制猫的控制器
export class CatController { // 猫控制器类
constructor(private readonly catService: CatService) {} // 构造函数,传入一个唯一的CatService实例
@Get() // 获取所有猫的信息
getCats(): Cat[] {
return this.catService.getCats();
}
@Post() // 创建一只猫
createCat(@Body() catDto: {name: string}): string {
return this.catService.createCat(catDto.name);
}
@Get(':id') // 根据ID获取一只猫的信息
getCat(@Param('id') id: string): Cat {
return this.catService.getCat(id);
}
}
// cat.module.ts // 猫模块
@Module({ // 模块定义
controllers: [CatController], // 控制器列表
providers: [], // 服务列表
})
export class CatModule {} // 猫模块类
如你所见,我们将用一个Cat
域来进行测试,该域包含3个不同的路由,以测试流程的顺畅。
- 获取 /cats
- POST /cats
- 获取 /cats/{id}
最后,我们将创建 cat.service.ts
和 cat.type.ts
这两个文件。
// cat.type.ts
export interface Cat {
id: string;
name: string;
}
// cat.service.ts
@Injectable()
export class CatService {
db: Map<string, Cat> = new Map(); // 模拟外部的数据库
getCats(): Cat[] {
return Array.from(this.db.values());
}
createCat(name: string): string {
const id = this.db.size + 1;
const newCat = {
id: id.toString(),
name,
};
this.db.set(newCat.id, newCat);
return newCat.id;
}
getCat(id: string): Cat {
const cat = this.db.get(id);
if (!cat) {
throw new Error("找不到这只猫咪");
}
return cat;
}
}
不要忘记在 cat.module.ts
中导入 cat.service.ts
,然后在 app.module.ts
中再导入 cat.module.ts
你的 modules/cat
文件夹应该看起来像这样
modules/
- cat/
- cat.controller.ts,
- cat.service.ts,
- cat.type.ts,
- cat.module.ts
现在我们已经设置好了整个流程,我想明确具体的问题是什么,通过利用效果可以解决这些问题,哪怕是在这么简单的流程里。
首先,我们来看看cat.service.ts
文件。这里有两个地方需要注意,一个在getCat方法里,另一个在createCat方法里。我们仔细看看。
getCat 方法
getCat(id: string): Cat {
const cat = this.db.get(id);
if (!cat) {
throw new Error("找不到这只猫了"); // 这里会抛出错误
}
return cat;
}
在这里,当我们找不到猫时,会抛出一个明确的异常。但由于TS没有提供任何Either/Result类型来让我们指定此方法将返回一个 Cat
或一个 Error
,当我们在这个方法被使用的地方,比如在控制器内部时,我们可能不会注意到这一点,忽略它,或者认为这个方法不会出错。
实现猫咪的函数
createCat(name: string): string {
const id = this.db.size + 1;
const newCat = { // 注意:缺少验证
id: id.toString(),
name,
};
this.db.set(newCat.id, newCat); // 直接使用新猫的ID和对象设置
return newCat.id;
}
这个 createCat
方法缺乏验证。通常我们在数据库插入之前或过程中会有一个验证步骤。使用纯 TS,我们无法轻易验证新的猫对象。用户可能成功输入一个无效的名字,无论是值还是类型,这可能会引发错误或损害数据。
那怎么用Effect来搞定呢?
效果流让我们把 cat.service.ts
文件改成使用 Effect。
首先,我们将这样修改 getCat 方法。
// 之前
getCat(id: string): Cat {
const cat = this.db.get(id);
if (!cat) {
throw new Error("找不到猫了");
}
return cat;
}
// 之后
getCat(id: string): Effect.Effect<Cat, Error> { // 新的返回类型,包含Effect
const cat = this.db.get(id);
return Effect.fromNullable(cat); // 将可能为空的值转换为Effect
}
这里有两点改动。
- 将返回类型更改为
Effect.Effect<Cat, Error>
类型,这种类型明确地返回可能的结果和可能的错误。 - 使用
Effect.fromNullable
函数将可能为 null(null 或 undefined)的值转换为 Effect 对象。
这两个变化大大改善了开发体验和维护的便利性
现在的 createCat,
// 之前
createCat(name: string): string {
const id = this.db.size + 1;
const newCat = {
id: id.toString(),
name,
};
this.db.set(newCat.id, newCat);
return newCat.id;
}
// 之后
createCat(name: string): Effect.Effect<string, ParseError> { // 明确值的类型和可能的解码错误
return Effect.gen(this, function* () { // 创建生成器函数返回一个效果
const id = this.db.size + 1;
const newCat = {
id: id.toString(),
name,
};
const cat = yield* Schema.decode(Cat)(newCat); // 使用Schema Cat解码对象,如果解码失败则抛出错误
this.db.set(cat.id, cat);
return cat.id;
});
}
在这里,我们用 Effect 架构模式 来解码 newCat
对象。若失败,它会自动返回一个 解析错误。
要做到这一点,我们还需要将我们的猫类型(Cat Type)改为使用 Effect Schema(效果模式) 而不是普通的 TS。
// cat.type.ts
const _Cat = Schema.Struct({
id: Schema.String,
name: Schema.String,
});
export interface Cat extends Schema.Schema.Type<typeof _Cat> {} // 这样使用TS类型就像之前一样
export const Cat: Schema.Schema<Cat> = _Cat; // 这样定义的Schema类型将用于解码
注意,我不会详细解释Effect的实现(如Schema.Struct,Generator函数等),我建议你直接查看官方文档,那里已经解释得很清楚了,你可以直接参考
现在我们解决了这些问题,让我们通过用一个使用Effect的新方法替换原来的getCats方法来完善我们的catService,以下是新的文件。
@Injectable()
export class CatService {
db: Map<string, Cat> = new Map();
getCats(): Effect.Effect<Cat[]> {
return Effect.succeed(Array.from(this.db.values()));
}
createCat(name: string): Effect.Effect<string, ParseError> {
return Effect.gen(this, function* () {
const id = this.db.size + 1;
const newCat = {
id: id.toString(),
name,
};
const cat = yield* Schema.decode(Cat)(newCat);
this.db.set(cat.id, cat);
return cat.id;
});
}
getCat(id: string): Effect.Effect<Cat, Error> {
const cat = this.db.get(id);
return Effect.fromNullable(cat);
}
}
我们还可以更换控制器,使其更贴合服务方法返回的类型。
@Controller('cats')
// 控制器类,用于处理与猫咪相关的请求
export class CatController {
constructor(private readonly catService: CatService) {}
// 构造函数,接收一个不可更改的CatService实例
@Get()
// 获取所有猫咪信息的HTTP GET请求处理方法
getCats(): Effect.Effect<Cat[]> {
// 返回所有猫咪的信息列表
return this.catService.getCats();
}
@Post()
// 创建新猫咪的HTTP POST请求处理方法
createCat(@Body() catDto: {name: string}): Effect.Effect<string, ParseError> {
// 接收一个对象,其中包含猫咪的名字,并创建一个新猫咪
return this.catService.createCat(catDto.name);
}
@Get(':id')
// 根据ID获取单个猫咪信息的HTTP GET请求处理方法
getCat(@Param('id') id: string): Effect.Effect<Cat, Error> {
// 根据ID返回单个猫咪的信息
return this.catService.getCat(id);
}
}
// 注释:"@Body()", "@Param()", "@Get()", "@Post()" 是装饰器,用于定义HTTP请求处理方法
// "Effect.Effect" 表示一个效果或异步操作的结果,可能包含成功或错误信息
// "ParseError" 表示解析错误,可能在数据解析过程中出现
// "Error" 表示一般错误,用于处理可能出现的各种异常情况
我们快完成了,但 nest 中一个重要特性是管道,尤其是非常常见的验证管道。让我们来看看如何用 Effect 实现它。
在我们项目的 app.controller.ts
文件中,有一个名为 createPost 的方法,可能需要一个合适的 Dto 对象。
// 使用一个新的 CatDto 类型
// |
@Post() // V
createCat(@Body() catDto: {name: string}): Effect.Effect<string, ParseError> {
return this.catService.createCat(catDto.name);
}
咱们来创建一个 cat.dto.ts
技术文件
// 通过使用类模式,您可以充分利用所有装饰器功能
export class CatDto extends Schema.Class<CatDto>('CatDto')({
name: Schema.String,
}) {}
我们现在可以在控制器里可以导入它了。
(Note: After applying the expert suggestions, "了" is retained twice to accurately reflect both suggestions, but typically, consecutive "了"s are not used. Therefore, a more polished version would be "我们现在可以在控制器里导入它了。")
最终优化后的翻译应为:
我们现在可以在控制器里导入它了。
@Post()
createCat(@Body() catDto: CatDto): Effect.Effect<string, ParseError> {
// 创建一只猫,接收一个包含猫名称的对象,并返回一个效果
return this.catService.createCat(catDto.name);
// 调用catService的createCat方法,传入猫的名字
}
最后,我们将创建一个自定义的 Pipe 来验证 Dto,并在新文件中实现它。
在 src
文件夹中创建一个 shared
文件夹,并在里面创建一个 effect-validation.pipe.ts
文件。
@Injectable()
export class EffectValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
// metadata.metatype 相当于 @Body() 或 @Param 参数中给定的类型
if (Schema.isSchema(metadata.metatype)) {
// 类似于使用 Schema.decode(CatDto)(value)
return new metadata.metatype(value);
}
return value;
}
}
就这样。如果用户输入不符合Dto,这会自动抛出错误,还会移除多余的键,只保留Dto模式中指定的那些键。
请注意,此 ValidationPipe 只是一个示例,可能不足以应对复杂的验证和类型检查。
不要忘记导入这个验证管道,要么一个一个地应用到每个路由上,要么全局应用它。
@Module({
imports: [CatModule],
controllers: [],
providers: [
{
provide: APP_PIPE,
useClass: EffectValidationPipe,
},
],
})
export class AppModule {}
// @Module 表示这是一个模块定义,用于组织应用的不同部分。
// imports: [CatModule] 表示引入 CatModule 模块。
// controllers: [] 表示不包含任何控制器。
// providers: [] 中定义了服务提供者,这里提供了一个 APP_PIPE,其使用 EffectValidationPipe 类来实现。
// APP_PIPE 是一个应用级别的管道,用于处理请求。
// useClass: EffectValidationPipe 表示使用 EffectValidationPipe 类来实现管道。
最后的改变
如果你现在尝试通过调用猫控制器端点来使用该应用,可能会看到一些奇怪的返回,比如这样的情况。
{
"_id": "退出",
"_tag": "成功",
"value": []
}
这是因为我们在返回值给用户之前,并没有正确地执行我们的Effect。确实,在我们指定它执行之前,Effect实际上并不会被运行。
在我们的应用中,做这件事的最佳地点是在拦截器里。我们在shared
文件夹里创建一个新文件,比如叫做effect.interceptor.ts
。
@Injectable()
export class EffectInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> | Promise<Observable<any>> {
return next
.handle()
.pipe(map((data: Effect.Effect<unknown>) => Effect.runPromise(data)));
// 在返回数据给用户前先运行效果
// 这行代码的作用是在数据返回给用户之前先执行效果
}
}
然后将这个拦截器要么应用到每个路由上,要么全局应用。
//模块: 定义了模块的元数据
@Module({
imports: [CatModule],
controllers: [],
//提供者: 注册的提供者列表
providers: [
{
provide: APP_INTERCEPTOR, //APP_INTERCEPTOR: 应用拦截器
useClass: EffectInterceptor, //使用类: 注册一个类来创建提供者
},
{
provide: APP_PIPE, //APP_PIPE: 应用管道
useClass: EffectValidationPipe,
},
],
})
//应用模块: 应用程序的根模块
export class AppModule {}
就这样 ✨Voila✨,你就可以拥有一个运行 Effect 的 NestJS 项目。
资源: 联系我们如果你有任何问题或遇到任何错误,可以在评论里留言哦!
你也可以通过以下方式联系我,比如:
共同学习,写下你的评论
评论加载中...
作者其他优质文章