Nestjs最佳实践教程:2基本数据操作

  • 视频地址: https://www.bilibili.com/vide...
学习目标
  • 简单地整合nestjs框架与[typeorm][]
  • 实现基本的CRUD数据操作
  • 使用[class-validator][]验证请求数据
  • 更换更加快速的[fastify][]适配器
  • 使用Thunder Client对测试接口
安装Mysql
实际生产环境中建议使用PostgreSQL,因为教程以学习为主,所以直接使用相对来说比较通用和简单的Mysql
使用以下命令安装Mysql
如果本机不是使用linux(比如使用wsl2),请到mysql官网点击download按钮下载安装包后在chrome查看下载地址,然后在开发机用wget下载
如果本机使用MacOS,使用brew install mysql,如果本机使用Arch系列,使用sudo pacman -Syy mysql
# 下载镜像包 cd /usr/local/src sudo wget sudo wget https://repo.mysql.com/mysql-apt-config_0.8.22-1_all.deb # 添加镜像(其它选项不用管,直接OK就可以) sudo apt-get install ./mysql-apt-config_0.8.22-1_all.deb # 升级包列表 sudo apt-get update # 开始安装,输入密码后,有一个密码验证方式,因为是开发用,所以选择第二个弱验证即可 sudo apt-get install mysql-server # 初始化,在是否加载验证组件时选择No,在是否禁用远程登录时也选择No sudo mysql_secure_installation # 因为是远程SSH连接开发所以需要开启远程数据库链接,如果是本地或者wsl2则不需要开启 mysql -u root -p CREATE USER 'root'@'%' IDENTIFIED BY '密码'; GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES;

接着使用Navicat等客户端就可以连接了
预装依赖
  • [lodash][]是常用的工具库
  • [cross-env][]用于跨平台设置环境变量
  • [class-transformer][]用于对请求和返回等数据进行序列化
  • [class-validator][]用于验证请求dto
  • [typeorm][]一个TS编写的[node.js][]ORM
  • [@nestjs/typeorm][]Nestjs的TypeOrm整合模块
  • [@nestjs/platform-fastify][]Fastify适配器,用于替代express
  • [nestjs-swagger][]生成open api文档,目前我们使用其PartialType函数是UpdateDto中的属性可选
  • [fastify-swagger][]生成Fastify的Open API
~ pnpm add class-transformer \ @nestjs/platform-fastify \ class-validator \ lodash \ @nestjs/swagger \ fastify-swagger \ mysql2 \ typeorm \ @nestjs/typeorm ~ pnpm add @types/lodash cross-env @types/node typescript -D

生命周期 要合理的编写应用必须事先了解清楚整个程序的访问流程,本教程会讲解如何一步步演进每一次访问流,作为第一步课时,我们的访问流非常简单,可以参考下图
Nestjs最佳实践教程:2基本数据操作
文章图片

文件结构 我们通过整合[typeorm][]来连接mysql实现一个基本的CRUD应用,首先我们需要创建一下文件结构
建议初学者手动创建,没必要使用CLI去创建,这样目录和文件更加清晰
  1. 创建模块
  2. 编写模型
  3. 编写Repository(如果有需要的话)
  4. 编写数据验证的DTO
  5. 编写服务
  6. 编写控制器
  7. 在每个以上代码各自的目录下建立一个index.ts并导出它们
  8. 在各自的Module里进行注册提供者,导出等
  9. AppModule中导入这两个模块
编写好之后的目录结构如下
. ├── app.module.ts# 引导模块 ├── config# 配置文件目录 │├── database.config.ts# 数据库配置 │└── index.ts ├── main.ts# 应用启动器 ├── modules ├── content# 内容模块目录 │├── content.module.ts# 内容模块 │├── controllers# 控制器 │├── dtos# DTO访问数据验证 │├── entities# 数据实体模型 |├── index.ts │├── repositories# 自定义Repository │├── services# 服务 └──core ├── constants.ts# 常量 ├── core.module.ts# 核心模块 ├── decorators# 装饰器 └── types.ts# 公共类型

应用编码 在开始编码之前需要先更改一下package.jsonnestjs-cli.json两个文件
package.json中修改一下启动命令,以便每次启动可以自动配置运行环境并兼容windows环境
"prebuild": "cross-env rimraf dist", "start": "cross-env NODE_ENV=development nest start", "start:dev": "cross-env NODE_ENV=development nest start --watch", "start:debug": "cross-env NODE_ENV=development nest start --debug --watch", "start:prod": "cross-env NODE_ENV=production node dist/main",

为了在每次重新编译前自动删除上次的产出,在nestjs-cli.json中配置 "deleteOutDir": true
main.ts
把适配器由[express][]换成更快的[fastify][],并把监听的IP改成0.0.0.0方便外部访问.为了在使用[class-validator][]的DTO类中也可以注入nestjs容器的依赖,需要添加useContainer
// main.ts import { NestFactory } from '@nestjs/core'; import { FastifyAdapter, NestFastifyApplication, } from '@nestjs/platform-fastify'; import { useContainer } from 'class-validator'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create( AppModule, new FastifyAdapter() ); useContainer(app.select(AppModule), { fallbackOnErrors: true }); await app.listen(3000,'0.0.0.0'); } bootstrap();

连接配置
创建一个src/config/database.config.ts文件
export const database: () => TypeOrmModuleOptions = () => ({ // ... // 此处entites设置为空即可,我们直接通过在模块内部使用`forFeature`来注册模型 // 后续魔改框架的时候,我们会通过自定义的模块创建函数来重置entities,以便给自己编写的CLI使用 // 所以这个配置后面会删除 entities: [], // 自动加载模块中注册的entity autoLoadEntities: true, // 可以在开发环境下同步entity的数据结构到数据库 // 后面教程会使用自定义的迁移命令来代替,以便在生产环境中使用,所以以后这个选项会永久false synchronize: process.env.NODE_ENV !== 'production', });

CoreModule
核心模块用于挂载一些全局类服务,比如整合[typeorm][]的`TypeormModule
注意: 这里不要使用@Global()装饰器来构建全局模块,因为后面在CoreModule类中添加一些其它方法
返回值中添加global: true来注册全局模块,并导出metadata.
// src/core/core.module.ts export class CoreModule { public static forRoot(options?: TypeOrmModuleOptions) { const imports: ModuleMetadata['imports'] = [TypeOrmModule.forRoot(options)]; return { global: true, imports, module: CoreModule, }; } }

AppModule导入该模块,并注册数据库连接
// src/app.module.ts @Module({ imports: [CoreModule.forRoot(database())], ... }) export class AppModule {}

自定义存储类
由于原来用于自定义Repository的@EntityRepository在typeorm0.3版本后已经不可用,特别不方便,所以根据这里的示例来自定义一个CustomRepository装饰器
// src/modules/core/constants.ts // 传入装饰器的metadata数据标识 export const CUSTOM_REPOSITORY_METADATA = 'https://www.it610.com/article/CUSTOM_REPOSITORY_METADATA'; // src/modules/core/decorators/repository.decorator.ts // 定义装饰器 import { CUSTOM_REPOSITORY_METADATA } from '../constants'; export const CustomRepository = (entity: ObjectType): ClassDecorator => SetMetadata(CUSTOM_REPOSITORY_METADATA, entity); // src/modules/core/decorators/index.ts export * from './repository.decorator';

定义静态方法用于注册自定义Repository
public static forRepository( repositories: T[], dataSourceName?: string, ): DynamicModule { const providers: Provider[] = []; for (const Repo of repositories) { const entity = Reflect.getMetadata(CUSTOM_REPOSITORY_METADATA, Repo); if (!entity) { continue; }providers.push({ inject: [getDataSourceToken(dataSourceName)], provide: Repo, useFactory: (dataSource: DataSource): typeof Repo => { const base = dataSource.getRepository(entity); return new Repo(base.target, base.manager, base.queryRunner); }, }); }return { exports: providers, module: CoreModule, providers, }; }

ContentModule
内容模块用于存放CRUD操作的逻辑代码
// src/modules/content/content.module.ts @Module({}) export class ContentModule {}

AppModule中注册
// src/app.module.ts @Module({ imports: [CoreModule.forRoot(database()),ContentModule], ... }) export class AppModule {}

实体模型 创建一个PostEntity用于文章数据表
PostEntity继承`BaseEntity,这样做是为了我们可以进行ActiveRecord操作,例如PostEntity.save(post),因为纯DataMapper的方式有时候代码会显得啰嗦,具体请查看此处
@CreateDateColumn @UpdateDateColumn是自动字段,会根据创建和更新数据的时间自动产生,写入后不必关注
// src/modules/content/entities/post.entity.ts // 'content_posts'是表名称 @Entity('content_posts') export class PostEntity extends BaseEntity { ... @CreateDateColumn({ comment: '创建时间', }) createdAt!: Date; @UpdateDateColumn({ comment: '更新时间', }) updatedAt!: Date; }

存储类 本节存储类是一个空类,后面会添加各种操作方法
这里用到我们前面定义的自定义CustomRepository装饰器
// src/modules/content/repositories/post.repository.ts @CustomRepository(PostEntity) export class PostRepository extends Repository {}

注册模型和存储类 在编写好entityrepository之后我们还需要通过Typeorm.forFeature这个静态方法进行注册,并把存储类导出为提供者以便在其它模块注入
// src/modules/content/content.module.ts @Module({ imports: [ TypeOrmModule.forFeature([PostEntity]), // 注册自定义Repository CoreModule.forRepository([PostRepository]), ], exports: [ // 导出自定义Repository,以供其它模块使用 CoreModule.forRepository([PostRepository]), ], }) export class ContentModule {}

DTO验证 DTO配合管道(PIPE)用于控制器的数据验证,验证器则使用[class-validator][]
class-validator是基于validator.js的封装,所以一些规则可以通过validator.js的文档查找,后面教程中我们会编写大量的自定义的验证规则,这节先尝试基本的用法
其基本的使用方法就是给DTO类的属性添加一个验证装饰器,如下
groups选项用于配置验证组
// src/modules/content/dtos/create-post.dto.ts @Injectable() export class CreatePostDto { @MaxLength(255, { always: true, message: '文章标题长度最大为$constraint1', }) @IsNotEmpty({ groups: ['create'], message: '文章标题必须填写' }) @IsOptional({ groups: ['update'] }) title!: string; ... }

更新验证类UpdatePostDto继承自CreatePostDto,为了使CreatePostDto中的属性变成可选,需要使用[@nestjs/swagger][]包中的PartialType方法,请查阅此处文档
// src/modules/content/dtos/update-post.dto.ts @Injectable() export class UpdatePostDto extends PartialType(CreatePostDto) { @IsUUID(undefined, { groups: ['update'], message: '文章ID格式错误' }) @IsDefined({ groups: ['update'], message: '文章ID必须指定' }) id!: string; }

服务类 服务一共包括5个简单的方法,通过调用PostRepository来操作数据
// src/modules/content/services/post.service.ts @Injectable() export class PostService { // 此处需要注入`PostRepository`的依赖 constructor(private postRepository: PostRepository) {} // 查询文章列表 async findList() // 查询一篇文章的详细信息 async findOne(id: string) // 添加文章 async create(data: CreatePostDto) // 更新文章 async update(data: UpdatePostDto) // 删除文章 async delete(id: string) }

控制器 控制器的方法通过@GET,@POST,@PUT,@PATCH,@Delete等装饰器对外提供接口,并且通过注入PostService服务来操作数据.在控制器的方法上使用框架自带的ValidationPipe管道来验证请求中的body数据,ParseUUIDPipe来验证params数据
// 控制器URL的前缀 @Controller('posts') export class PostController { constructor(protected postService: PostService) {}... // 其它方法请自行查看源码 @Get(':post') async show(@Param('post', new ParseUUIDPipe()) post: string) { return this.postService.findOne(post); }@Post() async store( @Body( new ValidationPipe({ transform: true, forbidUnknownValues: true, // 不在错误中暴露target validationError: { target: false }, groups: ['create'], }), ) data: CreatePostDto, ) { return this.postService.create(data); } }

注册控制器等
  • 为了后面`DTO中可能会导入服务,需要把DTO,同样注册为提供者并且改造一下main.ts,把容器加入到class-containter
  • PostService服务可能后续会被UserModule等其它模块使用,所以此处我们也直接导出
// src/modules/content/content.module.ts @Module({ imports: [ TypeOrmModule.forFeature([PostEntity]), // 注册自定义Repository CoreModule.forRepository([PostRepository]), ], providers: [PostService, CreatePostDto, UpdatePostDto], controllers: [PostController], exports: [ PostService, // 导出自定义Repository,以供其它模块使用 CoreModule.forRepository([PostRepository]), ], }) export class ContentModule {}

// src/main.ts ... async function bootstrap() { const app = await NestFactory.create( AppModule, new FastifyAdapter(), ); useContainer(app.select(AppModule), { fallbackOnErrors: true }); await app.listen(3000, '0.0.0.0'); }

【Nestjs最佳实践教程:2基本数据操作】最后启动应用在Thunder Client中测试接口

    推荐阅读