说明
学习NestJS 官方基础课程【中英字幕 NestJS Fundamentals Course】个人笔记
因为用不到测试,所以暂时学到了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提供了不同方法进行自定义提供者。【useValue、useClass】
假设在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/http-exception
@Catch()
export class HttpExceptionFilter<T> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {}
}
- 顶部的@Catch()装饰器将所需的元数据绑定到ExceptionFilter,这个@Catch装饰器可以采用单个参数或逗号分隔的列表,如果需要,允许一次为多种类型的异常设置过滤器
- 因为要处理所有属于HttpException实例的异常,所以在@Catch()中增加HttpException,同时将T泛型继承HttpException
@Catch(HttpException)
export class HttpExceptionFilter<T extends HttpException> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {
}
}
- 通过上面设置,可以实现自定义响应逻辑,为此,需要访问底层平台的Response对象,以便操纵或转换它并继续发送响应。
- 通过调用ArgumentsHost的实例host的switchToHttp()方法,可以能够访问到请求或响应对象。
- 调用context的getResponse()方法,可以返回底层平台(默认Express)的响应。
- 使用exception对象,提取两个参数:statusCode和body
const exceptionResponse = exception.getResponse();
const error =
typeof response === "string"
? { message: exceptionResponse } :
(exceptionResponse as Object);
- 设置响应
...error,
timestamp: new Date().toISOString()
});
完整的异常处理
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from "@nestjs/common";import { Response } from "express";
@Catch(HttpException)
export class HttpExceptionFilter<T extends HttpException> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {
const context = host.switchToHttp();
const response = context.getResponse<Response>();
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
const error =
typeof response === "string"
? { message: exceptionResponse } :
(exceptionResponse as Object);
response.status(status).json({
...error,
timestamp: new Date().toISOString()
});
}
}
- 将全局ExceptionFilter绑定到应用程序上
在main.ts中,通过app.useGlobalFilters进行绑定
import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
import { HttpExceptionFilter } from "./common/filters/http-exception.filter";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 参数白名单
forbidNonWhitelisted: true, // 上传白名单之外的参数,报错
transform: true,
transformOptions: {
enableImplicitConversion: true
}
}));
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
路由守卫
Guards的最佳用例之一:身份验证和授权
例如:实现一个Guard,它提取和验证一个Token,并使用提取的信息来确定请求是否可以继续
本例子实现两件事:
通过Nest CLI生成一个Guard类
nest g guard common/guards/api-key
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';import { Observable } from 'rxjs';
@Injectable()
export class ApiKeyGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}
Guard类的关键是实现canActivate()方法,返回true或者false来判断是否通过
- 全局绑定
在main.ts中通过app.useGlobalGuards(new ApiKeyGuard());进行绑定
import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
import { HttpExceptionFilter } from "./common/filters/http-exception.filter";
import { ApiKeyGuard } from "./common/guards/api-key.guard";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 参数白名单
forbidNonWhitelisted: true, // 上传白名单之外的参数,报错
transform: true,
transformOptions: {
enableImplicitConversion: true
}
}));
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalGuards(new ApiKeyGuard());
await app.listen(3000);
}
bootstrap();
为了实现"验证每个请求中应该存在API_KEY,且仅存在非公共路由上"
DATABASE_PASSWORD=pass123
DATABASE_NAME=postgres
DATABASE_PORT=5432
DATABASE_HOST=localhost
API_KEY=3f4a1a66501692601596e772d3db1c97bb22970c7b08e6b588ccb393baab7e3f
完整guard
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";import { Observable } from "rxjs";
import { Request } from "express";
@Injectable()
export class ApiKeyGuard implements CanActivate {
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
const request = context
.switchToHttp()
.getRequest<Request>();
const authHeader = request.header("Authorization");
return authHeader === process.env.API_KEY;
}
}
上述操作实现了访问路由时验证是否存在API令牌,但仍未检测正在访问的路由是否被声明为公共
通过自定义元数据可以以声明方式制定程序中哪些端点是公共的,或者希望与控制器或路由一起存储的任何数据
Nest提供了通过@SetMetadata装饰器将自定义元数据附加到路由处理程序的能力,使用方式如下:
@Get@SetMetadata('key', 'value')
getHello(): string{
return 'Hello World!';
}
@SetMetadata接收两个参数
- 第一个参数是将用作查找键的元数据"键"
- 第二个参数是可以是任何类型的元数据"值"
这是我们为这个特定键放置我们想要存储的任何值的地方
例如:在CoffeeController中,对findAll()方法增加元数据
@SetMetadata('isPublic',true)@Get()
findAll(@Query() paginationQuery: PaginationQueryDto) {
// const { limit, offset } = paginationQuery;
console.log(123);
return this.coffeesService.findAll(paginationQuery);
}
这是最简单的方法,但不是最佳实践。
理想情况下,应该创建自己的装饰器来实现相同的目标。
- 一是作为元数据"key"
- 二是新装饰器本身,称之为@Public
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY,true);
@UsePipes(ValidationPipe)
@Controller("coffee")
export class CoffeeController {
constructor(private readonly coffeesService: CoffeeService) {
}
@Public()
@Get()
findAll(@Query() paginationQuery: PaginationQueryDto) {
// const { limit, offset } = paginationQuery;
console.log(123);
return this.coffeesService.findAll(paginationQuery);
}
定义好公共路由后,改造Guard。
通过Reflector的实例对象可以访问当前上下文的元数据。
import { Observable } from "rxjs";
import { Request } from "express";
import { Reflector } from "@nestjs/core";
@Injectable()
export class ApiKeyGuard implements CanActivate {
constructor(private readonly reflector:Reflector) {
}
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
const request = context
.switchToHttp()
.getRequest<Request>();
const authHeader = request.header("Authorization");
return authHeader === process.env.API_KEY;
}
}
- key
- 目标对象上下文,这里使用context.getHandler()
- 如果需要从Class Level中检索元数据,这里调用content.getClass()
import { Observable } from "rxjs";
import { Request } from "express";
import { Reflector } from "@nestjs/core";
import { IS_PUBLIC_KEY } from "../decorators/public.decorator";
import { ConfigService } from "@nestjs/config";
@Injectable()
export class ApiKeyGuard implements CanActivate {
constructor(
private readonly reflector:Reflector,
private readonly configService:ConfigService) {
}
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
const isPublic = this.reflector.get(IS_PUBLIC_KEY, context.getHandler());
if(isPublic){
return true;
}
const request = context
.switchToHttp()
.getRequest<Request>();
const authHeader = request.header("Authorization");
return authHeader === this.configService.get('API_KEy');
}
}
这时候会报错
原因:在Guard内部使用了依赖注入,并在main.ts中实例化
依赖其他类的全局守卫必须在@Module上下文中注册,解决问题并将这个守卫添加到module中
import { APP_GUARD } from "@nestjs/core";
import { ApiKeyGuard } from "./guards/api-key.guard";
import { ConfigModule } from "@nestjs/config";
@Module({
imports:[ConfigModule],
providers:[
{
provide: APP_GUARD,
useClass: ApiKeyGuard
}
]
})
export class CommonModule {}
import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
import { HttpExceptionFilter } from "./common/filters/http-exception.filter";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 参数白名单
forbidNonWhitelisted: true, // 上传白名单之外的参数,报错
transform: true,
transformOptions: {
enableImplicitConversion: true
}
}));
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
拦截器
例子:希望接口的响应总是位于"data" property 数据属性中,创建一个新的拦截器(WrapResponseInterceptor)处理该问题。该拦截器处理所有传入的请求,并自动包装数据
- 通过Nest CLI自动生成
nest g interceptor common/interceptors/wrap-response
import { Observable } from 'rxjs';
@Injectable()
export class WrapResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle();
}
}
- Interceptor是一个带@Injectable()装饰器的类
- 所有拦截器都应该实现从'@nestjs/common'导出的NestInterceptor接口
- NestInterceptor接口要求在类中提供intercept()方法
- intercept()方法应该从RxJS库返回一个Observable
- CallHandler接口实现了handle()方法,使用该方法在拦截器中调用路由处理程序方法
- 如果没有在拦截方法的实现中调用handle()方法,路由处理程序不会被执行
- intercept()方法有效地包装了请求、响应流,允许在执行最终路由处理程序之前和之后实现自定义逻辑
import { Observable } from 'rxjs';
import {tap,map} from 'rxjs/operators'
@Injectable()
export class WrapResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('before')
// return next.handle().pipe(tap(data=>console.log('adter...',data)));
return next.handle().pipe(map(data=>({data})));
}
}
将这个拦截器全局绑定在应用程序上
在main.ts中,使用app.useGlobalInterceptors(new WrapResponseInterceptor())进行绑定
import { NestFactory } from "@nestjs/core";import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
import { HttpExceptionFilter } from "./common/filters/http-exception.filter";
import { WrapResponseInterceptor } from "./common/interceptors/wrap-response.interceptor";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 参数白名单
forbidNonWhitelisted: true, // 上传白名单之外的参数,报错
transform: true,
transformOptions: {
enableImplicitConversion: true
}
}));
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new WrapResponseInterceptor())
await app.listen(3000);
}
bootstrap();
在拦截器中,通过return可以返回处理之后的数据。
通过return next.handle().pipe(map(data=>({data})));可以将所有的返回结果包装在data中
拦截器处理超时
生成拦截器
nest g interceptor common/interceptors/timeout
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';import { Observable,timeout } from 'rxjs';
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(timeout(3000));
}
}
在main.ts中引入
import { NestFactory } from "@nestjs/core";import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
import { HttpExceptionFilter } from "./common/filters/http-exception.filter";
import { WrapResponseInterceptor } from "./common/interceptors/wrap-response.interceptor";
import { TimeoutInterceptor } from "./common/interceptors/timeout.interceptor";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 参数白名单
forbidNonWhitelisted: true, // 上传白名单之外的参数,报错
transform: true,
transformOptions: {
enableImplicitConversion: true
}
}));
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(
new WrapResponseInterceptor(),
new TimeoutInterceptor()
)
await app.listen(3000);
}
bootstrap();
在findAll()中通过setTimeout模拟时长
async findAll(paginationQuery: PaginationQueryDto) {await new Promise(resolve => setTimeout(resolve,5000))
const { limit, offset } = paginationQuery;
return this.coffeeRepository.find({
relations: ["flavors"],
skip: offset,
take: limit
});
}
管道
管道对控制器的路由处理程序正在处理的参数进行操作
NestJS在方法被调用之前触发一个管道,管道还接收要传递给方法的参数,任何转换或者验证操作都在这时发生,之后,使用任何可能转换的参数调用路由处理程序
NestJS提供几个开箱即用的管道,全部来自于@nestjs/common
- ViladationPipe:参数格式化
- ParseArrayPipe:解析和验证数组
构建自己的管道,自动将任何传入的字符串解析为整数,称之为:ParseIntPipe
nest g pipe common/pipes/parse-int
@Injectable()
export class ParseIntPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value;
}
}
@Injectable()
export class ParseIntPipe implements PipeTransform {
transform(value: string, metadata: ArgumentMetadata) {
const val = parseInt(value,10);
if(isNaN(val)){
throw new BadRequestException(`Validation failed. "${val} is not an integer."`)
}
return val;
}
}
findOne(@Param("id", ParseIntPipe) id: string) {
console.log(id);
console.log(typeof id);
return this.coffeesService.findOne(id);
}
中间件
中间件是一个在处理路由处理程序和任何其他构建块之前调用的函数。这包括拦截器、守卫和管道。
中间件函数可以访问Request、Response,并且不专门绑定到任何方法,而是绑定到指定的路由路径。
中间件可以执行以下任务:
使用中间件时,如果当前中间件函数没有结束请求、响应周期,它必须调用next()方法,该方法将控制权传递给下一个中间件函数,否则请求将被挂起,永远不会完成
创建中间件:自定义Nest中间件可以在Function和Class中实现
函数中间件是无状态的,不能注入依赖,且无权访问Nest容器
类中间件可以依赖外部依赖并注入在同一模块范围内注册的提供程序
- 通过Nest CLI生成一个中间件类,称之为`logging``
nest g middleware common/middleware/logging
@Injectable()
export class LoggingMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
next();
}
}
- 在其中增加一句打印
@Injectable()
export class LoggingMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
console.log('Hi from middleware!');
next();
}
}
- 注册新创建的中间件。该中间件没有特别绑定到任何方法,不能使用装饰器以声明方式绑定,但是可以将中间件绑定到路由路径,表示为字符串
- 注册到LoggingMiddleware
- 在common.module.ts中,让CommonModule实现了NestModule接口,并在configure中调用LoggingMiddleware可以指定路由、指定请求方法或排除路由
import { ConfigModule } from "@nestjs/config";
import { LoggingMiddleware } from "./middleware/logging.middleware";
@Module({
imports:[ConfigModule],
providers:[
// {
// provide: APP_GUARD,
// useClass: ApiKeyGuard
// }
]
})
export class CommonModule implements NestModule{
configure(consumer: MiddlewareConsumer) {
// consumer.apply(LoggingMiddleware).forRoutes('*'); // 绑定到所有路由
// consumer.apply(LoggingMiddleware).forRoutes('coffee'); // 绑定到coffee路由
// consumer.apply(LoggingMiddleware).forRoutes({
// path: '*',
// method:RequestMethod.GET
// }); // 指定请求方法与路由
// consumer.apply(LoggingMiddleware).exclude('coffee'); // 排除coffee
consumer.apply(LoggingMiddleware).forRoutes('*');
}
}
更改logging.middle.ts,输出请求时常
import { Injectable, NestMiddleware } from "@nestjs/common";@Injectable()
export class LoggingMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
console.time("Request-response time");
console.log("Hi from middleware!");
res.on("finish", () => console.timeEnd("Request-response time"));
next();
}
}
自定义参数装饰器
在common/decorators中新建protocol.decorator.ts
import { createParamDecorator, ExecutionContext } from "@nestjs/common";export const Protocol = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.protocol;
}
);
在findAll()中定义@Protocol
@Public()@Get()
findAll(@Protocol() protocol: string, @Query() paginationQuery: PaginationQueryDto) {
console.log(protocol);
// const { limit, offset } = paginationQuery;
// console.log(123);
return this.coffeesService.findAll(paginationQuery);
}
更新protocol.decorator.ts与findAll()传递默认值
import { createParamDecorator, ExecutionContext } from "@nestjs/common";export const Protocol = createParamDecorator(
(defaultValue: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.protocol;
}
);@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);
}
Swagger
记录应用程序如何工作并显示我们的API参数和返回是大多数应用程序文档的重要组成部分
公开外部软件开发工具(或SDK)尤其如此
Swagger是自动化整个过程的一个很好的工具
NesJS集成和自动生成Open API文档
使用Open API规范记录应用程序
Open API规范是一种与语言无关的定义格式,用于描述RESTful AP
Open API文档允许我们描述整个API,包括
- 可用的操作或端点
- 操作参数:每个操作的输入和输出
- 认证方法
- 联系信息、许可、使用条款和其他信息
Nest提供了一个专用模块@nestjs/swagger,可以简单地通过利用装饰器来生成Open API文档
yarn add @nestjs/swagger swagger-ui-express
.setTitle("Iluvcoffee")
.setDescription("Coffee application")
.setVersion("1.0")
.build();
- 挂载Swagger UI的路由路径
- 应用程序实例
- 实例化的文档对象
初始化的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
- yarn test:用于单元测试
- yarn test:cov:用于单元测试和收集测试覆盖率
- yarn test:e2e:用于端到端(End to End)测试
对于NestJS中的单元测试,通常的做法是通过将.spec.ts文件保存在与它们测试的应用程序源代码文件相同的文件夹中
每个Controller、Provicer、Service等都应该有自己的专用测试文件
测试文件扩展名必须是*.spec.ts
端到端测试默认情况下通常位于专用的/test/目录中,端到端测试通常按照测试的"特性"或者"功能"分组到单独的文件中。
端到端测试文件扩展名必须是*.e2e-spec.ts
单元测试侧重于单个类和函数,端到端测试适合对整个系统进行高级验证