共计 8674 个字符,预计需要花费 22 分钟才能阅读完成。
- 视频地址: 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 |
生命周期
要正当的编写利用必须当时理解分明整个程序的拜访流程, 本教程会解说如何一步步演进每一次拜访流, 作为第一步课时, 咱们的拜访流非常简单, 能够参考下图
文件构造
咱们通过整合 [typeorm][] 来连贯 mysql 实现一个根本的 CRUD 利用, 首先咱们须要创立一下文件构造
倡议初学者手动创立,没必要应用 CLI 去创立,这样目录和文件更加清晰
- 创立模块
- 编写模型
- 编写 Repository(如果有需要的话)
- 编写数据验证的 DTO
- 编写服务
- 编写控制器
- 在每个以上代码各自的目录下建设一个
index.ts
并导出它们 - 在各自的
Module
里进行注册提供者, 导出等 - 在
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.json
和nestjs-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<NestFastifyApplication>( | |
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 = 'CUSTOM_REPOSITORY_METADATA'; | |
// src/modules/core/decorators/repository.decorator.ts | |
// 定义装璜器 | |
import {CUSTOM_REPOSITORY_METADATA} from '../constants'; | |
export const CustomRepository = <T>(entity: ObjectType<T>): ClassDecorator => | |
SetMetadata(CUSTOM_REPOSITORY_METADATA, entity); | |
// src/modules/core/decorators/index.ts | |
export * from './repository.decorator'; |
定义静态方法用于注册自定义 Repository
public static forRepository<T extends Type<any>>(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<ObjectType<any>>(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<PostEntity> {} |
注册模型和存储类
在编写好 entity
和repository
之后咱们还须要通过 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<NestFastifyApplication>( | |
AppModule, | |
new FastifyAdapter(),); | |
useContainer(app.select(AppModule), {fallbackOnErrors: true}); | |
await app.listen(3000, '0.0.0.0'); | |
} |
最初启动利用在 Thunder Client
中测试接口