NestJS框架的学习心得有哪些?
- 内容介绍
- 文章标签
- 相关推荐
本文共计12477个文字,预计阅读时间需要50分钟。
说明+学习NestJS官方基础课程【中文版+NestJS Fundamentals Course】个人笔记+因为用不到测试,所以暂时学到P65+后续项目可能用不到MongoDB,后面的也还没看+基础结构+生成controller
说明
学习NestJS 官方基础课程个人笔记
因为用不到测试,所以暂时学到了P65
后续项目可能用不到MogoDB,后面的也就没看
基础结构
- 生成controller
nest g controller coffee - 生成service
nest g service coffee - 生成module
nest g module coffee - 生成entities
nest g class coffee/entities/coffee.entity --no-spec - 生成DTO
nest g class coffee/dto/create-coffee.dto --no-specDTO和Entity的区别,
- Entity可能带有ID,是查询数据时定义的接口,
- DTO是生成数据或更新时候用的
验证数据正确性
NextJS提供了ValidationPipe进行数据验证
ValidationPipe提供了对所有传入客户端有效负载强制执行验证规则的便捷方式
- 在main.ts中加入app.useGlobalPipes(new ValidationPipe());
import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();a
- 安装两个包yarn add class-validator class-transformer
export class CreateCoffeeDto {
@IsString()
readonly name: string;
@IsString()
readonly brand: string;
@IsString({ each: true })
readonly flavors: string[];
}
DTO代码抽离
PartialType:表示继承所有属性,但是所有属性都是可选的,相当于只验证正确性,不验证存在性
import { CreateCoffeeDto } from "./create-coffee.dto";
export class UpdateCoffeeDto extends PartialType(CreateCoffeeDto) {
}
配置参数白名单,进行参数过滤
在ValidationPipe中传入一个对象,其中包含键/值白名单:true
import { NestFactory } from "@nestjs/core";import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true
}));
await app.listen(3000);
}
bootstrap();
开启后,通过post上传参数,将自动过滤掉不需要的参数
如果开启forbidNonWhitelisted:true,即
import { NestFactory } from "@nestjs/core";import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 参数白名单
forbidNonWhitelisted: true // 上传白名单之外的参数,报错
}));
await app.listen(3000);
}
bootstrap();
如果上传不需要参数,会报错
instanceof
默认接受的参数instanceof dto是false
@Post()create(@Body() createCoffeeDto: CreateCoffeeDto) {
console.log(createCoffeeDto instanceof CreateCoffeeDto);
return this.coffeesService.create(createCoffeeDto);
}
通过在ValidationPipe配置transform:true,可以返回true
import { NestFactory } from "@nestjs/core";import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 参数白名单
forbidNonWhitelisted: true, // 上传白名单之外的参数,报错
transform: true
}));
await app.listen(3000);
}
bootstrap();
Docker配置
参考
DockerId:kaisarh
Email:hkzxh1104
password:hkzxh1104
使用Docker
services:
db:
image: postgres
restart: always
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD:
目前Docker Compose YAML文件中只列出了一项服务,但供将来参考
使用typeorm关联数据库
yarn add @nestjs/typeorm typeorm@2 pg
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { CoffeeModule } from "./coffee/coffee.module";
import { TypeOrmModule } from "@nestjs/typeorm";
@Module({
imports: [CoffeeModule, TypeOrmModule.forRoot({
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "pass123",
database: "postgres",
autoLoadEntities: true, // 有助于自动加载模块,而不是指定实体数组
synchronize: true // 同步,确保我们的TypeORM实体在每次运行应用程序时都会与数据库同步 生产环境设置为true,开发环境设置为false
})],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {
}
- 通过@Entity注解标注实体表
- 通过PrimaryGeneratedColumn标注自增主键
- 通过@Column标注行,可以通过设置options配置参数。例如nullable设置非空
在coffee.module.ts中进行导入imports: [TypeOrmModule.forFeature([CoffeeEntity])],
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CoffeeEntity } from "./entities/coffee.entity";
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([CoffeeEntity])],
controllers: [CoffeeController],
providers: [CoffeeService]
})
export class CoffeeModule {
}
通过使用forFeature()将TypeORM注册到此模块中
我们在主AppModule中使用了forRoot(),但我们只这样做了一次,注册实体时,所有其他模块都将使用forFeature()
在这里的forFeature()内部,传入一个实体数组,在咖啡例子中,只有一个咖啡实体
typeorm操作数据库
{
id: 1,
name: "Shipwreck Roast",
brand: "Buddy Brew",
flavors: ["chocolate", "vanilla"]
},
{
id: 2,
name: "Raw coconut Latte",
brand: "Lucky Coffee",
flavors: ["coconut", "vanilla"]
}
];
将数据表注入后,可以删除这部分数据,并通过与数据库交互直接操作数据库
import { HttpException, HttpStatus, Injectable, NotFoundException } from "@nestjs/common";import { CoffeeEntity } from "./entities/coffee.entity";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { CreateCoffeeDto } from "./dto/create-coffee.dto";
import { UpdateCoffeeDto } from "./dto/update-coffee.dto";
// 创建命令 nest g module coffee
@Injectable()
export class CoffeeService {
constructor(
@InjectRepository(CoffeeEntity)
private readonly coffeeEntityRepository: Repository<CoffeeEntity>
) {
}
findAll() {
return this.coffeeEntityRepository.find();
}
async findOne(id: string) {
// 抛出JS错误会返回服务器500
// throw "A random error";
const coffee = await this.coffeeEntityRepository.findOne(id);
// 错误处理,抛出异常
if (!coffee) {
//throw new HttpException(`Coffee #${id} not found`, HttpStatus.NOT_FOUND);
throw new NotFoundException(`Coffee #${id} not found!`);
} else {
return coffee;
}
}
create(createCoffeeDto: CreateCoffeeDto) {
const coffee = this.coffeeEntityRepository.create(createCoffeeDto);
return this.coffeeEntityRepository.save(coffee);
}
async update(id: string, updateCoffeeDto: UpdateCoffeeDto) {
// preload首先查看数据库中是否存在实体,存在更新实体中的所有值,不存在返回undefined
// 注意:preload只会查找并更新实体,不会更新数据库
const coffee = await this.coffeeEntityRepository.preload({
id: +id,
...updateCoffeeDto
});
if (!coffee) {
throw new NotFoundException(`Coffee #${id} not found`);
}
return this.coffeeEntityRepository.save(coffee);
}
async remove(id: string) {
const coffee = await this.findOne(id);
return this.coffeeEntityRepository.remove(coffee);
}
}
表之间关系
- 一对一:@OneToOne()
- 一对多:@OneToMany() 或者@ManyToOne()
- 多对多:@ManyToMany()
不同表之间建立关联
定义实体
@Entity()
export class FlavorEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
}
- 删除flavors的@Column()装饰器,并通过其他装饰器与FlavorEntity设置Relation
- 从typeorm中引入@JoinTable()装饰器,其可以指定关系的OWNER端,在这里是Coffee Entity
- 通过@ManyToMany在Coffee Entity中指定与Flavor Entity的关系
- 第一个参数指定type
- 第二个参数绑定与type中的哪个参数绑定关联
import { JoinTable } from "typeorm/browser";
import { Flavor } from "./flavor.entity";
@Entity()
// 默认 sql table === 'coffee'
// 可以在Entity('TABLE_NAME')进行指定
export class Coffee {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
brand: string;
@JoinTable()
@ManyToMany(
type => Flavor,
flavor => flavor.coffees)
flavors: string[];
}
- 通过@ManyToMany在Flavor Entity中指定与Coffee Entity的关系
import { Coffee } from "./coffee.entity";
@Entity()
export class Flavor {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(
type => Coffee,
coffee => coffee.flavors
)
coffees: Coffee[];
}
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor])],
controllers: [CoffeeController],
providers: [CoffeeService]
})
export class CoffeeModule {
}
- 关于relations的解释为:
- Indicates what relations of entity should be loaded (simplified left join form).
- 指示应加载的实体关系(简化的左联接形式)。
return this.coffeeRepository.find({
relations:['flavors']
});
}
async findOne(id: string) {
// 抛出JS错误会返回服务器500
// throw "A random error";
const coffee = await this.coffeeRepository.findOne(id,{
relations:['flavors']
});
// 错误处理,抛出异常
if (!coffee) {
//throw new HttpException(`Coffee #${id} not found`, HttpStatus.NOT_FOUND);
throw new NotFoundException(`Coffee #${id} not found!`);
} else {
return coffee;
}
}
级联插入
添加新的咖啡Coffee的时候,如果口味Flavor不存在?
import { Flavor } from "./flavor.entity";
@Entity()
// 默认 sql table === 'coffee'
// 可以在Entity('TABLE_NAME')进行指定
export class Coffee {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
brand: string;
@JoinTable()
@ManyToMany(
type => Flavor,
(Flavor) => Flavor.coffees,
{
cascade:true // ['insert']
})
flavors: string[];
}
- 将Flavor Repository注入到CoffeesService类中
@InjectRepository(Coffee)
private readonly coffeeRepository: Repository<Coffee>,
@InjectRepository(Flavor) // 將Flavor注入到coffeeService中
private readonly flavorRepository:Repository<Flavor>
) {
}
- 定义一个新的私有方法并将其命名为:preloadFlavorByName
const existingFlavor = await this.flavorRepository.findOne({name});
if(existingFlavor){
return existingFlavor;
}
this.flavorRepository.create({name});
}
- 调整create()方法
// 使用map遍历CreateCoffeeDto所中有风味,对不存在的数据进行创建
const flavors = await Promise.all(
createCoffeeDto.flavors.map(name => this.preloadFlavorByName(name))
);
const coffee = this.coffeeRepository.create({
...createCoffeeDto,
flavors
});
return this.coffeeRepository.save(coffee);
}
调整coffee.entity.ts中flavors的类型
import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from "typeorm";import { Flavor } from "./flavor.entity";
@Entity()
// 默认 sql table === 'coffee'
// 可以在Entity('TABLE_NAME')进行指定
export class Coffee {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
brand: string;
@JoinTable()
@ManyToMany(
type => Flavor,
(Flavor) => Flavor.coffees,
{
cascade:true // ['insert']
})
flavors: Flavor[]; // 将flavors的类型设置为Flavor
}
- 调整update()方法
// preload首先查看数据库中是否存在实体,存在更新实体中的所有值,不存在返回undefined
// 注意:preload只会查找并更新实体,不会更新数据库
const flavors =
updateCoffeeDto.flavors &&
(await Promise.all(
updateCoffeeDto.flavors.map(name => this.preloadFlavorByName(name))
));
const coffee = await this.coffeeRepository.preload({
id: +id,
...updateCoffeeDto,
flavors
});
if (!coffee) {
throw new NotFoundException(`Coffee #${id} not found`);
}
return this.coffeeRepository.save(coffee);
}
分页查询
nest g class common/dto/pagination-query.dto --no-spec
limit: number;
offset: number;
}
export class PaginationQueryDto {
@Type(() => Number)
limit: number;
@Type(() => Number)
offset: number;
}
这一步也可以通过在ValidationPipe中添加transformOptions对象,将enableImplicitConversion设置为true,在全局层面上启用隐式类型转换
import { NestFactory } from "@nestjs/core";import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 参数白名单
forbidNonWhitelisted: true, // 上传白名单之外的参数,报错
transform: true,
transformOptions: {
enableImplicitConversion: true
}
}));
await app.listen(3000);
}
bootstrap();
import { IsOptional } from "class-validator";
export class PaginationQueryDto {
@IsOptional()
@Type(() => Number)
limit: number;
@Type(() => Number)
@IsOptional()
offset: number;
}
import { IsOptional, IsPositive } from "class-validator";
export class PaginationQueryDto {
@IsPositive()
@IsOptional()
@Type(() => Number)
limit: number;
@IsPositive()
@IsOptional()
@Type(() => Number)
offset: number;
}
export class PaginationQueryDto {
@IsPositive()
@IsOptional()
limit: number;
@IsPositive()
@IsOptional()
offset: number;
}
findAll(@Query() paginationQuery: PaginationQueryDto) {
// const { limit, offset } = paginationQuery;
return this.coffeesService.findAll(paginationQuery);
}
const { limit, offset } = paginationQuery;
return this.coffeeRepository.find({
relations: ["flavors"],
skip: offset,
take: limit
});
}
事务
@Entity()
export class Event {
@PrimaryGeneratedColumn()
id: number;
@Column()
type: string;
@Column()
name: string;
// payload 是存储事件有效负载通用列
@Column("json")
payload: Record<string, any>;
}
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [CoffeeService]
})
export class CoffeeModule {
}
import { Flavor } from "./flavor.entity";
@Entity()
// 默认 sql table === 'coffee'
// 可以在Entity('TABLE_NAME')进行指定
export class Coffee {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
brand: string;
// 新增推荐属性
@Column({ default: 0 })
recommendations: number;
@JoinTable()
@ManyToMany(
type => Flavor,
(Flavor) => Flavor.coffees,
{
cascade: true // ['insert']
})
flavors: Flavor[]; // 将flavors的类型设置为Flavor
}
@InjectRepository(Coffee)
private readonly coffeeRepository: Repository<Coffee>,
@InjectRepository(Flavor) // 將Flavor注入到coffeeService中
private readonly flavorRepository: Repository<Flavor>,
// 引入Connection用来创建事务
private readonly connection: Connection
) {
}
const queryRunner = this.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
}
- 首先创建一个新的queryRunner
- 使用创建的queryRunner创建到数据库的新连接
- 建立连接后,可以开始交易过程
- 将整个事务包装在try / catch / finally中,以确保如果出现任何问题,catch可以回滚整个事务
- 事务是我们能够回滚和撤销发生的任何事情,以防出现问题
const queryRunner = this.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
coffee.recommendations++;
const recommendEvent = new Event();
recommendEvent.name = "recommend_coffee";
recommendEvent.type = "coffee";
recommendEvent.payload = { coffeeId: coffee.id };
await queryRunner.manager.save(coffee);
await queryRunner.manager.save(recommendEvent);
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
} finally {
await queryRunner.release();
}
}
- 在try中,增加coffee的推荐属性并创建一个新的推荐咖啡事件,使用查询运行器实体管理器来保存咖啡和事件实体
- 在catch语句中看到,如果出现任何问题,保存任一实体失败,通过回滚整个事务来防止数据库中的不一致
- 在finallye中,保证一切结束后释放或关闭queryRunner
缓存
- 使用@Index()装饰器在列上定义一个索引
@Column()
name: string;
- 列的复合索引,可以通过将@Index()装饰器应用在类本身,并在装饰器内传递一个列名数组作为参数
@Index(["name", "type"])
@Entity()
export class Event {
@PrimaryGeneratedColumn()
id: number;
@Column()
type: string;
@Index()
@Column()
name: string;
// payload 是存储事件有效负载通用列
@Column("json")
payload: Record<string, any>;
}
数据库迁移
数据库迁移提供了一种增量更新我们的数据库模式并使其与应用程序数据模型保持同步的方法,同时保留我们数据库中的现有数据。
To generate, run and revert migrations
生成、运行和恢复迁移
在创建新的迁移之前,我们需要创建一个新的TypeORM配置文件并正确连接我们的数据库
- 在项目的根目录中创建一个ormconfig.js文件
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "pass123",
database: "postgres",
entities: ["dist/**/*.entity.js"],
migrations: ["dist/migrations/*.js"],
cli:{
migrationsDir:'src/migrations'
}
};
这里的配置设置是我们从Docker Compose文件中使用的所有端口、密码等,还有一些额外的关键值用于让TypeORM迁移,知道我们的实体和迁移文件将在哪里
- 执行迁移命令,并将此迁移命名为:CoffeeRefactor
npx typeorm migration:create -n CoffeeRefactor - 该命令在/src/migrations目录中生成一个新的迁移文件
- 假设需要更改coffee.entity,将name更改为title
title: string;
- 对实体的更新会自动更新开发数据库,因为设置了synchronize: true,但是不会更新生产数据库,这是迁移非常方便的主要原因之一
- 更新name为title后,不仅会删除名称列,还会删除该列中的所有数据
- 只有在删除该列后,才会创建没有任何旧数据的新标题列
- 迁移帮助我们重命名现有列并维护我们以前的所有数据
- 打开迁移文件并增加迁移逻辑,让数据库知道需要进行更改
- 基础迁移文件都有一个up()和down()方法
export class CoffeeRefactor1653399205455 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}
- up()是只是需要更改的内容以及如何更改的内容
- down()是撤销或回滚任何这些更改的地方,万一出现问题,需要一个退出策略,帮助撤销一切,down()就可以保证我们的迁移回滚
- 在up()中新增更改表字段名称的数据库语句
'ALTER TABLE "coffee" RENAME COLUMN "name" TO "title"',
)
在这里可以执行所需的任何类型的数据库迁移,同时,必须为回滚迁移提供逻辑
- 在down()中新增回滚逻辑
'ALTER TABLE "coffee" RENAME COLUMN "title" TO "name"',
)
- 迁移文件完整代码
export class CoffeeRefactor1653399205455 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "coffee" RENAME COLUMN "name" TO "title"',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "coffee" RENAME COLUMN "title" TO "name"',
)
}
}
- 测试迁移
- 确保构建源代码,以便TypeORM CLI可以在/dist目录下找到身份和迁移文件
构建代码yarn run build - 构建完成后,生成dist目录
- 通过以下方式运行"迁移"命令类型:
- npx typeorm migration:run
- 再次执行
- 恢复更改
- npx typeorm migration:revert
- TypeORMCLI可以自动生成迁移,连接到数据库并将现有表与提供的实体定义进行比较,如果发现差异,TypeORM会生成一个新的迁移
- coffee.entity中新增description
description: string;
- 编译代码:yarn run build
- 输入命令,让TypeORM生成迁移,并将其命名为SchemaSync
- npx typeorm migration:generate -n SchemaSync
- 打开/src/migrations/中新生成的迁移文件,查看up()和down()
export class SchemaSync1653400611645 implements MigrationInterface {
name = 'SchemaSync1653400611645'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "coffee" ADD "description" character varying`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "coffee" DROP COLUMN "description"`);
}
}
- 执行迁移:npx typeorm migration:run
依赖注入
当我使用CoffeeService并将其注入到构造函数中时
constructor(private readonly coffeesService: CoffeeService) {}NextJS通过以下三件事实现:
此装饰器将CoffeeService类标记为Provider
这个请求高速Nest将提供程序注入到我们的控制器类中
)容器注册了这个容器
封装
- coffee-rating-module
import { CoffeeRatingService } from './coffee-rating.service';
@Module({
providers: [CoffeeRatingService]
})
export class CoffeeRatingModule {}
- coffee-rating-service
@Injectable()
export class CoffeeRatingService {}
- 因为属于不同模块,
- 所以在CoffeeRtaingModule中导入CoffeeModule
import { CoffeeRatingService } from "./coffee-rating.service";
import { CoffeeModule } from "../coffee/coffee.module";
@Module({
imports: [CoffeeModule],
providers: [CoffeeRatingService]
})
export class CoffeeRatingModule {
}
- 切换到CoffeeRatingService, 并使用基于构造函数的注入来添加CoffeeService
import { CoffeeService } from "../coffee/coffee.service";
@Injectable()
export class CoffeeRatingService {
constructor(private readonly coffeeService: CoffeeService) {
}
}
- 这样运行后会报错
- 原因:默认情况下,所有模块都封装了他们的提供者(Provider)如果想在另外一个模块中使用它们,必须明确地将他们定义为导出(exported),使它们成为该模块的公共API的一部分
- 解决:
- 在coffee.module.ts中将CoffeeService导出
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [CoffeeService],
exports: [CoffeeService]
})
export class CoffeeModule {
}
- 更改后可以正常运行
自定义提供程序
以下场景:
- 创建我们的提供者自定义实例,而不是让Nest实例化该类
- 在第二个依赖项中重用现有类
- 用模拟版本覆盖一个类进行测试
- 使用策略模式,提供一个抽象类并根据不同条件交换实际实现(或要使用的实际类)
通过Nest定义自定义提供程序来处理这些场景。providers数组形式只是简写,实际上只是提供TOKEN并在该TOKEN的位置提供"要注入的内容"的简写版本。完整写法为:
providers:[{
provider: CoffeesService,
useClass: CoffeesService
}
]
Nest提供了不同方法进行自定义提供者。
假设在Nest容器中添加一个外部库,或者用Mock\{}对象替代服务的真实实现。
例如:将CoffeeService替换为自定义Provider并使用useValue语法,当在程序中注入CoffeeService时,每当CoffeeService TOKEN被解析时,他将指向新的MockCoffeeService,
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
class MockCoffeeService {
}
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [{ provide: CoffeeService, useValue: new MockCoffeeService() }],
exports: [CoffeeService]
})
export class CoffeeModule {
}
因此,可以通过使用useValue
重命名提供者令牌
在之前,均是使用类名作为Provider tokens,provider tokens是我们传递给provider属性的任何内容,通过使用更灵活的字符串或符号作为依赖注入令牌
例如提供一个字符串值标记"COFFEE_BRANDS",通过useValue将值设置为字符串数组
import { Module } from "@nestjs/common";import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
class MockCoffeeService {
}
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [
CoffeeService,
{ provide: "COFFEE_BRANDS", useValue: ["buddy brew", "nescafe"] }
],
exports: [CoffeeService]
})
export class CoffeeModule {
}
使用@Inject()装饰器,并将需要查找的令牌作为参数进行赋值
constructor(@InjectRepository(Coffee)
private readonly coffeeRepository: Repository<Coffee>,
@InjectRepository(Flavor) // 將Flavor注入到coffeeService中
private readonly flavorRepository: Repository<Flavor>,
// 引入Connection用来创建事务
private readonly connection: Connection,
@Inject("COFFEE_BRANDS") coffeeBrands: string[]
) {
}
这样就可以使用COFFEE_BRANDS并访问我们传递给此提供程序的值数组
最好在一个单独的CONSTANT常量文件夹中定义TOKEN并导出、导入使用
- 在coffee目录下新建coffee.constants.ts
- 在Module和Service中,引入常量并使用
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
import { COFFEE_BRANDS } from "./coffee.constants";
class MockCoffeeService {
}
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [
CoffeeService,
{ provide: COFFEE_BRANDS, useValue: ["buddy brew", "nescafe"] }
],
exports: [CoffeeService]
})
export class CoffeeModule {
}
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
import { COFFEE_BRANDS } from "./coffee.constants";
class ConfigService {
}
class DevelopmentConfigService {
}
class ProductionConfigService {
}
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [
CoffeeService,
{
provide: ConfigService,
useClass:
process.env.NODE_ENV === "development" ?
DevelopmentConfigService :
ProductionConfigService
},
{ provide: COFFEE_BRANDS, useValue: ["buddy brew", "nescafe"] }
],
exports: [CoffeeService]
})
export class CoffeeModule {
}
useFactory的返回值将被提供者(provider)使用。
新的更现实的例子,并在其中注入一些提供程序:
定义一个随机提供者,并确保将其注册为提供者(@Injectable())
@Injectable()export class CoffeeBrandsFactory {
create() {
// do something
return ["buddy brew", "nescafe"];
}
}
更新现有的COFFEE_BRANDS提供程序以使用CoffeeBrandsFactory,新增一个名为inject的属性
inject本身接收一个提供者(provider)数组,这些提供者被传递到useFactory函数中后可以随意使用,返回需要的值
import { Injectable, Module } from "@nestjs/common";import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
import { COFFEE_BRANDS } from "./coffee.constants";
@Injectable()
export class CoffeeBrandsFactory {
create() {
// do something
return ["buddy brew", "nescafe"];
}
}
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [
CoffeeService,
CoffeeBrandsFactory,
{
provide: COFFEE_BRANDS,
useFactory: (brandsFactory: CoffeeBrandsFactory) => brandsFactory.create(),
inject: [CoffeeBrandsFactory]
}
],
exports: [CoffeeService]
})
export class CoffeeModule {
}
通过使用Promise,将async/await与useFactory语法结合使用,可以实现异步(比如数据库未连接不接受请求)
import { Injectable, Module } from "@nestjs/common";import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
import { COFFEE_BRANDS } from "./coffee.constants";
import { Connection } from "typeorm";
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [
CoffeeService,
{
provide: COFFEE_BRANDS,
useFactory: async (connection: Connection): Promise<string[]> => {
// const coffeeBrands = await connection.query('SELECT *** ...');
const coffeeBrands = await Promise.resolve(["buddy brew", "nescafe"]);
console.log("[!] Async Factory");
return coffeeBrands;
},
inject: [Connection]
}
],
exports: [CoffeeService]
})
export class CoffeeModule {
}
动态模块
有时在使用模块时需要更多的灵活性,例如:静态模块不能由使用它们的模块配置其Provider
比如:有一个通用模块,该模块需要在不同情况下表现不同
动态模块需要一些配置才可以被消费者使用
测试
nest g mo database
import { createConnection } from "typeorm";
@Module({
providers: [
{
provide: "CONNECTION",
useValue: createConnection({
type: "postgres",
host: "localhost",
port: 5432
})
}
]
})
export class DatabaseModule {
}
如果另一个应用程序想要使用这个模块但是需要使用不同的端口怎么办?
通过使用Nest的动态模块功能,可以让消费模块使用API来控制导入时如自定义DatabaseModule
- 在DatabaseModule上定义一个名为register()的静态方法
- register()可以接收消费模块传递过来的参数
- register()返回DynamicModule类型结果,它与典型的@Module()具有基本相同的接口,但需要传递一个module属性,也就是当前模块本身
- 通过register(),可以将接收的参数用于创建数据库连接中
import { ConnectionOptions, createConnection } from "typeorm";
@Module({})
export class DatabaseModule {
static register(options: ConnectionOptions): DynamicModule {
return {
module: DatabaseModule,
providers: [
{
provide: "CONNECTION",
useValue: createConnection(options)
}
]
};
}
}
使用方法如下:
import { Module } from "@nestjs/common";import { CoffeeRatingService } from "./coffee-rating.service";
import { CoffeeModule } from "../coffee/coffee.module";
import { DatabaseModule } from "../database/database.module";
@Module({
imports: [DatabaseModule.register({
type: "postgres",
host: "localhost",
password: "password",
port: 5432
}), CoffeeModule],
providers: [CoffeeRatingService]
})
export class CoffeeRatingModule {
}
服务提供者的Scope
SpringBoot中提供了Scope注解来指明Bean的作用域,NestJs也提供了类似的@Scope()装饰器:
scope名称
说明
SINGLETON
单例模式,整个应用内只存在一份实例
REQUEST
每个请求初始化一次
TRANSIENT
每次注入都会实例化
Config Module
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { CoffeeModule } from "./coffee/coffee.module";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CoffeeRatingModule } from "./coffee-rating/coffee-rating.module";
import { DatabaseModule } from "./database/database.module";
import { ConfigModule } from "@nestjs/config";
@Module({
imports: [ConfigModule.forRoot(), CoffeeModule, TypeOrmModule.forRoot({
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "pass123",
database: "postgres",
autoLoadEntities: true, // 有助于自动加载模块,而不是指定实体数组
synchronize: true // 同步,确保我们的TypeORM实体在每次运行应用程序时都会与数据库同步 生产环境设置为true,开发环境设置为false
}), CoffeeRatingModule, DatabaseModule],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {
}
DATABASE_PASSWORD=pass123
DATABASE_NAME=postgres
DATABASE_PORT=5432
DATABASE_HOST=localhost
配置均与数据库配置相关,来此docker-compose
打开.gitignore,增加以下行
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { CoffeeModule } from "./coffee/coffee.module";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CoffeeRatingModule } from "./coffee-rating/coffee-rating.module";
import { DatabaseModule } from "./database/database.module";
import { ConfigModule } from "@nestjs/config";
@Module({
imports: [ConfigModule.forRoot(), CoffeeModule, TypeOrmModule.forRoot({
type: "postgres",
host: process.env.DATABASE_HOST,
port: +process.env.DATABASE_PORT,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
autoLoadEntities: true, // 有助于自动加载模块,而不是指定实体数组
synchronize: true // 同步,确保我们的TypeORM实体在每次运行应用程序时都会与数据库同步 生产环境设置为true,开发环境设置为false
}), CoffeeRatingModule, DatabaseModule],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {
}
envFilePath: ".environment"
}),
除了传递字符串值,也可以传递字符串数组来为.env文件指定多个路径
如果在多个文件中找到相同变量,优先使用第一个匹配文件中的变量
ignoreEnvFile: true
}),
- 安装:yarn add @hapi/joi
- 安装types:yarn add -D @types/hapi__joi
- 定义验证模式。在ConfigModule.forRoot()方法内部,通过validationSchema确保以正确的格式传入某些环境变量
validationSchema: Joi.object({
DATABASE_HOST: Joi.required(),
DATABASE_PORT: Joi.number().default(5432)
})
}),
完整配置:
import { Module } from "@nestjs/common";import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { CoffeeModule } from "./coffee/coffee.module";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CoffeeRatingModule } from "./coffee-rating/coffee-rating.module";
import { DatabaseModule } from "./database/database.module";
import { ConfigModule } from "@nestjs/config";
import * as Joi from "@hapi/joi";
@Module({
imports: [
ConfigModule.forRoot({
validationSchema: Joi.object({
DATABASE_HOST: Joi.required(),
DATABASE_PORT: Joi.number().default(5432)
})
}),
CoffeeModule, TypeOrmModule.forRoot({
type: "postgres",
host: process.env.DATABASE_HOST,
port: +process.env.DATABASE_PORT,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
autoLoadEntities: true, // 有助于自动加载模块,而不是指定实体数组
synchronize: true // 同步,确保我们的TypeORM实体在每次运行应用程序时都会与数据库同步 生产环境设置为true,开发环境设置为false
}), CoffeeRatingModule, DatabaseModule],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {
}
- 在coffee.module中导入ConfigModule。
我们在主AppModule中使用了forRoot()方法,其他地方不需要做任何事
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
import { COFFEE_BRANDS } from "./coffee.constants";
import { Connection } from "typeorm";
import { ConfigModule } from "@nestjs/config";
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event]), ConfigModule],
controllers: [CoffeeController],
providers: [
CoffeeService,
{
provide: COFFEE_BRANDS,
useFactory: async (connection: Connection): Promise<string[]> => {
// const coffeeBrands = await connection.query('SELECT *** ...');
const coffeeBrands = await Promise.resolve(["buddy brew", "nescafe"]);
console.log("[!] Async Factory");
return coffeeBrands;
},
inject: [Connection]
}
],
exports: [CoffeeService]
})
export class CoffeeModule {
}
- 在CoffeeService中注入并通过get()方法获取参数
constructor(
@InjectRepository(Coffee)
private readonly coffeeRepository: Repository<Coffee>,
@InjectRepository(Flavor) // 將Flavor注入到coffeeService中
private readonly flavorRepository: Repository<Flavor>,
// 引入Connection用来创建事务
private readonly connection: Connection,
@Inject(COFFEE_BRANDS) coffeeBrands: string[],
private readonly configService: ConfigService
) {
const databaseHost = this.configService.get<string>("DATABASE_HOST");
console.log(databaseHost);
}
get()可以接收第二个参数,设置默认值,如果获取不到值则取默认值
const databaseHost = this.configService.get<string>("DATABASE_HOSTa", "demo");配置文件
通过配置文件对不同配置进行管理与使用
environment: process.env.NODE_ENV || "development",
database: {
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT, 10) | 5432
}
})
通过工厂函数导出配置,包括环境和数据库,主机和端口通过env类指定
- 配置:在app.module.ts中ConfigModule.forRoot传入一个一个load新属性,它接收一个配置工厂数组
load: [appConfig]
}),
- 获取:在coffee.service.ts中,通过get()方法获取配置的时候,不需要使用DATABASE_HOST获取,直接使用database.host即可
随着项目增长,配置文件增多,可能需要位于多个不同目录的"特定于功能"的配置文件
随着拥有越来越多的配置键,使用非类型化的configService.get()方法获取所有配置值很容易出错,由于必须使用"点表示法"(即a.b)来检索嵌套项
为了防止这种情况,结合两项技术:配置命名空间和部分注册以进行验证配置。
- 新建src/config/coffee.config.ts,通过registerAs()函数可以在命名空间内定义一个token,也就是第一个参数
export default registerAs("coffee", () => ({
foo: "bar"
}));
- 在coffee.module.ts中使用ConfigModule.forFeature()注册这个coffeeConfig,也就是部分配准
TypeOrmModule.forFeature([Coffee, Flavor, Event]),
ConfigModule.forFeature(coffeeConfig)
],
- 在CoffeeService中通过get()获取配置
console.log(coffeeConfig);
也可以通过点语法获取对应值
const coffeeConfig = this.configService.get("coffee");const coffeeConfigFoo = this.configService.get("coffee.foo");
console.log(coffeeConfig);
console.log(coffeeConfigFoo);
private readonly coffeeConfiguration: ConfigType<typeof coffeeConfig>
每个命名空间配置都暴露了一个Token也就是key属性,可以使用该属性将整个对象注入到Nest容器中注册的任何类
ConfigType是一个开箱即用的辅助类型,推断函数的返回类型
console.log(coffeeConfiguration.foo);可以直接通过该对象获取配置,甚至有强类型的好处
异步import
目前app.module.ts中配置如下
import { Module } from "@nestjs/common";import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { CoffeeModule } from "./coffee/coffee.module";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CoffeeRatingModule } from "./coffee-rating/coffee-rating.module";
import { DatabaseModule } from "./database/database.module";
import { ConfigModule } from "@nestjs/config";
import * as Joi from "@hapi/joi";
import appConfig from "./config/app.config";
@Module({
imports: [
// ConfigModule.forRoot({
// validationSchema: Joi.object({
// DATABASE_HOST: Joi.required(),
// DATABASE_PORT: Joi.number().default(5432)
// })
// }),
ConfigModule.forRoot({ load: [appConfig] }),
CoffeeModule,
TypeOrmModule.forRoot({
type: "postgres",
host: process.env.DATABASE_HOST,
port: +process.env.DATABASE_PORT,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
autoLoadEntities: true, // 有助于自动加载模块,而不是指定实体数组
synchronize: true // 同步,确保我们的TypeORM实体在每次运行应用程序时都会与数据库同步 生产环境设置为true,开发环境设置为false
}),
CoffeeRatingModule,
DatabaseModule
],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {
}
使用process.env的配置是在加载环境配置ConfigModule.forRoot({load: [appConfig]})之后的,如果在之前使用,会报错
通过异步加载可以解决,使用forRootAsync结合工厂函数进行配置
import { Module } from "@nestjs/common";import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { CoffeeModule } from "./coffee/coffee.module";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CoffeeRatingModule } from "./coffee-rating/coffee-rating.module";
import { DatabaseModule } from "./database/database.module";
import { ConfigModule } from "@nestjs/config";
import * as Joi from "@hapi/joi";
import appConfig from "./config/app.config";
@Module({
imports: [
// ConfigModule.forRoot({
// validationSchema: Joi.object({
// DATABASE_HOST: Joi.required(),
// DATABASE_PORT: Joi.number().default(5432)
// })
// }),
TypeOrmModule.forRootAsync({
useFactory: () => ({
type: "postgres",
host: process.env.DATABASE_HOST,
port: +process.env.DATABASE_PORT,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
autoLoadEntities: true, // 有助于自动加载模块,而不是指定实体数组
synchronize: true // 同步,确保我们的TypeORM实体在每次运行应用程序时都会与数据库同步 生产环境设置为true,开发环境设置为false
})
}),
ConfigModule.forRoot({ load: [appConfig] }),
CoffeeModule,
CoffeeRatingModule,
DatabaseModule
],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {
}
异常过滤器、管道、守卫、拦截器
- 转换:将输入数据转换为期望的输出
- 验证:评估输入数据,有效则通过管道,无效抛出异常
- 在方法执行之前或之后绑定额外的逻辑
- 转换方法返回的结果
- 扩展基本方法行为
- 完全覆盖方法
例如:处理诸如"缓存响应"之类的事情
如何将上述四种构建块绑定到我们的应用程序?基本上有三种不同的绑定方式:过滤器、守卫和拦截器绑定到路由处理程序,管道特定(仅适用于管道)
嵌套构建块可以是:
- "全局"范围
- "控制器"范围
- "方法"范围
- 额外的第4个"参数"范围:仅适用于管道
在main.ts中,通过ValidationPipe设置全局管道
import { NestFactory } from "@nestjs/core";import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 参数白名单
forbidNonWhitelisted: true, // 上传白名单之外的参数,报错
transform: true,
transformOptions: {
enableImplicitConversion: true
}
}));
await app.listen(3000);
}
bootstrap();
但是在这里设置无法注入任何依赖,因此可以在app.module.ts中通过provider进行设置并
定义一个名为APP_PIPEprovider的东西,以这种方式提供ValidationPipe,可以在AppModule的范围内实例化ValidationPipe并在创建后将其注册为全局管道。
每个其他构建块功能也有类似的标记
import { APP_PIPE, APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from "@nestjs/core";如何设置非全局,例如将ValidationPipe绑定到仅在CoffeeController中定义的每个路由处理程序
在app.controller.ts中使用@UsePipes装饰器绑定单个管道或用,分隔的管道列表
其他相同
UsePipes, UseFilters, UseGuards, UseInterceptors,import {Controller,
Get,
Post,
Body,
Param,
HttpCode,
HttpStatus,
Res,
Patch,
Delete,
Query,
UsePipes, UseFilters, UseGuards, UseInterceptors, ValidationPipe
} from "@nestjs/common";
import { CoffeeService } from "./coffee.service";
import { CreateCoffeeDto } from "./dto/create-coffee.dto";
import { UpdateCoffeeDto } from "./dto/update-coffee.dto";
import { PaginationQueryDto } from "../common/dto/pagination-query.dto";
class demo {
canActivate(context) {
return true;
}
}
// 创建命令 nest g controller coffee
interface PostHello {
name: string,
id: number | string
}
@UsePipes(ValidationPipe)
@Controller("coffee")
export class CoffeeController {
constructor(private readonly coffeesService: CoffeeService) {
}
@UseGuards(demo)
@Get()
findAll(@Query() paginationQuery: PaginationQueryDto) {
// const { limit, offset } = paginationQuery;
console.log(123);
return this.coffeesService.findAll(paginationQuery);
}
@Get(":id")
findOne(@Param("id") id: string) {
return this.coffeesService.findOne(id);
}
@Post()
create(@Body() createCoffeeDto: CreateCoffeeDto) {
return this.coffeesService.create(createCoffeeDto);
}
@Patch(":id")
update(@Param("id") id: string, @Body() updateCoffeeDto: UpdateCoffeeDto) {
return this.coffeesService.update(id, updateCoffeeDto);
}
@Delete(":id")
delete(@Param("id") id: string) {
return this.coffeesService.remove(id);
}
}
基于参数的管道
@Patch(":id")update(@Param("id") id: string, @Body() updateCoffeeDto: UpdateCoffeeDto) {
return this.coffeesService.update(id, updateCoffeeDto);
}
查看更新函数,有两个参数,资源"id"以及更新现有实体所需的"有效负载"
如果想将Pipe绑定到请求的Body而不是id参数,可以使用基于参数的管道
通过将ValidationPipe类引用直接传递给这里的@Body装饰器,可以让Nest只在这个参数上执行this particular pipe
@Patch(":id")update(@Param("id") id: string, @Body(ValidationPipe) updateCoffeeDto: UpdateCoffeeDto) {
return this.coffeesService.update(id, updateCoffeeDto);
}
使用过滤器捕获异常
通过创建ExceptionFilter负责捕获作为HttpException类实例的异常,并为它实现自定义相应逻辑
- 使用Nest CLI过滤器原理图生成过滤器类
nest g filter common/filters/127.0.0.1:3000/api/#/ - 启用新的Swagger CLI插件,打开nest-cli.json增加以下配置 "compilerOptions": {
- 重启后刷新127.0.0.1:3000/api/#/default/CoffeeController_create有了所需要的DTO
- 但是在Patch中仍无法正常显示DTO
- 打开update-coffee.dto.ts,从"@nestjs/swagger"中导出PartialType而不是从"@nestjs/mapped-types"中导出 import { PartialType } from "@nestjs/swagger";
- 刷新127.0.0.1:3000/api/#/default/CoffeeController_update正常
- yarn test:用于单元测试
- yarn test:cov:用于单元测试和收集测试覆盖率
- yarn test:e2e:用于端到端(End to End)测试
初始化的Swagger UI内容并不完整,例如POST请求中,没有标明参数
但是有一个专门的DOT类,代表该接口的输入参数
Nest提供了一个插件来增强TypeScript编译过程,减少需要创建的样板代码数量,从而解决该问题。
推荐:在需要覆盖插件提供的基本功能的任何地方添加特定的装饰器
"deleteOutDir": true,
"plugins": [
"@nestjs/swagger/plugin"
]
}
import { CreateCoffeeDto } from "./create-coffee.dto";
export class UpdateCoffeeDto extends PartialType(CreateCoffeeDto) {
}
在Swigger UI显示的DTO中,无法很好的看出参数的含义
通过在create-coffee.dto.ts中,通过@ApiProperty()注解给每个参数增加描述、举例等
import { IsString } from "class-validator";import { ApiProperty } from "@nestjs/swagger";
export class CreateCoffeeDto {
@ApiProperty({ description: "The name of a coffee" })
@IsString()
readonly name: string;
@ApiProperty({ description: "The brand of a coffee" })
@IsString()
readonly brand: string;
@ApiProperty({ description: "The flavor of a coffee", example: ["caramel", "chocolate"] })
@IsString({ each: true })
readonly flavors: string[];
}
刷新后可以显示
定义其他响应结果
通过@ApiResponse()注解,可以为路由定义不同的返回状态结果。
也可以通过专门的注解(@ApiForbiddenResponse等),返回描述信息
同样可以定义专门的装饰器进行复用,减少重复代码
// @ApiResponse({ status: 403, description: "Forbidden." })@ApiForbiddenResponse({ description: "Forbidden." })@Public()
@Get()
findAll(@Protocol("https") protocol: string, @Query() paginationQuery: PaginationQueryDto) {
console.log(protocol);
// const { limit, offset } = paginationQuery;
// console.log(123);
return this.coffeesService.findAll(paginationQuery);
}
使用标签(Tag)对标签进行分组,可以将相关的端点、API进行分组
通过在coffee.controller.ts中使用@ApiTags("coffee")注解装饰CoffeeController,可以进行分组
import { ApiTags } from "@nestjs/swagger";@ApiTags("coffee")
@Controller("coffee")
export class CoffeeController {}
Jest
对于NestJS中的单元测试,通常的做法是通过将.spec.ts文件保存在与它们测试的应用程序源代码文件相同的文件夹中
每个Controller、Provicer、Service等都应该有自己的专用测试文件
测试文件扩展名必须是*.spec.ts
端到端测试默认情况下通常位于专用的/test/目录中,端到端测试通常按照测试的"特性"或者"功能"分组到单独的文件中。
端到端测试文件扩展名必须是*.e2e-spec.ts
单元测试侧重于单个类和函数,端到端测试适合对整个系统进行高级验证
本文共计12477个文字,预计阅读时间需要50分钟。
说明+学习NestJS官方基础课程【中文版+NestJS Fundamentals Course】个人笔记+因为用不到测试,所以暂时学到P65+后续项目可能用不到MongoDB,后面的也还没看+基础结构+生成controller
说明
学习NestJS 官方基础课程个人笔记
因为用不到测试,所以暂时学到了P65
后续项目可能用不到MogoDB,后面的也就没看
基础结构
- 生成controller
nest g controller coffee - 生成service
nest g service coffee - 生成module
nest g module coffee - 生成entities
nest g class coffee/entities/coffee.entity --no-spec - 生成DTO
nest g class coffee/dto/create-coffee.dto --no-specDTO和Entity的区别,
- Entity可能带有ID,是查询数据时定义的接口,
- DTO是生成数据或更新时候用的
验证数据正确性
NextJS提供了ValidationPipe进行数据验证
ValidationPipe提供了对所有传入客户端有效负载强制执行验证规则的便捷方式
- 在main.ts中加入app.useGlobalPipes(new ValidationPipe());
import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();a
- 安装两个包yarn add class-validator class-transformer
export class CreateCoffeeDto {
@IsString()
readonly name: string;
@IsString()
readonly brand: string;
@IsString({ each: true })
readonly flavors: string[];
}
DTO代码抽离
PartialType:表示继承所有属性,但是所有属性都是可选的,相当于只验证正确性,不验证存在性
import { CreateCoffeeDto } from "./create-coffee.dto";
export class UpdateCoffeeDto extends PartialType(CreateCoffeeDto) {
}
配置参数白名单,进行参数过滤
在ValidationPipe中传入一个对象,其中包含键/值白名单:true
import { NestFactory } from "@nestjs/core";import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true
}));
await app.listen(3000);
}
bootstrap();
开启后,通过post上传参数,将自动过滤掉不需要的参数
如果开启forbidNonWhitelisted:true,即
import { NestFactory } from "@nestjs/core";import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 参数白名单
forbidNonWhitelisted: true // 上传白名单之外的参数,报错
}));
await app.listen(3000);
}
bootstrap();
如果上传不需要参数,会报错
instanceof
默认接受的参数instanceof dto是false
@Post()create(@Body() createCoffeeDto: CreateCoffeeDto) {
console.log(createCoffeeDto instanceof CreateCoffeeDto);
return this.coffeesService.create(createCoffeeDto);
}
通过在ValidationPipe配置transform:true,可以返回true
import { NestFactory } from "@nestjs/core";import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 参数白名单
forbidNonWhitelisted: true, // 上传白名单之外的参数,报错
transform: true
}));
await app.listen(3000);
}
bootstrap();
Docker配置
参考
DockerId:kaisarh
Email:hkzxh1104
password:hkzxh1104
使用Docker
services:
db:
image: postgres
restart: always
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD:
目前Docker Compose YAML文件中只列出了一项服务,但供将来参考
使用typeorm关联数据库
yarn add @nestjs/typeorm typeorm@2 pg
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { CoffeeModule } from "./coffee/coffee.module";
import { TypeOrmModule } from "@nestjs/typeorm";
@Module({
imports: [CoffeeModule, TypeOrmModule.forRoot({
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "pass123",
database: "postgres",
autoLoadEntities: true, // 有助于自动加载模块,而不是指定实体数组
synchronize: true // 同步,确保我们的TypeORM实体在每次运行应用程序时都会与数据库同步 生产环境设置为true,开发环境设置为false
})],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {
}
- 通过@Entity注解标注实体表
- 通过PrimaryGeneratedColumn标注自增主键
- 通过@Column标注行,可以通过设置options配置参数。例如nullable设置非空
在coffee.module.ts中进行导入imports: [TypeOrmModule.forFeature([CoffeeEntity])],
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CoffeeEntity } from "./entities/coffee.entity";
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([CoffeeEntity])],
controllers: [CoffeeController],
providers: [CoffeeService]
})
export class CoffeeModule {
}
通过使用forFeature()将TypeORM注册到此模块中
我们在主AppModule中使用了forRoot(),但我们只这样做了一次,注册实体时,所有其他模块都将使用forFeature()
在这里的forFeature()内部,传入一个实体数组,在咖啡例子中,只有一个咖啡实体
typeorm操作数据库
{
id: 1,
name: "Shipwreck Roast",
brand: "Buddy Brew",
flavors: ["chocolate", "vanilla"]
},
{
id: 2,
name: "Raw coconut Latte",
brand: "Lucky Coffee",
flavors: ["coconut", "vanilla"]
}
];
将数据表注入后,可以删除这部分数据,并通过与数据库交互直接操作数据库
import { HttpException, HttpStatus, Injectable, NotFoundException } from "@nestjs/common";import { CoffeeEntity } from "./entities/coffee.entity";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { CreateCoffeeDto } from "./dto/create-coffee.dto";
import { UpdateCoffeeDto } from "./dto/update-coffee.dto";
// 创建命令 nest g module coffee
@Injectable()
export class CoffeeService {
constructor(
@InjectRepository(CoffeeEntity)
private readonly coffeeEntityRepository: Repository<CoffeeEntity>
) {
}
findAll() {
return this.coffeeEntityRepository.find();
}
async findOne(id: string) {
// 抛出JS错误会返回服务器500
// throw "A random error";
const coffee = await this.coffeeEntityRepository.findOne(id);
// 错误处理,抛出异常
if (!coffee) {
//throw new HttpException(`Coffee #${id} not found`, HttpStatus.NOT_FOUND);
throw new NotFoundException(`Coffee #${id} not found!`);
} else {
return coffee;
}
}
create(createCoffeeDto: CreateCoffeeDto) {
const coffee = this.coffeeEntityRepository.create(createCoffeeDto);
return this.coffeeEntityRepository.save(coffee);
}
async update(id: string, updateCoffeeDto: UpdateCoffeeDto) {
// preload首先查看数据库中是否存在实体,存在更新实体中的所有值,不存在返回undefined
// 注意:preload只会查找并更新实体,不会更新数据库
const coffee = await this.coffeeEntityRepository.preload({
id: +id,
...updateCoffeeDto
});
if (!coffee) {
throw new NotFoundException(`Coffee #${id} not found`);
}
return this.coffeeEntityRepository.save(coffee);
}
async remove(id: string) {
const coffee = await this.findOne(id);
return this.coffeeEntityRepository.remove(coffee);
}
}
表之间关系
- 一对一:@OneToOne()
- 一对多:@OneToMany() 或者@ManyToOne()
- 多对多:@ManyToMany()
不同表之间建立关联
定义实体
@Entity()
export class FlavorEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
}
- 删除flavors的@Column()装饰器,并通过其他装饰器与FlavorEntity设置Relation
- 从typeorm中引入@JoinTable()装饰器,其可以指定关系的OWNER端,在这里是Coffee Entity
- 通过@ManyToMany在Coffee Entity中指定与Flavor Entity的关系
- 第一个参数指定type
- 第二个参数绑定与type中的哪个参数绑定关联
import { JoinTable } from "typeorm/browser";
import { Flavor } from "./flavor.entity";
@Entity()
// 默认 sql table === 'coffee'
// 可以在Entity('TABLE_NAME')进行指定
export class Coffee {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
brand: string;
@JoinTable()
@ManyToMany(
type => Flavor,
flavor => flavor.coffees)
flavors: string[];
}
- 通过@ManyToMany在Flavor Entity中指定与Coffee Entity的关系
import { Coffee } from "./coffee.entity";
@Entity()
export class Flavor {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(
type => Coffee,
coffee => coffee.flavors
)
coffees: Coffee[];
}
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor])],
controllers: [CoffeeController],
providers: [CoffeeService]
})
export class CoffeeModule {
}
- 关于relations的解释为:
- Indicates what relations of entity should be loaded (simplified left join form).
- 指示应加载的实体关系(简化的左联接形式)。
return this.coffeeRepository.find({
relations:['flavors']
});
}
async findOne(id: string) {
// 抛出JS错误会返回服务器500
// throw "A random error";
const coffee = await this.coffeeRepository.findOne(id,{
relations:['flavors']
});
// 错误处理,抛出异常
if (!coffee) {
//throw new HttpException(`Coffee #${id} not found`, HttpStatus.NOT_FOUND);
throw new NotFoundException(`Coffee #${id} not found!`);
} else {
return coffee;
}
}
级联插入
添加新的咖啡Coffee的时候,如果口味Flavor不存在?
import { Flavor } from "./flavor.entity";
@Entity()
// 默认 sql table === 'coffee'
// 可以在Entity('TABLE_NAME')进行指定
export class Coffee {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
brand: string;
@JoinTable()
@ManyToMany(
type => Flavor,
(Flavor) => Flavor.coffees,
{
cascade:true // ['insert']
})
flavors: string[];
}
- 将Flavor Repository注入到CoffeesService类中
@InjectRepository(Coffee)
private readonly coffeeRepository: Repository<Coffee>,
@InjectRepository(Flavor) // 將Flavor注入到coffeeService中
private readonly flavorRepository:Repository<Flavor>
) {
}
- 定义一个新的私有方法并将其命名为:preloadFlavorByName
const existingFlavor = await this.flavorRepository.findOne({name});
if(existingFlavor){
return existingFlavor;
}
this.flavorRepository.create({name});
}
- 调整create()方法
// 使用map遍历CreateCoffeeDto所中有风味,对不存在的数据进行创建
const flavors = await Promise.all(
createCoffeeDto.flavors.map(name => this.preloadFlavorByName(name))
);
const coffee = this.coffeeRepository.create({
...createCoffeeDto,
flavors
});
return this.coffeeRepository.save(coffee);
}
调整coffee.entity.ts中flavors的类型
import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from "typeorm";import { Flavor } from "./flavor.entity";
@Entity()
// 默认 sql table === 'coffee'
// 可以在Entity('TABLE_NAME')进行指定
export class Coffee {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
brand: string;
@JoinTable()
@ManyToMany(
type => Flavor,
(Flavor) => Flavor.coffees,
{
cascade:true // ['insert']
})
flavors: Flavor[]; // 将flavors的类型设置为Flavor
}
- 调整update()方法
// preload首先查看数据库中是否存在实体,存在更新实体中的所有值,不存在返回undefined
// 注意:preload只会查找并更新实体,不会更新数据库
const flavors =
updateCoffeeDto.flavors &&
(await Promise.all(
updateCoffeeDto.flavors.map(name => this.preloadFlavorByName(name))
));
const coffee = await this.coffeeRepository.preload({
id: +id,
...updateCoffeeDto,
flavors
});
if (!coffee) {
throw new NotFoundException(`Coffee #${id} not found`);
}
return this.coffeeRepository.save(coffee);
}
分页查询
nest g class common/dto/pagination-query.dto --no-spec
limit: number;
offset: number;
}
export class PaginationQueryDto {
@Type(() => Number)
limit: number;
@Type(() => Number)
offset: number;
}
这一步也可以通过在ValidationPipe中添加transformOptions对象,将enableImplicitConversion设置为true,在全局层面上启用隐式类型转换
import { NestFactory } from "@nestjs/core";import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 参数白名单
forbidNonWhitelisted: true, // 上传白名单之外的参数,报错
transform: true,
transformOptions: {
enableImplicitConversion: true
}
}));
await app.listen(3000);
}
bootstrap();
import { IsOptional } from "class-validator";
export class PaginationQueryDto {
@IsOptional()
@Type(() => Number)
limit: number;
@Type(() => Number)
@IsOptional()
offset: number;
}
import { IsOptional, IsPositive } from "class-validator";
export class PaginationQueryDto {
@IsPositive()
@IsOptional()
@Type(() => Number)
limit: number;
@IsPositive()
@IsOptional()
@Type(() => Number)
offset: number;
}
export class PaginationQueryDto {
@IsPositive()
@IsOptional()
limit: number;
@IsPositive()
@IsOptional()
offset: number;
}
findAll(@Query() paginationQuery: PaginationQueryDto) {
// const { limit, offset } = paginationQuery;
return this.coffeesService.findAll(paginationQuery);
}
const { limit, offset } = paginationQuery;
return this.coffeeRepository.find({
relations: ["flavors"],
skip: offset,
take: limit
});
}
事务
@Entity()
export class Event {
@PrimaryGeneratedColumn()
id: number;
@Column()
type: string;
@Column()
name: string;
// payload 是存储事件有效负载通用列
@Column("json")
payload: Record<string, any>;
}
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [CoffeeService]
})
export class CoffeeModule {
}
import { Flavor } from "./flavor.entity";
@Entity()
// 默认 sql table === 'coffee'
// 可以在Entity('TABLE_NAME')进行指定
export class Coffee {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
brand: string;
// 新增推荐属性
@Column({ default: 0 })
recommendations: number;
@JoinTable()
@ManyToMany(
type => Flavor,
(Flavor) => Flavor.coffees,
{
cascade: true // ['insert']
})
flavors: Flavor[]; // 将flavors的类型设置为Flavor
}
@InjectRepository(Coffee)
private readonly coffeeRepository: Repository<Coffee>,
@InjectRepository(Flavor) // 將Flavor注入到coffeeService中
private readonly flavorRepository: Repository<Flavor>,
// 引入Connection用来创建事务
private readonly connection: Connection
) {
}
const queryRunner = this.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
}
- 首先创建一个新的queryRunner
- 使用创建的queryRunner创建到数据库的新连接
- 建立连接后,可以开始交易过程
- 将整个事务包装在try / catch / finally中,以确保如果出现任何问题,catch可以回滚整个事务
- 事务是我们能够回滚和撤销发生的任何事情,以防出现问题
const queryRunner = this.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
coffee.recommendations++;
const recommendEvent = new Event();
recommendEvent.name = "recommend_coffee";
recommendEvent.type = "coffee";
recommendEvent.payload = { coffeeId: coffee.id };
await queryRunner.manager.save(coffee);
await queryRunner.manager.save(recommendEvent);
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
} finally {
await queryRunner.release();
}
}
- 在try中,增加coffee的推荐属性并创建一个新的推荐咖啡事件,使用查询运行器实体管理器来保存咖啡和事件实体
- 在catch语句中看到,如果出现任何问题,保存任一实体失败,通过回滚整个事务来防止数据库中的不一致
- 在finallye中,保证一切结束后释放或关闭queryRunner
缓存
- 使用@Index()装饰器在列上定义一个索引
@Column()
name: string;
- 列的复合索引,可以通过将@Index()装饰器应用在类本身,并在装饰器内传递一个列名数组作为参数
@Index(["name", "type"])
@Entity()
export class Event {
@PrimaryGeneratedColumn()
id: number;
@Column()
type: string;
@Index()
@Column()
name: string;
// payload 是存储事件有效负载通用列
@Column("json")
payload: Record<string, any>;
}
数据库迁移
数据库迁移提供了一种增量更新我们的数据库模式并使其与应用程序数据模型保持同步的方法,同时保留我们数据库中的现有数据。
To generate, run and revert migrations
生成、运行和恢复迁移
在创建新的迁移之前,我们需要创建一个新的TypeORM配置文件并正确连接我们的数据库
- 在项目的根目录中创建一个ormconfig.js文件
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "pass123",
database: "postgres",
entities: ["dist/**/*.entity.js"],
migrations: ["dist/migrations/*.js"],
cli:{
migrationsDir:'src/migrations'
}
};
这里的配置设置是我们从Docker Compose文件中使用的所有端口、密码等,还有一些额外的关键值用于让TypeORM迁移,知道我们的实体和迁移文件将在哪里
- 执行迁移命令,并将此迁移命名为:CoffeeRefactor
npx typeorm migration:create -n CoffeeRefactor - 该命令在/src/migrations目录中生成一个新的迁移文件
- 假设需要更改coffee.entity,将name更改为title
title: string;
- 对实体的更新会自动更新开发数据库,因为设置了synchronize: true,但是不会更新生产数据库,这是迁移非常方便的主要原因之一
- 更新name为title后,不仅会删除名称列,还会删除该列中的所有数据
- 只有在删除该列后,才会创建没有任何旧数据的新标题列
- 迁移帮助我们重命名现有列并维护我们以前的所有数据
- 打开迁移文件并增加迁移逻辑,让数据库知道需要进行更改
- 基础迁移文件都有一个up()和down()方法
export class CoffeeRefactor1653399205455 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}
- up()是只是需要更改的内容以及如何更改的内容
- down()是撤销或回滚任何这些更改的地方,万一出现问题,需要一个退出策略,帮助撤销一切,down()就可以保证我们的迁移回滚
- 在up()中新增更改表字段名称的数据库语句
'ALTER TABLE "coffee" RENAME COLUMN "name" TO "title"',
)
在这里可以执行所需的任何类型的数据库迁移,同时,必须为回滚迁移提供逻辑
- 在down()中新增回滚逻辑
'ALTER TABLE "coffee" RENAME COLUMN "title" TO "name"',
)
- 迁移文件完整代码
export class CoffeeRefactor1653399205455 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "coffee" RENAME COLUMN "name" TO "title"',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "coffee" RENAME COLUMN "title" TO "name"',
)
}
}
- 测试迁移
- 确保构建源代码,以便TypeORM CLI可以在/dist目录下找到身份和迁移文件
构建代码yarn run build - 构建完成后,生成dist目录
- 通过以下方式运行"迁移"命令类型:
- npx typeorm migration:run
- 再次执行
- 恢复更改
- npx typeorm migration:revert
- TypeORMCLI可以自动生成迁移,连接到数据库并将现有表与提供的实体定义进行比较,如果发现差异,TypeORM会生成一个新的迁移
- coffee.entity中新增description
description: string;
- 编译代码:yarn run build
- 输入命令,让TypeORM生成迁移,并将其命名为SchemaSync
- npx typeorm migration:generate -n SchemaSync
- 打开/src/migrations/中新生成的迁移文件,查看up()和down()
export class SchemaSync1653400611645 implements MigrationInterface {
name = 'SchemaSync1653400611645'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "coffee" ADD "description" character varying`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "coffee" DROP COLUMN "description"`);
}
}
- 执行迁移:npx typeorm migration:run
依赖注入
当我使用CoffeeService并将其注入到构造函数中时
constructor(private readonly coffeesService: CoffeeService) {}NextJS通过以下三件事实现:
此装饰器将CoffeeService类标记为Provider
这个请求高速Nest将提供程序注入到我们的控制器类中
)容器注册了这个容器
封装
- coffee-rating-module
import { CoffeeRatingService } from './coffee-rating.service';
@Module({
providers: [CoffeeRatingService]
})
export class CoffeeRatingModule {}
- coffee-rating-service
@Injectable()
export class CoffeeRatingService {}
- 因为属于不同模块,
- 所以在CoffeeRtaingModule中导入CoffeeModule
import { CoffeeRatingService } from "./coffee-rating.service";
import { CoffeeModule } from "../coffee/coffee.module";
@Module({
imports: [CoffeeModule],
providers: [CoffeeRatingService]
})
export class CoffeeRatingModule {
}
- 切换到CoffeeRatingService, 并使用基于构造函数的注入来添加CoffeeService
import { CoffeeService } from "../coffee/coffee.service";
@Injectable()
export class CoffeeRatingService {
constructor(private readonly coffeeService: CoffeeService) {
}
}
- 这样运行后会报错
- 原因:默认情况下,所有模块都封装了他们的提供者(Provider)如果想在另外一个模块中使用它们,必须明确地将他们定义为导出(exported),使它们成为该模块的公共API的一部分
- 解决:
- 在coffee.module.ts中将CoffeeService导出
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [CoffeeService],
exports: [CoffeeService]
})
export class CoffeeModule {
}
- 更改后可以正常运行
自定义提供程序
以下场景:
- 创建我们的提供者自定义实例,而不是让Nest实例化该类
- 在第二个依赖项中重用现有类
- 用模拟版本覆盖一个类进行测试
- 使用策略模式,提供一个抽象类并根据不同条件交换实际实现(或要使用的实际类)
通过Nest定义自定义提供程序来处理这些场景。providers数组形式只是简写,实际上只是提供TOKEN并在该TOKEN的位置提供"要注入的内容"的简写版本。完整写法为:
providers:[{
provider: CoffeesService,
useClass: CoffeesService
}
]
Nest提供了不同方法进行自定义提供者。
假设在Nest容器中添加一个外部库,或者用Mock\{}对象替代服务的真实实现。
例如:将CoffeeService替换为自定义Provider并使用useValue语法,当在程序中注入CoffeeService时,每当CoffeeService TOKEN被解析时,他将指向新的MockCoffeeService,
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
class MockCoffeeService {
}
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [{ provide: CoffeeService, useValue: new MockCoffeeService() }],
exports: [CoffeeService]
})
export class CoffeeModule {
}
因此,可以通过使用useValue
重命名提供者令牌
在之前,均是使用类名作为Provider tokens,provider tokens是我们传递给provider属性的任何内容,通过使用更灵活的字符串或符号作为依赖注入令牌
例如提供一个字符串值标记"COFFEE_BRANDS",通过useValue将值设置为字符串数组
import { Module } from "@nestjs/common";import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
class MockCoffeeService {
}
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [
CoffeeService,
{ provide: "COFFEE_BRANDS", useValue: ["buddy brew", "nescafe"] }
],
exports: [CoffeeService]
})
export class CoffeeModule {
}
使用@Inject()装饰器,并将需要查找的令牌作为参数进行赋值
constructor(@InjectRepository(Coffee)
private readonly coffeeRepository: Repository<Coffee>,
@InjectRepository(Flavor) // 將Flavor注入到coffeeService中
private readonly flavorRepository: Repository<Flavor>,
// 引入Connection用来创建事务
private readonly connection: Connection,
@Inject("COFFEE_BRANDS") coffeeBrands: string[]
) {
}
这样就可以使用COFFEE_BRANDS并访问我们传递给此提供程序的值数组
最好在一个单独的CONSTANT常量文件夹中定义TOKEN并导出、导入使用
- 在coffee目录下新建coffee.constants.ts
- 在Module和Service中,引入常量并使用
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
import { COFFEE_BRANDS } from "./coffee.constants";
class MockCoffeeService {
}
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [
CoffeeService,
{ provide: COFFEE_BRANDS, useValue: ["buddy brew", "nescafe"] }
],
exports: [CoffeeService]
})
export class CoffeeModule {
}
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
import { COFFEE_BRANDS } from "./coffee.constants";
class ConfigService {
}
class DevelopmentConfigService {
}
class ProductionConfigService {
}
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [
CoffeeService,
{
provide: ConfigService,
useClass:
process.env.NODE_ENV === "development" ?
DevelopmentConfigService :
ProductionConfigService
},
{ provide: COFFEE_BRANDS, useValue: ["buddy brew", "nescafe"] }
],
exports: [CoffeeService]
})
export class CoffeeModule {
}
useFactory的返回值将被提供者(provider)使用。
新的更现实的例子,并在其中注入一些提供程序:
定义一个随机提供者,并确保将其注册为提供者(@Injectable())
@Injectable()export class CoffeeBrandsFactory {
create() {
// do something
return ["buddy brew", "nescafe"];
}
}
更新现有的COFFEE_BRANDS提供程序以使用CoffeeBrandsFactory,新增一个名为inject的属性
inject本身接收一个提供者(provider)数组,这些提供者被传递到useFactory函数中后可以随意使用,返回需要的值
import { Injectable, Module } from "@nestjs/common";import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
import { COFFEE_BRANDS } from "./coffee.constants";
@Injectable()
export class CoffeeBrandsFactory {
create() {
// do something
return ["buddy brew", "nescafe"];
}
}
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [
CoffeeService,
CoffeeBrandsFactory,
{
provide: COFFEE_BRANDS,
useFactory: (brandsFactory: CoffeeBrandsFactory) => brandsFactory.create(),
inject: [CoffeeBrandsFactory]
}
],
exports: [CoffeeService]
})
export class CoffeeModule {
}
通过使用Promise,将async/await与useFactory语法结合使用,可以实现异步(比如数据库未连接不接受请求)
import { Injectable, Module } from "@nestjs/common";import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
import { COFFEE_BRANDS } from "./coffee.constants";
import { Connection } from "typeorm";
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [
CoffeeService,
{
provide: COFFEE_BRANDS,
useFactory: async (connection: Connection): Promise<string[]> => {
// const coffeeBrands = await connection.query('SELECT *** ...');
const coffeeBrands = await Promise.resolve(["buddy brew", "nescafe"]);
console.log("[!] Async Factory");
return coffeeBrands;
},
inject: [Connection]
}
],
exports: [CoffeeService]
})
export class CoffeeModule {
}
动态模块
有时在使用模块时需要更多的灵活性,例如:静态模块不能由使用它们的模块配置其Provider
比如:有一个通用模块,该模块需要在不同情况下表现不同
动态模块需要一些配置才可以被消费者使用
测试
nest g mo database
import { createConnection } from "typeorm";
@Module({
providers: [
{
provide: "CONNECTION",
useValue: createConnection({
type: "postgres",
host: "localhost",
port: 5432
})
}
]
})
export class DatabaseModule {
}
如果另一个应用程序想要使用这个模块但是需要使用不同的端口怎么办?
通过使用Nest的动态模块功能,可以让消费模块使用API来控制导入时如自定义DatabaseModule
- 在DatabaseModule上定义一个名为register()的静态方法
- register()可以接收消费模块传递过来的参数
- register()返回DynamicModule类型结果,它与典型的@Module()具有基本相同的接口,但需要传递一个module属性,也就是当前模块本身
- 通过register(),可以将接收的参数用于创建数据库连接中
import { ConnectionOptions, createConnection } from "typeorm";
@Module({})
export class DatabaseModule {
static register(options: ConnectionOptions): DynamicModule {
return {
module: DatabaseModule,
providers: [
{
provide: "CONNECTION",
useValue: createConnection(options)
}
]
};
}
}
使用方法如下:
import { Module } from "@nestjs/common";import { CoffeeRatingService } from "./coffee-rating.service";
import { CoffeeModule } from "../coffee/coffee.module";
import { DatabaseModule } from "../database/database.module";
@Module({
imports: [DatabaseModule.register({
type: "postgres",
host: "localhost",
password: "password",
port: 5432
}), CoffeeModule],
providers: [CoffeeRatingService]
})
export class CoffeeRatingModule {
}
服务提供者的Scope
SpringBoot中提供了Scope注解来指明Bean的作用域,NestJs也提供了类似的@Scope()装饰器:
scope名称
说明
SINGLETON
单例模式,整个应用内只存在一份实例
REQUEST
每个请求初始化一次
TRANSIENT
每次注入都会实例化
Config Module
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { CoffeeModule } from "./coffee/coffee.module";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CoffeeRatingModule } from "./coffee-rating/coffee-rating.module";
import { DatabaseModule } from "./database/database.module";
import { ConfigModule } from "@nestjs/config";
@Module({
imports: [ConfigModule.forRoot(), CoffeeModule, TypeOrmModule.forRoot({
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "pass123",
database: "postgres",
autoLoadEntities: true, // 有助于自动加载模块,而不是指定实体数组
synchronize: true // 同步,确保我们的TypeORM实体在每次运行应用程序时都会与数据库同步 生产环境设置为true,开发环境设置为false
}), CoffeeRatingModule, DatabaseModule],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {
}
DATABASE_PASSWORD=pass123
DATABASE_NAME=postgres
DATABASE_PORT=5432
DATABASE_HOST=localhost
配置均与数据库配置相关,来此docker-compose
打开.gitignore,增加以下行
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { CoffeeModule } from "./coffee/coffee.module";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CoffeeRatingModule } from "./coffee-rating/coffee-rating.module";
import { DatabaseModule } from "./database/database.module";
import { ConfigModule } from "@nestjs/config";
@Module({
imports: [ConfigModule.forRoot(), CoffeeModule, TypeOrmModule.forRoot({
type: "postgres",
host: process.env.DATABASE_HOST,
port: +process.env.DATABASE_PORT,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
autoLoadEntities: true, // 有助于自动加载模块,而不是指定实体数组
synchronize: true // 同步,确保我们的TypeORM实体在每次运行应用程序时都会与数据库同步 生产环境设置为true,开发环境设置为false
}), CoffeeRatingModule, DatabaseModule],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {
}
envFilePath: ".environment"
}),
除了传递字符串值,也可以传递字符串数组来为.env文件指定多个路径
如果在多个文件中找到相同变量,优先使用第一个匹配文件中的变量
ignoreEnvFile: true
}),
- 安装:yarn add @hapi/joi
- 安装types:yarn add -D @types/hapi__joi
- 定义验证模式。在ConfigModule.forRoot()方法内部,通过validationSchema确保以正确的格式传入某些环境变量
validationSchema: Joi.object({
DATABASE_HOST: Joi.required(),
DATABASE_PORT: Joi.number().default(5432)
})
}),
完整配置:
import { Module } from "@nestjs/common";import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { CoffeeModule } from "./coffee/coffee.module";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CoffeeRatingModule } from "./coffee-rating/coffee-rating.module";
import { DatabaseModule } from "./database/database.module";
import { ConfigModule } from "@nestjs/config";
import * as Joi from "@hapi/joi";
@Module({
imports: [
ConfigModule.forRoot({
validationSchema: Joi.object({
DATABASE_HOST: Joi.required(),
DATABASE_PORT: Joi.number().default(5432)
})
}),
CoffeeModule, TypeOrmModule.forRoot({
type: "postgres",
host: process.env.DATABASE_HOST,
port: +process.env.DATABASE_PORT,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
autoLoadEntities: true, // 有助于自动加载模块,而不是指定实体数组
synchronize: true // 同步,确保我们的TypeORM实体在每次运行应用程序时都会与数据库同步 生产环境设置为true,开发环境设置为false
}), CoffeeRatingModule, DatabaseModule],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {
}
- 在coffee.module中导入ConfigModule。
我们在主AppModule中使用了forRoot()方法,其他地方不需要做任何事
import { CoffeeController } from "./coffee.controller";
import { CoffeeService } from "./coffee.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Coffee } from "./entities/coffee.entity";
import { Flavor } from "./entities/flavor.entity";
import { Event } from "../events/entities/event.entity";
import { COFFEE_BRANDS } from "./coffee.constants";
import { Connection } from "typeorm";
import { ConfigModule } from "@nestjs/config";
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event]), ConfigModule],
controllers: [CoffeeController],
providers: [
CoffeeService,
{
provide: COFFEE_BRANDS,
useFactory: async (connection: Connection): Promise<string[]> => {
// const coffeeBrands = await connection.query('SELECT *** ...');
const coffeeBrands = await Promise.resolve(["buddy brew", "nescafe"]);
console.log("[!] Async Factory");
return coffeeBrands;
},
inject: [Connection]
}
],
exports: [CoffeeService]
})
export class CoffeeModule {
}
- 在CoffeeService中注入并通过get()方法获取参数
constructor(
@InjectRepository(Coffee)
private readonly coffeeRepository: Repository<Coffee>,
@InjectRepository(Flavor) // 將Flavor注入到coffeeService中
private readonly flavorRepository: Repository<Flavor>,
// 引入Connection用来创建事务
private readonly connection: Connection,
@Inject(COFFEE_BRANDS) coffeeBrands: string[],
private readonly configService: ConfigService
) {
const databaseHost = this.configService.get<string>("DATABASE_HOST");
console.log(databaseHost);
}
get()可以接收第二个参数,设置默认值,如果获取不到值则取默认值
const databaseHost = this.configService.get<string>("DATABASE_HOSTa", "demo");配置文件
通过配置文件对不同配置进行管理与使用
environment: process.env.NODE_ENV || "development",
database: {
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT, 10) | 5432
}
})
通过工厂函数导出配置,包括环境和数据库,主机和端口通过env类指定
- 配置:在app.module.ts中ConfigModule.forRoot传入一个一个load新属性,它接收一个配置工厂数组
load: [appConfig]
}),
- 获取:在coffee.service.ts中,通过get()方法获取配置的时候,不需要使用DATABASE_HOST获取,直接使用database.host即可
随着项目增长,配置文件增多,可能需要位于多个不同目录的"特定于功能"的配置文件
随着拥有越来越多的配置键,使用非类型化的configService.get()方法获取所有配置值很容易出错,由于必须使用"点表示法"(即a.b)来检索嵌套项
为了防止这种情况,结合两项技术:配置命名空间和部分注册以进行验证配置。
- 新建src/config/coffee.config.ts,通过registerAs()函数可以在命名空间内定义一个token,也就是第一个参数
export default registerAs("coffee", () => ({
foo: "bar"
}));
- 在coffee.module.ts中使用ConfigModule.forFeature()注册这个coffeeConfig,也就是部分配准
TypeOrmModule.forFeature([Coffee, Flavor, Event]),
ConfigModule.forFeature(coffeeConfig)
],
- 在CoffeeService中通过get()获取配置
console.log(coffeeConfig);
也可以通过点语法获取对应值
const coffeeConfig = this.configService.get("coffee");const coffeeConfigFoo = this.configService.get("coffee.foo");
console.log(coffeeConfig);
console.log(coffeeConfigFoo);
private readonly coffeeConfiguration: ConfigType<typeof coffeeConfig>
每个命名空间配置都暴露了一个Token也就是key属性,可以使用该属性将整个对象注入到Nest容器中注册的任何类
ConfigType是一个开箱即用的辅助类型,推断函数的返回类型
console.log(coffeeConfiguration.foo);可以直接通过该对象获取配置,甚至有强类型的好处
异步import
目前app.module.ts中配置如下
import { Module } from "@nestjs/common";import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { CoffeeModule } from "./coffee/coffee.module";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CoffeeRatingModule } from "./coffee-rating/coffee-rating.module";
import { DatabaseModule } from "./database/database.module";
import { ConfigModule } from "@nestjs/config";
import * as Joi from "@hapi/joi";
import appConfig from "./config/app.config";
@Module({
imports: [
// ConfigModule.forRoot({
// validationSchema: Joi.object({
// DATABASE_HOST: Joi.required(),
// DATABASE_PORT: Joi.number().default(5432)
// })
// }),
ConfigModule.forRoot({ load: [appConfig] }),
CoffeeModule,
TypeOrmModule.forRoot({
type: "postgres",
host: process.env.DATABASE_HOST,
port: +process.env.DATABASE_PORT,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
autoLoadEntities: true, // 有助于自动加载模块,而不是指定实体数组
synchronize: true // 同步,确保我们的TypeORM实体在每次运行应用程序时都会与数据库同步 生产环境设置为true,开发环境设置为false
}),
CoffeeRatingModule,
DatabaseModule
],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {
}
使用process.env的配置是在加载环境配置ConfigModule.forRoot({load: [appConfig]})之后的,如果在之前使用,会报错
通过异步加载可以解决,使用forRootAsync结合工厂函数进行配置
import { Module } from "@nestjs/common";import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { CoffeeModule } from "./coffee/coffee.module";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CoffeeRatingModule } from "./coffee-rating/coffee-rating.module";
import { DatabaseModule } from "./database/database.module";
import { ConfigModule } from "@nestjs/config";
import * as Joi from "@hapi/joi";
import appConfig from "./config/app.config";
@Module({
imports: [
// ConfigModule.forRoot({
// validationSchema: Joi.object({
// DATABASE_HOST: Joi.required(),
// DATABASE_PORT: Joi.number().default(5432)
// })
// }),
TypeOrmModule.forRootAsync({
useFactory: () => ({
type: "postgres",
host: process.env.DATABASE_HOST,
port: +process.env.DATABASE_PORT,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
autoLoadEntities: true, // 有助于自动加载模块,而不是指定实体数组
synchronize: true // 同步,确保我们的TypeORM实体在每次运行应用程序时都会与数据库同步 生产环境设置为true,开发环境设置为false
})
}),
ConfigModule.forRoot({ load: [appConfig] }),
CoffeeModule,
CoffeeRatingModule,
DatabaseModule
],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {
}
异常过滤器、管道、守卫、拦截器
- 转换:将输入数据转换为期望的输出
- 验证:评估输入数据,有效则通过管道,无效抛出异常
- 在方法执行之前或之后绑定额外的逻辑
- 转换方法返回的结果
- 扩展基本方法行为
- 完全覆盖方法
例如:处理诸如"缓存响应"之类的事情
如何将上述四种构建块绑定到我们的应用程序?基本上有三种不同的绑定方式:过滤器、守卫和拦截器绑定到路由处理程序,管道特定(仅适用于管道)
嵌套构建块可以是:
- "全局"范围
- "控制器"范围
- "方法"范围
- 额外的第4个"参数"范围:仅适用于管道
在main.ts中,通过ValidationPipe设置全局管道
import { NestFactory } from "@nestjs/core";import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 参数白名单
forbidNonWhitelisted: true, // 上传白名单之外的参数,报错
transform: true,
transformOptions: {
enableImplicitConversion: true
}
}));
await app.listen(3000);
}
bootstrap();
但是在这里设置无法注入任何依赖,因此可以在app.module.ts中通过provider进行设置并
定义一个名为APP_PIPEprovider的东西,以这种方式提供ValidationPipe,可以在AppModule的范围内实例化ValidationPipe并在创建后将其注册为全局管道。
每个其他构建块功能也有类似的标记
import { APP_PIPE, APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from "@nestjs/core";如何设置非全局,例如将ValidationPipe绑定到仅在CoffeeController中定义的每个路由处理程序
在app.controller.ts中使用@UsePipes装饰器绑定单个管道或用,分隔的管道列表
其他相同
UsePipes, UseFilters, UseGuards, UseInterceptors,import {Controller,
Get,
Post,
Body,
Param,
HttpCode,
HttpStatus,
Res,
Patch,
Delete,
Query,
UsePipes, UseFilters, UseGuards, UseInterceptors, ValidationPipe
} from "@nestjs/common";
import { CoffeeService } from "./coffee.service";
import { CreateCoffeeDto } from "./dto/create-coffee.dto";
import { UpdateCoffeeDto } from "./dto/update-coffee.dto";
import { PaginationQueryDto } from "../common/dto/pagination-query.dto";
class demo {
canActivate(context) {
return true;
}
}
// 创建命令 nest g controller coffee
interface PostHello {
name: string,
id: number | string
}
@UsePipes(ValidationPipe)
@Controller("coffee")
export class CoffeeController {
constructor(private readonly coffeesService: CoffeeService) {
}
@UseGuards(demo)
@Get()
findAll(@Query() paginationQuery: PaginationQueryDto) {
// const { limit, offset } = paginationQuery;
console.log(123);
return this.coffeesService.findAll(paginationQuery);
}
@Get(":id")
findOne(@Param("id") id: string) {
return this.coffeesService.findOne(id);
}
@Post()
create(@Body() createCoffeeDto: CreateCoffeeDto) {
return this.coffeesService.create(createCoffeeDto);
}
@Patch(":id")
update(@Param("id") id: string, @Body() updateCoffeeDto: UpdateCoffeeDto) {
return this.coffeesService.update(id, updateCoffeeDto);
}
@Delete(":id")
delete(@Param("id") id: string) {
return this.coffeesService.remove(id);
}
}
基于参数的管道
@Patch(":id")update(@Param("id") id: string, @Body() updateCoffeeDto: UpdateCoffeeDto) {
return this.coffeesService.update(id, updateCoffeeDto);
}
查看更新函数,有两个参数,资源"id"以及更新现有实体所需的"有效负载"
如果想将Pipe绑定到请求的Body而不是id参数,可以使用基于参数的管道
通过将ValidationPipe类引用直接传递给这里的@Body装饰器,可以让Nest只在这个参数上执行this particular pipe
@Patch(":id")update(@Param("id") id: string, @Body(ValidationPipe) updateCoffeeDto: UpdateCoffeeDto) {
return this.coffeesService.update(id, updateCoffeeDto);
}
使用过滤器捕获异常
通过创建ExceptionFilter负责捕获作为HttpException类实例的异常,并为它实现自定义相应逻辑
- 使用Nest CLI过滤器原理图生成过滤器类
nest g filter common/filters/127.0.0.1:3000/api/#/ - 启用新的Swagger CLI插件,打开nest-cli.json增加以下配置 "compilerOptions": {
- 重启后刷新127.0.0.1:3000/api/#/default/CoffeeController_create有了所需要的DTO
- 但是在Patch中仍无法正常显示DTO
- 打开update-coffee.dto.ts,从"@nestjs/swagger"中导出PartialType而不是从"@nestjs/mapped-types"中导出 import { PartialType } from "@nestjs/swagger";
- 刷新127.0.0.1:3000/api/#/default/CoffeeController_update正常
- yarn test:用于单元测试
- yarn test:cov:用于单元测试和收集测试覆盖率
- yarn test:e2e:用于端到端(End to End)测试
初始化的Swagger UI内容并不完整,例如POST请求中,没有标明参数
但是有一个专门的DOT类,代表该接口的输入参数
Nest提供了一个插件来增强TypeScript编译过程,减少需要创建的样板代码数量,从而解决该问题。
推荐:在需要覆盖插件提供的基本功能的任何地方添加特定的装饰器
"deleteOutDir": true,
"plugins": [
"@nestjs/swagger/plugin"
]
}
import { CreateCoffeeDto } from "./create-coffee.dto";
export class UpdateCoffeeDto extends PartialType(CreateCoffeeDto) {
}
在Swigger UI显示的DTO中,无法很好的看出参数的含义
通过在create-coffee.dto.ts中,通过@ApiProperty()注解给每个参数增加描述、举例等
import { IsString } from "class-validator";import { ApiProperty } from "@nestjs/swagger";
export class CreateCoffeeDto {
@ApiProperty({ description: "The name of a coffee" })
@IsString()
readonly name: string;
@ApiProperty({ description: "The brand of a coffee" })
@IsString()
readonly brand: string;
@ApiProperty({ description: "The flavor of a coffee", example: ["caramel", "chocolate"] })
@IsString({ each: true })
readonly flavors: string[];
}
刷新后可以显示
定义其他响应结果
通过@ApiResponse()注解,可以为路由定义不同的返回状态结果。
也可以通过专门的注解(@ApiForbiddenResponse等),返回描述信息
同样可以定义专门的装饰器进行复用,减少重复代码
// @ApiResponse({ status: 403, description: "Forbidden." })@ApiForbiddenResponse({ description: "Forbidden." })@Public()
@Get()
findAll(@Protocol("https") protocol: string, @Query() paginationQuery: PaginationQueryDto) {
console.log(protocol);
// const { limit, offset } = paginationQuery;
// console.log(123);
return this.coffeesService.findAll(paginationQuery);
}
使用标签(Tag)对标签进行分组,可以将相关的端点、API进行分组
通过在coffee.controller.ts中使用@ApiTags("coffee")注解装饰CoffeeController,可以进行分组
import { ApiTags } from "@nestjs/swagger";@ApiTags("coffee")
@Controller("coffee")
export class CoffeeController {}
Jest
对于NestJS中的单元测试,通常的做法是通过将.spec.ts文件保存在与它们测试的应用程序源代码文件相同的文件夹中
每个Controller、Provicer、Service等都应该有自己的专用测试文件
测试文件扩展名必须是*.spec.ts
端到端测试默认情况下通常位于专用的/test/目录中,端到端测试通常按照测试的"特性"或者"功能"分组到单独的文件中。
端到端测试文件扩展名必须是*.e2e-spec.ts
单元测试侧重于单个类和函数,端到端测试适合对整个系统进行高级验证

