发布于 

NestJs

官方基础课程(快速体验)

快速创建nest项目

全局安装nest

1
npm i -g nest

快速创建nest项目

1
nest new

创建出来目录结构:

  • main.js 主入口
  • app.module.ts 创建的控制器会被controllers集成
  • app.service.ts
  • app.controller.ts

生成controller

1
2
nest generate controller controllerName
nest g co controllerName

nuxt开发时热更新命令:

1
npm run start:dev

控制器 Controller

@Controller

定义映射器类
/coffee

1
2
3
4
@Controller('coffee')
export class CoffeeController{
// 控制器的内部实现
}
@Get

/coffee/flavors

1
2
3
4
5
6
7
8
@Controller('coffee')
export class CoffeeController{
// 路径映射
@Get('flavors')
findAll(){
return 'You find all coffees'
}
}
@Param

/coffee/123456

1
2
3
4
5
6
7
8
9
@Controller('coffee')
export class CoffeeController{
// 带参数映射
@Get(':id')
// findOne(@Param() params){ // 获取所有params参数
findOne(@Param('id') id: string){ // 获取指定paran参数
return `get #${id} coffee`
}
}
@Post/@Body/@HttpCode

/coffee/create

1
2
3
4
5
6
7
8
9
10
11
12
@Controller('coffee')
export class CoffeeController{
// post请求
@Post()
@HttpCode(HttpStatus.BAD_GATEWAY) // 自定义状态码
// Body参数
// create(@Body() body){
// 获取特定的属性
create(@Body('message') body){
return body;
}
}
@Res

/coffee/yummy

1
2
3
4
5
6
7
8
@Controller('coffee')
export class CoffeeController{
// 直接调用express库方法
@Get('candy')
yummy(@Res() response){
response.status(200).send('yummy candy')
}
}
@Patch

更新数据
/coffee/1234

1
2
3
4
5
6
7
8
9
10
11
@Controller('coffee')
export class CoffeeController{
// patch 更新
@Patch(':id')
update(@Param('id') id: string, @Body() body) {
return {
message:`This action updates #${id} coffee`,
data:body
}
}
}
@Delete

删除数据
/coffee/1234

1
2
3
4
5
6
7
@Controller('coffee')
export class CoffeeController{
@Delete(':id')
remove(@Param('id') id:string){
return `delete #${id}`
}
}

服务 Service|Provider

创建Service
1
2
nest generate service
nest g s

service会被创建在src对应路径下,
同时更新在app.modules.ts下的providers数组中

每个Service都是一个provider,

  • provider内可以注入依赖
  • provider可以作为依赖被注入到controller中

service负责处理数据存储、检索等工作,
为controller提供数据。

entity

创建实体,作为service控制的对象:
nest中将controller和service集成在同一个目录下,
entity实体也创建在此目录下。

/src/coffee/entities/coffee.entity.ts

1
2
3
4
5
6
export class Coffee{
id: number;
name: string;
brand: string;
flavors: string[];
}
Controller+Service实现CRUD
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import { Injectable } from '@nestjs/common';
import { Coffee } from './entities/coffee.entity';

// @Injectable标识表示service
@Injectable()
export class CoffeeService {
private coffees: Coffee[] = [
{
id: 1,
name: 'Shipwreck Roast',
brand: 'Buddy Brew',
flavors: ['chocolate', 'vanilla'],
},
];
/**
* 查找全部
*/
findAll(){return this.coffees}
/**
* 根据id返回指定值
* @param id
*/
findOne(id:string){
return this.coffees.find(item=>item.id === +id)
}

/**
* 新建
* @param createCoffeeDto
*/
create(createCoffeeDto: any){
this.coffees.push(createCoffeeDto);
return this.coffees
}

/**
* 更新
* @param id
* @param updateCoffeeDto
*/
update(id: string,updateCoffeeDto: any){
const existingCoffee = this.findOne(id)
if(existingCoffee){
// update this entoty
Object.assign(existingCoffee, updateCoffeeDto)
return existingCoffee
}
return 'cant find'
}
/**
*
* @param id 删除
*/
remove(id:string){
const coffeeIndex = this.coffees.findIndex(item=>item.id == + id)
if(coffeeIndex >= 0){
this.coffees.splice(coffeeIndex,1)
}
}
}

在controller中通过构造函数,注入service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Controller('coffee')
export class CoffeeController {
// service/provider注入
constructor(private readonly coffeeService) {}

// 查询所有
@Get('findAll')
findAll() {
return this.coffeeService.findAll()
}

// 指定查询
@Get(':id')
// findOne(@Param() params){ // 获取所有params参数
findOne(@Param('id') id: string) {
return this.coffeeService.findOne(id)
}

// 新增
@Post()
create(@Body() body) {
return this.coffeeService.create(body);
}


// 更新
@Patch(':id')
update(@Param('id') id: string, @Body() body) {
return this.coffeeService.update(id,body)
}

// 删除
@Delete(':id')
remove(@Param('id') id:string){
return this.coffeeService.remove(id)
}

}
1
2
3
4
5
6
7
8
9
10
11
12
@Injectable()
export class CoffeeService {
findOne(id: string) {
const coffee = this.coffees.find(item => item.id === +id)
if (!coffee) {
// 抛出对应网络异常
// throw new HttpException(`Coffee #${id} not found`, HttpStatus.NOT_FOUND)
throw new NotFoundException(`Coffee #${id} not found`)
}
return coffee
}
}

模块 Module

module

集成module

1
nest g module moduleName

在app.modules.ts横纵的imports内会自动引入新创建的Module

使用module将controllers和providers集成在一起:

1
2
3
4
5
6
import { Module } from '@nestjs/common';
import { CoffeeController } from './coffee.controller';
import { CoffeeService } from './coffee.service';

@Module({ controllers:[CoffeeController], providers:[CoffeeService] })
export class CoffeeModule {}

将app.module.ts的代码清理一下,保证controller和service仅被实例化一次:

1
2
3
4
5
6
7
8
9
10
11
12
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CoffeeModule } from './coffee/coffee.module';

@Module({
imports: [CoffeeModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

DTO

data transfer object
用于封装规范接口传输的数据类型

生成DTO

1
2
nest g class className
nest g class coffee/dto/create-coffee.dto --no-spec

dto的定义:
create-coffee.dto.ts

1
2
3
4
5
export class CreateCoffeeDto {
readonly name: string;
readonly brand: string;
readonly flavors: string[];
}

dto的使用:
coffee.controller.ts

1
2
3
4
5
6
7
8
9
@Controller('coffee')
export class CoffeeController{
constructor(private readonly coffeeService: CoffeeService){}

@Post()
create(@Body() createCoffeeDto:CreateCoffeeDto){
return this.coffeeService.create(createCoffeeDto)
}
}
数据验证

数据验证要用到ValidationPipe

需要下载如下包:

1
npm i class-validator class-transformer @nestjs/mapped-types

在main.ts中加入pipe

  • whitelist 白名单模式
  • forbidNonWhitelisted 非白名单数据报错
  • transform 将传入数据转化为指定的数据类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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: false,
transform: true,
})) // 配置验证管道
await app.listen(3000);
}
bootstrap();

在DTO中加入数据验证描述符:

1
2
3
4
5
6
7
8
9
10
// create-coffee.dto.ts
import {IsString} from 'class-validator'
export class CreateCoffeeDto {
@IsString()
readonly name: string;
@IsString()
readonly brand: string;
@IsString({each:true})
readonly flavors: string[];
}

使用mapped-types包,
实现数据验证的继承与附加属性配置:

1
2
3
4
5
6
import {PartialType} from '@nestjs/mapped-types'
import { CreateCoffeeDto } from '../create-coffee.dto/create-coffee.dto';

// PartialType用于给将每个属性变为可选值 @IsOptional()
// 同时继承装饰器类型
export class UpdateCoffeeDto extends PartialType(CreateCoffeeDto) { }

Docker

根目录下配置docker-compose.yml文件:

1
2
3
4
5
6
7
8
9
10
11
version: "3"

services:
db:
image: postgres
restart: always
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: ashley122

启动镜像:

1
docker-compose up -d

docker拉取镜像地址

1
2
3
{
"registry-mirrors": ["https://dockerhub.icu"]
}

ORM

安装orm相关包:

  • @nestjs/typeorm typeorm
  • pg postgres相关包
    1
    npm i @nestjs/typeorm typeorm pg

app.module.ts中引入TypeOrmModule:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
CoffeeModule,
CakeModule,
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username:'postgres',
password:'ashley122',
database:'postgres',
autoLoadEntities:true,
// 仅用于开发环境,生产环境中尽量关闭
synchronize:true, // 实体与数据库同步
})
],
controllers: [AppController],
providers: [AppService],
})

将之前仅作为接口类型约束的entities下的实体定义文件,进行ORM模型化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {Entity, PrimaryGeneratedColumn, Column} from 'typeorm'

// 实体类描述符
@Entity('cake')
export class Cake{

// 主键描述符
@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@Column()
price: number;

// 可空描述
@Column('json', {nullable:true})
flavors: string[];
}

在cake子模块中注册内部实现的实体:

1
2
3
4
5
6
7
@Module({
// 每个子模块为自己注册实体
imports:[TypeOrmModule.forFeature([Cake])],
controllers:[CakeController],
providers:[CakeService]
})
export class CakeModule {}
repository

创建好Entity之后,ORM模型会自动映射成为数据库表,
在service中作为数据库进行关联:

1
2
3
4
5
6
7
8
9
@Injectable()
export class CakeService {
// 关联数据库
constructor(
// 数据库注入
@InjectRepository(Cake)
private readonly cakeRepository: Repository<Cake>
) {}
}

将之前的CURD操作对象迁移到数据库上:

  • 查询

    • find 查询所有
    • findOne 查询指定
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      /**
      * 查找全部
      */
      findAll(){
      return this.cakeRepository.find()
      }

      /**
      * 查找指定蛋糕
      */
      async findOne(id:number){
      const cake = await this.cakeRepository.findOne({
      where:{
      id:id
      }
      })
      // let cake = this.cakes.find(item=>item.id == id)
      if(!cake){
      throw new NotFoundException(`Cake #${id} is not find`)
      }
      return cake
      }
  • 新增

    • create 创建
    • save 保存
      1
      2
      3
      4
      5
      6
      7
      8
      /**
      * 新增蛋糕
      * @param cake
      */
      async create(createCakeDto: CreateCakeDto){
      const cake = this.cakeRepository.create(createCakeDto)
      return this.cakeRepository.save(cake)
      }
  • 更新

    • preload
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      /**
      * 修改蛋糕
      * @param id
      * @param cake
      */
      async update(id:number, updateCoffeeDto:UpdateCakeDto){
      // preload 更新 找不到指定id返回undefined
      const cake = await this.cakeRepository.preload({
      id:id,
      ...updateCoffeeDto
      })
      if(!cake){
      throw new NotFoundException(`Cake #${id} is not exit`)
      }
      return this.cakeRepository.save(cake)
      }
  • 删除

    • remove
      1
      2
      3
      4
      5
      6
      7
      8
      /**
      * 删除蛋糕
      * @param cake
      */
      async delete(id: number){
      const cake = await this.findOne(id)
      return this.cakeRepository.remove(cake)
      }

Relation

表间关联类型:

  • @OneToOne()
  • @OneToMany()
  • @ManyToOne()
  • @ManyToMany()

关联操作:

  • 创建关系

    创建一个新的Entity:Flavors,表示风味

    1
    nest g class cake/entities/flavor.entity --no-spec   

    新表加入模块定义中

    1
    @Module({ imports:[TypeOrmModule.forFeature([Cake,Flavor])] })

    使用描述符对cake.flavors进行关联:

    • @JoinTable() 标识关联属性所有者
    1
    2
    3
    4
    5
    6
    7
    @JoinTable()
    @ManyToMany(
    type => Flavor, // 关联表类型
    flavor => flavor.cakes, // 反向收集
    { cascade:true } // 级联关系
    )
    flavors: Flavor[];

    在被关联表中进行关联配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Entity()
    export class Flavor {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @ManyToMany(
    type => Cake,
    cake => cake.flavors
    )
    cakes: Cake[]
    }

  • 关联查询:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
findAll(){
return this.cakeRepository.find({
relations: ['flavors'], // 配置收集flavors
})
}
async findOne(id:number){
const cake = await this.cakeRepository.findOne({
where:{ id:id },
relations:['flavors']
})
if(!cake){
throw new NotFoundException(`Cake #${id} is not find`)
}
return cake
}
  • 级联插入

cake作为关系拥有表,当用新的flavor创建新的cake时,
新的flavor数据也会被级联创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* 获取/新增flavor
* @param name
* @returns
*/
private async preloadFlavorByName(name: string): Promise<Flavor>{
const existingFlavor = await this.flavorRepository.findOne({
where:{name:name}
})
if(existingFlavor){
return existingFlavor
}
return this.flavorRepository.create({name})
}
/**
* 新增蛋糕
* @param cake
*/
async create(createCakeDto: CreateCakeDto){
const flavors = await Promise.all(
createCakeDto.flavors.map(name=>this.preloadFlavorByName(name))
)
const cake = this.cakeRepository.create({
...createCakeDto,
flavors
})
return this.cakeRepository.save(cake)
}
/**
* 修改蛋糕
* @param id
* @param cake
*/
async update(id:number, updateCoffeeDto:UpdateCakeDto){
const flavors = updateCoffeeDto.flavors && (await Promise.all(
updateCoffeeDto.flavors.map(name=>this.preloadFlavorByName(name))
))
// preload 更新 找不到指定id返回undefined
const cake = await this.cakeRepository.preload({
id:id,
...updateCoffeeDto,
flavors
})
if(!cake){
throw new NotFoundException(`Cake #${id} is not exit`)
}
return this.cakeRepository.save(cake)
}

PaginationQueryDto

分页查询
  1. 为分页查询参数创建dto
1
nest g class common/dto/pagination-query.dto --no-spec
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { IsOptional, IsPositive } from "class-validator";
import {Type} from 'class-transformer';

export class PaginationQueryDto {

@IsOptional()
@IsPositive() // 正数
// @Type(()=>Number) // 强制转换为Number类型
limit: number;

@IsOptional()
@IsPositive()
// @Type(()=>Number)
offset: number;
}

可以在ValidationPipe中,配置强制类型转换:

1
2
3
4
5
6
app.useGlobalPipes(new ValidationPipe({
// ...
transformOptions:{
enableImplicitConversion: true
}
}))
  1. 修改controller和service中查询配置
1
2
3
4
5
// 查询所有蛋糕
@Get('getMenu')
getMenu(@Query() paginationQueryDto:PaginationQueryDto){
return this.cakeService.findAll(paginationQueryDto)
}
1
2
3
4
5
6
7
8
9
10
11
/**
* 查找全部
*/
findAll(paginationQueryDto: PaginationQueryDto){
const {limit, offset} = paginationQueryDto
return this.cakeRepository.find({
relations: ['flavors'], // 配置收集flavors
skip: offset, // 页数
take: limit, // 页大小
})
}

Transaction 事务

事务:一组操作作为一个整体,要么全部执行成功,要么全部不执行

使用事务控制,在service中声明typeorm.DataSource类属性:

1
2
3
4
5
// 关联数据库
constructor(
//...
private readonly connection: DataSource,
){}

使用QueryRunner和try/catch/finally进行事务处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 推荐蛋糕
async recommendCake(cake: Cake){
const queryRunner = this.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try{
cake.recommendations++
this.cakeRepository.preload(cake)
this.cakeRepository.save(cake)
const recommendEvent = new Event()
recommendEvent.name = 'recommend_cake'
recommendEvent.type = 'cake'
recommendEvent.payload = {cakeId: cake.id}
await queryRunner.commitTransaction()
}catch(err){
// 回滚事务
await queryRunner.rollbackTransaction()
}finally{
await queryRunner.release()
}
return cake
}

数据库迁移

迁移报错解决方法

首先需要在根目录下创建ormconfig.ts文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { DataSource } from 'typeorm';

export const connectionSource = new DataSource({
// migrationsTableName: 'migrations',
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'ashley122',
database: 'postgres',
entities: ['dist/**/*.entity.js'],
migrations: ['dist/migrations/*.js'],
subscribers:['src/subscriber/**/*.ts']
});
  • create 创建迁移脚本:

    1
    npx typeorm migration:create ./src/migrations/CoffeeRefactor  
  • generate 生成迁移脚本

    1
    npx typeorm migration:generate ./src/migrations/CoffeeRefactor -d ./dist/ormconfig.js
  • run 运行迁移脚本

    1
    npx typeorm migration:run -d ./dist/ormconfig.js
  • revert 恢复

    1
    npx typeorm migration:revert -d ./dist/ormconfig.js

对要修改的表对应的ORM进行修改,
再在生成的迁移文件中进行表操作:

1
2
3
4
5
6
7
8
// 实体类描述符
@Entity('cake')
export class Cake {
// ...
// 将name表段重命名为label
@Column()
label: string;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { MigrationInterface, QueryRunner } from "typeorm";

export class CakeRefactor1720149725474 implements MigrationInterface {

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALERT TABLE "cake" RENAME COLUMN "name" TO "label"`
)
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALERT TABLE "cake" RENAME COLUMN "label" TO "name"`
)
}

}

依赖注入 Dependency Injection

应用配置 Application Configuration

Next其它功能模块

Exception Filters 异常过滤器

Exception Filters用于处理应用中可能出现的异常

Pipes 管道

Pipes用于对输入的数据进行验证/转换

Guards 守卫

Guards用于对访问的用户进行身份验证

Interceptors 拦截器

Interceptors用于在接收到请求之前进行一些特殊操作