![]() |
鼻子大的书包 · 河北省医疗保障局 国家、省药品集中采购名称和降幅· 5 月前 · |
![]() |
没有腹肌的沙滩裤 · 《出土战国文献字词集释》出版-清华大学出土文 ...· 8 月前 · |
![]() |
帅气的玉米 · 武汉市九上数学原调试卷_头条· 10 月前 · |
![]() |
成熟的烤地瓜 · 苹果交通卡高级用法(交通联合) - 知乎· 1 年前 · |
![]() |
奋斗的香蕉 · ova圣华女学院校长 - 抖音· 1 年前 · |
学习NestJS 官方基础课程【中英字幕 NestJS Fundamentals Course】个人笔记
因为用不到测试,所以暂时学到了P65
后续项目可能用不到MogoDB,后面的也就没看
nest g controller coffee
nest g service coffee
nest g module coffee
nest g class coffee/entities/coffee.entity --no-spec
nest g class coffee/dto/create-coffee.dto --no-spec
DTO和Entity的区别,
NextJS提供了
ValidationPipe
进行数据验证
ValidationPipe
提供了对所有传入客户端有效负载强制执行验证规则的便捷方式
app.useGlobalPipes(new 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());
await app.listen(3000);
}
bootstrap();a
yarn add class-validator class-transformer
import { IsString } from "class-validator";
export class CreateCoffeeDto {
@IsString()
readonly name: string;
@IsString()
readonly brand: string;
@IsString({ each: true })
readonly flavors: string[];
}
yarn add @nestjs/mapped-types
update-coffee.dto.ts
中,使用
PartialType
进行验证
import { PartialType } from "@nestjs/mapped-types";
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 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();
参考
DockerId:kaisarh
Email:hkzxh1104
password:hkzxh1104
docker-compose.yml
version: "3"
services:
db:
image: postgres
restart: always
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD:
docker-compose up -d
-d 标志意味着我们以“分离”模式运行我们的容器,意味着它们将在
background
中运行
目前Docker Compose YAML文件中只列出了一项服务,但供将来参考
yarn add @nestjs/typeorm typeorm@2 pg
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";
@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 {
}
[Nest] 8316 - 2022/05/24下午2:15:07 LOG [InstanceLoader]
coffee.entities.ts
中
@Entity
注解标注实体表
PrimaryGeneratedColumn
标注自增主键
@Column
标注行,可以通过设置options配置参数。例如
nullable
设置非空
coffee.module.ts
中进行导入
imports: [TypeOrmModule.forFeature([CoffeeEntity])],
import { Module } from "@nestjs/common";
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
提供的
Repository
类作为对数据源的抽象,并通过方法与数据进行交互
CoffeesModule
的范围内注册了
Coffee Entity
,可以使用从
@nestjs/typeorm
包导出的
@InjectRepository
装饰器将自动生成的
存储库
注册到
CoffeeService
中
private coffees: CoffeeEntity[] = [
{
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()
风味实体
nest g class coffee/entities/flavor.entity --no-spec
实体中,导出的类不要有
Entity
后缀,因为我们不希望数据库表名存在entity
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class FlavorEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
}
Coffee Entity
中更新
flavor
属性
flavors
的
@Column()
装饰器,并通过其他装饰器与
FlavorEntity
设置
Relation
typeorm
中引入
@JoinTable()
装饰器,其可以指定关系的
OWNER
端,在这里是
Coffee Entity
@ManyToMany
在
Coffee Entity
中指定与
Flavor Entity
的关系
type
type
中的哪个参数绑定关联
import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from "typeorm";
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 { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from "typeorm";
import { Coffee } from "./coffee.entity";
@Entity()
export class Flavor {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(
type => Coffee,
coffee => coffee.flavors
)
coffees: Coffee[];
}
Coffee.module.ts
中引入
Flavor
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";
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor])],
controllers: [CoffeeController],
providers: [CoffeeService]
})
export class CoffeeModule {
}
coffee_flavors_flavor
Get
方案,更新
findAll
与
findOne
,通过配置
relations
关联参数
relations
的解释为:
findAll() {
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
不存在?
@ManyToMany()
进行关联的时候,配置第三个参数,设置
cascade: true
,或者设置为仅在插入时候生效
cascade:['insert']
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: string[];
}
create-coffee.dto
,其包含一个风味属性,是一个字符串数组,为了确保将这些字符串(风味的名称)映射到真实的实体,即风味实体的实例,需要做以下事情
Flavor Repository
注入到
CoffeesService
类中
constructor(
@InjectRepository(Coffee)
private readonly coffeeRepository: Repository<Coffee>,
@InjectRepository(Flavor) // 將Flavor注入到coffeeService中
private readonly flavorRepository:Repository<Flavor>
) {
}
preloadFlavorByName
private async preloadFlavorByName(name:string):Promise<Flavor>{
const existingFlavor = await this.flavorRepository.findOne({name});
if(existingFlavor){
return existingFlavor;
}
this.flavorRepository.create({name});
}
create()
方法
async create(createCoffeeDto: CreateCoffeeDto) {
// 使用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()
方法
async update(id: string, updateCoffeeDto: UpdateCoffeeDto) {
// 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
和
offset
两个属性
export class PaginationQueryDto {
limit: number;
offset: number;
}
@Type()
装饰器保证值被返回为
Number
import { Type } from "class-transformer";
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();
@IsOptional()
装饰器,将属性标记为
可选
,意味着没有如果它缺失或未定义,将抛出错误
import { Type } from "class-transformer";
import { IsOptional } from "class-validator";
export class PaginationQueryDto {
@IsOptional()
@Type(() => Number)
limit: number;
@Type(() => Number)
@IsOptional()
offset: number;
}
@IsPositive()
装饰器,检查值是否为正数大于0
import { Type } from "class-transformer";
import { IsOptional, IsPositive } from "class-validator";
export class PaginationQueryDto {
@IsPositive()
@IsOptional()
@Type(() => Number)
limit: number;
@IsPositive()
@IsOptional()
@Type(() => Number)
offset: number;
}
import { IsOptional, IsPositive } from "class-validator";
export class PaginationQueryDto {
@IsPositive()
@IsOptional()
limit: number;
@IsPositive()
@IsOptional()
offset: number;
}
coffee.controller
中的
findAll()
方法,将
paginationQuery
类型设置为
PaginationQueryDto
@Get()
findAll(@Query() paginationQuery: PaginationQueryDto) {
// const { limit, offset } = paginationQuery;
return this.coffeesService.findAll(paginationQuery);
}
coffee.service
中的
findAll()
方法,接收参数,并通过
skip
和
take
向
findAll()
方法传递给
find()
方法
findAll(paginationQuery: PaginationQueryDto) {
const { limit, offset } = paginationQuery;
return this.coffeeRepository.find({
relations: ["flavors"],
skip: offset,
take: limit
});
}
entity
nest g class events/entities/event.entity --no-spec
Event
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Event {
@PrimaryGeneratedColumn()
id: number;
@Column()
type: string;
@Column()
name: string;
// payload 是存储事件有效负载通用列
@Column("json")
payload: Record<string, any>;
}
coffee
模块中引入
Event
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";
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [CoffeeService]
})
export class CoffeeModule {
}
咖啡实体
中新增推荐属性
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;
// 新增推荐属性
@Column({ default: 0 })
recommendations: number;
@JoinTable()
@ManyToMany(
type => Flavor,
(Flavor) => Flavor.coffees,
{
cascade: true // ['insert']
})
flavors: Flavor[]; // 将flavors的类型设置为Flavor
}
coffee.service.ts
中,引入
Connection
创建事务
constructor(
@InjectRepository(Coffee)
private readonly coffeeRepository: Repository<Coffee>,
@InjectRepository(Flavor) // 將Flavor注入到coffeeService中
private readonly flavorRepository: Repository<Flavor>,
// 引入Connection用来创建事务
private readonly connection: Connection
) {
}
coffee.service.ts
中,新建异步方法,并命名为
推荐Coffee
,接收一个参数
咖啡
async recommendCoffee(coffee: Coffee) {
const queryRunner = this.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
}
queryRunner
queryRunner
创建到数据库的新连接
try / catch / finally
中,以确保如果出现任何问题,
catch
可以回滚整个事务
async recommendCoffee(coffee: Coffee) {
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()
装饰器在列上定义一个索引
@Index()
@Column()
name: string;
@Index()
装饰器应用在类本身,并在装饰器内传递一个列名数组作为参数
import { Column, Entity, Index, PrimaryGeneratedColumn } from "typeorm";
@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
文件
module.exports = {
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迁移,知道我们的实体和迁移文件将在哪里
npx typeorm migration:create -n CoffeeRefactor
/src/migrations
目录中生成一个新的迁移文件
coffee.entity
,将
name
更改为
title
@Column()
title: string;
synchronize: true
,但是不会更新生产数据库,这是迁移非常方便的主要原因之一
name
为
title
后,不仅会删除名称列,还会删除该列中的所有数据
up()
和
down()
方法
import {MigrationInterface, QueryRunner} from "typeorm";
export class CoffeeRefactor1653399205455 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}
up()
是只是需要更改的内容以及如何更改的内容
down()
是撤销或回滚任何这些更改的地方,万一出现问题,需要一个退出策略,帮助撤销一切,
down()
就可以保证我们的迁移回滚
up()
中新增更改表字段名称的数据库语句
await queryRunner.query(
'ALTER TABLE "coffee" RENAME COLUMN "name" TO "title"',
)
在这里可以执行所需的任何类型的数据库迁移,同时,必须为 回滚迁移提供逻辑
down()
中新增回滚逻辑
await queryRunner.query(
'ALTER TABLE "coffee" RENAME COLUMN "title" TO "name"',
)
import {MigrationInterface, QueryRunner} from "typeorm";
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
@Column({ nullable: true })
description: string;
yarn run build
TypeORM
生成迁移,并将其命名为
SchemaSync
npx typeorm migration:generate -n SchemaSync
/src/migrations/
中新生成的迁移文件,查看
up()
和
down()
import {MigrationInterface, QueryRunner} from "typeorm";
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
中通过
@Injectable()
装饰器声明了一个可以由Next"容器"管理的类。
CoffeeService
类标记为
Provider
CoffeeController
中,在构造函数中请求
CoffeeService
coffeesService:
这个请求高速Nest将提供程序注入到我们的控制器类中
-this-
类也是一个
Provider
,因为在
CoffeeModule
中包含了
-here-
,它向Nest反转控制(IoC
coffee-rating module
nest g mo coffee-rating
coffee-rating service
并将其作为providers写入
coffee-rating module
nest g s coffee-rating
coffee-rating
coffee-rating-module
import { Module } from '@nestjs/common';
import { CoffeeRatingService } from './coffee-rating.service';
@Module({
providers: [CoffeeRatingService]
})
export class CoffeeRatingModule {}
coffee-rating-service
import { Injectable } from '@nestjs/common';
@Injectable()
export class CoffeeRatingService {}
CoffeeRatingService
依赖
CoffeeService
从数据库中获取咖啡
CoffeeRtaingModule
中导入
CoffeeModule
import { Module } from "@nestjs/common";
import { CoffeeRatingService } from "./coffee-rating.service";
import { CoffeeModule } from "../coffee/coffee.module";
@Module({
imports: [CoffeeModule],
providers: [CoffeeRatingService]
})
export class CoffeeRatingModule {
}
CoffeeRatingService
, 并使用基于构造函数的注入来添加
CoffeeService
import { Injectable } from "@nestjs/common";
import { CoffeeService } from "../coffee/coffee.service";
@Injectable()
export class CoffeeRatingService {
constructor(private readonly coffeeService: CoffeeService) {
}
}
coffee.module.ts
中将
CoffeeService
导出
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";
// 创建命令 nest g module coffee
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeeController],
providers: [CoffeeService],
exports: [CoffeeService]
})
export class CoffeeModule {
}
以下场景:
通过Nest定义自定义提供程序来处理这些场景。
providers
数组形式只是简写,实际上只是提供
TOKEN
并在该
TOKEN
的位置提供
"要注入的内容"
的简写版本。完整写法为:
providers:[
{
provider: CoffeesService,
useClass: CoffeesService
}
]
Nest提供了不同方法进行自定义提供者。【
useValue
、
useClass
】
useValue
,
useValue
语法对于注入常量
constant
值很有用。
Mock\{}
对象替代服务的真实实现。
CoffeeService
替换为
自定义Provider
并使用
useValue
语法,当在程序中注入
CoffeeService
时,每当
CoffeeService TOKEN
被解析时,他将指向新的
MockCoffeeService
,
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: [{ 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
export const COFFEE_BRANDS = "COFFEE_BRANDS";
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";
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 {
}
useClass
,
useClass
允许动态确定一个
Token
应该解析到的
Class
例如,有一个抽象或默认的ConfigService类,根据当前环境,需要Nest为每个配置服务提供不同的实现
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";
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
,允许
"动态"
创建提供者,如果需要将提供者的值基于各种其他依赖项、值,这将非常有用。
useFactory
的返回值将被提供者(provider)使用。
{ provide: COFFEE_BRANDS, useFactory: () => ["buddy brew", "nescafe"] }
新的更现实的例子,并在其中注入一些提供程序:
定义一个随机提供者,并确保将其注册为提供者(
@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
比如:有一个通用模块,该模块需要在不同情况下表现不同
动态模块需要一些配置才可以被消费者使用
测试
DatabaseModule
,可以在实例化之前传递配置
nest g mo database
"CONNECTION"
,并使用
useValue
从
typeorm
中调用
createConnection()
方法,传入一些任意值建立数据库连接
import { Module } from "@nestjs/common";
import { createConnection } from "typeorm";
@Module({
providers: [
{
provide: "CONNECTION",
useValue: createConnection({
type: "postgres",
host: "localhost",
port: 5432
})
}
]
})
export class DatabaseModule {
}
DatabaseModule
对这些连接选项进行了硬编码,所以不能轻易地在不同应用程序之间共享这个模块。这是因为在这个模块的配置是静态配置的,不能自定义。
Nest
的动态模块功能,可以让消费模块使用API来控制导入时如自定义
DatabaseModule
DatabaseModule
上定义一个名为
register()
的静态方法
register()
可以接收消费模块传递过来的参数
register()
返回
DynamicModule
类型结果,它与典型的
@Module()
具有基本相同的接口,但需要传递一个
module
属性,也就是当前模块本身
register()
,可以将接收的参数用于创建数据库连接中
import { DynamicModule, Module } from "@nestjs/common";
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 {
}
SpringBoot中提供了Scope注解来指明Bean的作用域,NestJs也提供了类似的
@Scope()
装饰器:
scope名称
|
说明
|
SINGLETON
|
单例模式,整个应用内只存在一份实例
|
REQUEST
|
每个请求初始化一次
|
TRANSIENT
|
每次注入都会实例化
|
nestjs/config
yarn add @nestjs/config
AppModule
,将来自
@nestjs/config
的
ConfigModule
添加到
imports:[]
数组中,并调用其静态方法
forRoot()
,它将从默认位置加载和解析
.env
文件,即项目的根目录
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";
@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 {
}
.env
保存重要的应用程序配置数据的键值对,无论是密钥、数据库选项、密码等,通过
.env
文件,在不同环境运行一个应用程序,只需要正确交换
.env
即可
.env
文件
DATABASE_USER=postgres
DATABASE_PASSWORD=pass123
DATABASE_NAME=postgres
DATABASE_PORT=5432
DATABASE_HOST=localhost
配置均与数据库配置相关,来此docker-compose
.env
不会被git追踪
.gitignore
,增加以下行
# ENV
AppModule
,更新传递给
TypeOrmModule
的选项对象
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";
@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 {
}
yarn start:dev
重启后,数据库配置正常,正在使用从当前环境中加载的配置
ConfigModule
会在应用程序的根目录中查找
.env
文件,通过
envFilePath
为这个文件指定另一个路径。
ConfigModule.forRoot({
envFilePath: ".environment"
}),
除了传递字符串值,也可以传递字符串数组来为
.env
文件指定多个路径
如果在多个文件中找到相同变量,优先使用第一个匹配文件中的变量
.env
文件,可以设置
ignoreEnvFile
完全禁止加载
.env
文件
ConfigModule.forRoot({
ignoreEnvFile: true
}),
@nestjs/config
包中的
joi
包确保任何重要的环境变量都得到验证,使用
Joi
,可以定义对象模式并针对它验证JavaScript对象
yarn add @hapi/joi
yarn add -D @types/hapi__joi
ConfigModule.forRoot()
方法内部,通过
validationSchema
确保以正确的格式传入某些环境变量
ConfigModule.forRoot({
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 {
}
ConfigModule
带有一个名为
ConfigService
的有用服务,该服务提供了一个
get()
方法来读取解析的配置变量
coffee.module
中导入
ConfigModule
。
AppModule
中使用了
forRoot()
方法,其他地方不需要做任何事
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";
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()
方法获取参数
export class CoffeeService {
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");
通过配置文件对不同配置进行管理与使用
src/config/app.config.ts
export default () => ({
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
新属性,它接收一个配置工厂数组
ConfigModule.forRoot({
load: [appConfig]
}),
coffee.service.ts
中,通过
get()
方法获取配置的时候,不需要使用
DATABASE_HOST
获取,直接使用
database.host
即可
const databaseHost = this.configService.get("database.host", "demo");
ConfigModule
允许使用嵌套对象定义和加载多个自定义配置文件,并通过提供的
ConfigService
(来自
@nestjs/config
)访问这些变量。
configService.get()
方法获取所有配置值很容易出错,由于必须使用"点表示法"(即a.b)来检索嵌套项
配置命名空间
和
部分注册
以进行验证配置。
src/config/coffee.config.ts
,通过
registerAs()
函数可以在命名空间内定义一个
token
,也就是第一个参数
import { registerAs } from "@nestjs/config";
export default registerAs("coffee", () => ({
foo: "bar"
}));
coffee.module.ts
中使用
ConfigModule.forFeature()
注册这个
coffeeConfig
,也就是
部分配准
imports: [
TypeOrmModule.forFeature([Coffee, Flavor, Event]),
ConfigModule.forFeature(coffeeConfig)
],
CoffeeService
中通过
get()
获取配置
const coffeeConfig = this.configService.get("coffee");
console.log(coffeeConfig);
也可以通过点语法获取对应值
const coffeeConfig = this.configService.get("coffee");
const coffeeConfigFoo = this.configService.get("coffee.foo");
console.log(coffeeConfig);
console.log(coffeeConfigFoo);
@Inject(coffeeConfig.KEY)
private readonly coffeeConfiguration: ConfigType<typeof coffeeConfig>
每个命名空间配置都暴露了一个
Token
也就是
key
属性,可以使用该属性将整个对象注入到Nest容器中注册的任何类
ConfigType
是一个开箱即用的辅助类型,推断函数的返回类型
console.log(coffeeConfiguration.foo);
可以直接通过该对象获取配置,甚至有强类型的好处
目前
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 {
}
例如:处理诸如"缓存响应"之类的事情
如何将上述四种构建块绑定到我们的应用程序?基本上有三种不同的绑定方式:过滤器、守卫和拦截器绑定到路由处理程序,管道特定(仅适用于管道)
嵌套构建块可以是:
在
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_PIPE
provider的东西,以这种方式提供
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
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
@Catch()
export class HttpExceptionFilter<T> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {}
}
@Catch()
装饰器将所需的元数据绑定到
ExceptionFilter
,这个
@Catch
装饰器可以采用单个参数或逗号分隔的列表,如果需要,允许一次为多种类型的异常设置过滤器
HttpException
实例的异常,所以在
@Catch()
中增加
HttpException
,同时将
T
泛型继承
HttpException
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from "@nestjs/common";
@Catch(HttpException)
export class HttpExceptionFilter<T extends HttpException> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {
}
}
Response
对象,以便操纵或转换它并继续发送响应。
ArgumentsHost
的实例
host
的
switchToHttp()
方法,可以能够访问到请求或响应对象。
const context = host.switchToHttp();
context
的
getResponse()
方法,可以返回底层平台(默认Express)的响应。
const response = context.getResponse<Response>();
exception
对象,提取两个参数:
statusCode
和
body
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()
});
完整的异常处理
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
绑定到应用程序上
app.useGlobalFilters
进行绑定
import { NestFactory } from "@nestjs/core";
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,并使用提取的信息来确定请求是否可以继续
本例子实现两件事:
API_KEY
是否存在于
authorization
请求头
"公共"
的
通过
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 { NestFactory } from "@nestjs/core";
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,且仅存在非公共路由上"
.env
文件中定义
API_KEY=3f4a1a66501692601596e772d3db1c97bb22970c7b08e6b588ccb393baab7e3f
DATABASE_USER=postgres
DATABASE_PASSWORD=pass123
DATABASE_NAME=postgres
DATABASE_PORT=5432
DATABASE_HOST=localhost
API_KEY=3f4a1a66501692601596e772d3db1c97bb22970c7b08e6b588ccb393baab7e3f
const request = context.switchToHttp().getRequest<Request>();
Authorization
请求头
const authHeader = request.header("Authorization");
return authHeader === process.env.API_KEY;
Authorization
请求头,且与
.env
中设置的值一样才可以访问
完整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);
}
这是最简单的方法,但不是最佳实践。
理想情况下,应该创建自己的装饰器来实现相同的目标。
/common/
目录下新建文件夹并命名为
/decorators/
,在这里存储装饰器
public.decorator.ts
的新文件
"key"
@Public
import { SetMetadata } from "@nestjs/common";
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY,true);
CoffeeController
中,对
findAll()
装饰器进行优化,使
用@Public
代替
@SetMetadata('isPublic',true)
import {Public} from "../common/decorators/public.decorator";
@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
的实例对象可以访问当前上下文的元数据。
api-key.guard.ts
中注入
Reflector
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
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;
}
}
reflector.get
获取元数据,接收两个参数
context.getHandler()
Class Level
中检索元数据,这里调用
content.getClass()
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
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中
common
nest g mo common
import { Module } from '@nestjs/common';
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 {}
main.ts
中的
app.useGlobalGuards(new ApiKeyGuard());
import { NestFactory } from "@nestjs/core";
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 { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class WrapResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle();
}
}
@Injectable()
装饰器的类
'@nestjs/common'
导出的
NestInterceptor
接口
NestInterceptor
接口要求在类中提供
intercept()
方法
intercept()
方法应该从
RxJS
库返回一个
Observable
CallHandler
接口实现了
handle()
方法,使用该方法在拦截器中调用路由处理程序方法
handle()
方法,路由处理程序不会被执行
intercept()
方法有效地包装了请求、响应流,允许在执行最终路由处理程序之前和之后实现自定义逻辑
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
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
构建自己的管道,自动将任何传入的字符串解析为整数,称之为:
ParseIntPipe
NestJS CLI
生成一个管道类
nest g pipe common/pipes/parse-int
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value;
}
}
string
返回
整数
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from "@nestjs/common";
@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;
}
}
@Param()
装饰器上,作为
@Param
的第二个参数进行传递
@Get(":id")
findOne(@Param("id", ParseIntPipe) id: string) {
console.log(id);
console.log(typeof id);
return this.coffeesService.findOne(id);
}
中间件是一个在处理路由处理程序和任何其他构建块之前调用的函数。这包括拦截器、守卫和管道。
中间件函数可以访问Request、Response,并且不专门绑定到任何方法,而是绑定到指定的路由路径。
中间件可以执行以下任务:
next()
中间件函数
使用中间件时,如果当前中间件函数没有结束请求、响应周期,它必须调用
next()
方法,该方法将控制权传递给下一个中间件函数,否则请求将被挂起,永远不会完成
创建中间件:自定义Nest中间件可以在Function和Class中实现
函数中间件是无状态的,不能注入依赖,且无权访问Nest容器
类中间件可以依赖外部依赖并注入在同一模块范围内注册的提供程序
Nest CLI
生成一个中间件类,称之为`logging``
nest g middleware common/middleware/logging
import { Injectable, NestMiddleware } from '@nestjs/common';
@Injectable()
export class LoggingMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
next();
}
}
import { Injectable, NestMiddleware } from '@nestjs/common';
@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 { MiddlewareConsumer, Module, NestModule, RequestMethod } from "@nestjs/common";
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);
}
记录应用程序如何工作并显示我们的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
main.ts
中,通过生成一个基本
文档
开始设置
swagger
const options = new DocumentBuilder()
.setTitle("Iluvcoffee")
.setDescription("Coffee application")
.setVersion("1.0")
.build();
SwaggerModule.createDocument()
方法创建文档
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup()
方法将所有内容连接在一起,此方法接收一些参数,包括
SwaggerModule.setup("api", app, document);
yarn start:dev
并访问
http://127.0.0.1:3000/api/#/
初始化的Swagger UI内容并不完整,例如POST请求中,没有标明参数
但是有一个专门的DOT类,代表该接口的输入参数
Nest提供了一个插件来增强TypeScript编译过程,减少需要创建的样板代码数量,从而解决该问题。
推荐:在需要覆盖插件提供的基本功能的任何地方添加特定的装饰器
Swagger CLI
插件,打开
nest-cli.json
增加以下配置
"compilerOptions": {
"deleteOutDir": true,
"plugins": [
"@nestjs/swagger/plugin"
]
}
http://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";
import { CreateCoffeeDto } from "./create-coffee.dto";
export class UpdateCoffeeDto extends PartialType(CreateCoffeeDto) {
}
http://127.0.0.1:3000/api/#/default/CoffeeController_update
正常
在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 {}
yarn test
:用于单元测试
yarn test:cov
:用于单元测试和收集测试覆盖率
yarn test:e2e
:用于端到端(End to End)测试
对于NestJS中的单元测试,通常的做法是通过将
.spec.ts
文件保存在与它们测试的应用程序源代码文件相同的文件夹中
每个Controller、Provicer、Service等都应该有自己的专用测试文件
测试文件扩展名必须是
*.spec.ts
端到端测试默认情况下通常位于专用的
/test/
目录中,端到端测试通常按照测试的"特性"或者"功能"分组到单独的文件中。
端到端测试文件扩展名必须是
*.e2e-spec.ts
单元测试侧重于单个类和函数,端到端测试适合对整个系统进行高级验证
![]() |
鼻子大的书包 · 河北省医疗保障局 国家、省药品集中采购名称和降幅 5 月前 |
![]() |
帅气的玉米 · 武汉市九上数学原调试卷_头条 10 月前 |
![]() |
成熟的烤地瓜 · 苹果交通卡高级用法(交通联合) - 知乎 1 年前 |
![]() |
奋斗的香蕉 · ova圣华女学院校长 - 抖音 1 年前 |