1.基础

1.1 IOC 控制反转

引入问题 - 控制反转

class A {
  // 类 A 依赖类 B 和类 C
  constructor(private a = new B(new C())) {
    this.a = a
  }
  getA() {
    this.a.getB()
  }
}

// 类 B 依赖类 C
class B {
  constructor(private c = new C()) {
    this.c = c
  }
  getB() {
    this.c.getC()
  }
}

class C {
  constructor() {}
  getC() {
    console.log('ai')
  }
}

const a = new A()
a.getA() // ai
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

解决问题 - 控制反转

专门新建一个容器类 Container,收集其他类的依赖并提供取出的方法。nestjs 中 service 层的 @Injectable 就是实现这一功能的

class Container {
  mo: {
    [key: string]: any
  }
  constructor() {
    this.mo = {}
  }
  // 收集依赖
  provide<V>(key: string, mo: V) {
    this.mo[key] = mo
  }
  // 获取依赖
  get(key: string) {
    return this.mo[key]
  }
}

class A {
  b: B
  constructor(mo: Container) {
    // 依赖类 B 就注入类 B
    this.b = mo.get('b')
  }
  getA() {
    return this.b.getB()
  }
}

class B {
  c: C
  constructor(mo: Container) {
    // 依赖类 C 就注入类 C
    this.c = mo.get('c')
  }
  getB() {
    return this.c.getC()
  }
}

class C {
  constructor() {}
  getC() {
    console.log('ai')
  }
}

const mo = new Container()
mo.provide('c', new C())
mo.provide('b', new B(mo))

const a = new A(mo)
a.getA() // ai
const b = new B(mo)
b.getB() // ai
const c = new C()
c.getC() // ai
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

1.2 前置知识 - 装饰器

类装饰器

target 是构造函数(类)本身

const doc: ClassDecorator = target => {
  target.prototype.sex = '0'
}

@doc
class Yuanke {
  public name: string
  constructor() {
    this.name = 'yuanke'
  }
}

console.log((<any>new Yuanke()).sex) // 0
1
2
3
4
5
6
7
8
9
10
11
12
13

属性装饰器

target 对静态属性来说是构造函数(Constructor),对实例属性来说是类的原型对象(Prototype);key 是属性名(这里是 name);属性装饰器没有第三个参数

const doc: PropertyDecorator = (target, key) => {
  console.log(target, key) // {} name
}

class Yuanke {
  // target -> Yuanke.prototype
  // key -> 'name'
  // descriptor -> undefined
  @doc
  public name: string
  constructor() {
    this.name = 'yuanke'
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

方法装饰器

target 对静态方法来说是构造函数(Constructor),对实例方法来说是类的原型对象(Prototype);key 是方法名;descriptor 是属性的属性描述符

const doc: MethodDecorator = (target, key, descriptor) => {
  console.log(target, key, descriptor) // {} getName (第三个参数打印如下)
}

class Yuanke {
  public name: string
  constructor() {
    this.name = 'yuanke'
  }
  // target -> Yuanke.prototype
  // key -> 'getName'
  // descriptor -> PropertyDescriptor类型
  @doc
  getName(name: string) {}
}

// {
//   value: [Function: getName], // 函数本身
//   writable: true, // 可写
//   enumerable: false, // 可枚举
//   configurable: true // 可配置
// }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

参数装饰器

target 对于静态属性来说是类的构造函数,对于实例属性是类的原型对象;key 是方法的名字;index 是参数在函数参数列表中的索引

const doc: ParameterDecorator = (target, key, index) => {
  console.log(target, key, index) // {} getName 1
}

class Yuanke {
  public name: string
  constructor() {
    this.name = 'yuanke'
  }
  // target -> Yuanke.prototype
  // key -> 'getName'
  // index -> 0
  getName(name: string, @doc age: number) {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

1.3 TypeScript 的元数据(Metadata)

引入了 reflect-metadata 后,就可以使用其封装在 Reflect 上的相关接口。装饰器函数中可以通过下列三种 metadatakey 获取类型信息:

  • design:type:属性类型
  • design:paramtypes:参数类型
  • design:returntype:返回值类型
  1. 安装:yarn add reflect-metadata
  2. 设置 tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
1
2
3
4
5
6
  1. 引入:import 'reflect-metadata'
  2. 实例:
import 'reflect-metadata'

const classDecorator: ClassDecorator = target => {
  console.log(Reflect.getMetadata('design:paramtypes', target))
}

const propertyDecorator: PropertyDecorator = (target, key) => {
  console.log(Reflect.getMetadata('design:type', target, key))
  console.log(Reflect.getMetadata('design:paramtypes', target, key))
  console.log(Reflect.getMetadata('design:returntype', target, key))
}

@classDecorator
class Demo {
  innerValue: string
  constructor(innerValue: string) {
    this.innerValue = innerValue
  }
  @propertyDecorator
  demoParam: string = 'demoParam'
}

/**
 * 打印值:
 * [Function: Function] [ [Function: String] ] [Function: String]
 * [Function: String] undefined undefined
 * [ [Function: String] ]
 */
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

各种装饰器所含有的(非 undefined)元数据类型

  • 类装饰器:design:paramtypes
  • 属性装饰器:design:type
  • 参数装饰器、方法装饰器:design:typedesign:paramtypesdesign:returntype

1.4 依赖注入(DI)

@Injectable 将装饰的 class 的所有参数(参数中调用了其他类)收集起来

简单依赖注入实例

import 'reflect-metadata'

// 构造函数类型
type Constructor<T = any> = new (...args: any[]) => T

// 类装饰器:用于标识类是否要注入
const Injectable = (): ClassDecorator => target => {}

// 需要注入的类
class InjectService {
  a = 'inject'
}

// 被注入的类
@Injectable()
class DemoService {
  constructor(public injectService: InjectService) {}
  test() {
    console.log(this.injectService.a)
  }
}

// 依赖注入函数 Factory
const Factory = <T>(target: Constructor<T>): T => {
  // 获取 target 类的构造函数参数 providers
  const providers = Reflect.getMetadata('design:paramtypes', target)
  // 将参数依次实例化
  const args = providers.map((provider: Constructor) => new provider())
  // 将实例化的数组作为 target 类的参数,并返回 target 的实例
  return new target(...args)
}

Factory(DemoService).test() // inject
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

完整版

import 'reflect-metadata'

// ioc 容器
const classPool: Array<Function> = []

// 注册该类进入容器
const Injectable = (): ClassDecorator => _constructor => {
  // 获取 Demo 类中构造函数参数 paramTypes
  const paramTypes: Array<Function> = Reflect.getMetadata(
    'design:paramtypes',
    _constructor
  )
  // 已注册
  if (!classPool.includes(_constructor)) return
  for (let param of paramTypes) {
    if (param === _constructor) throw new Error('不能依赖自己')
    else if (!classPool.includes(param)) throw new Error(`${param} 没有被注册`)
  }
  // 注册
  classPool.push(_constructor)
}

// 实例化工厂
const classFacory = <T>(_constructor: { new (...args: Array<any>): T }): T => {
  const paramTypes: Array<Function> = Reflect.getMetadata(
    'design:paramtypes',
    _constructor
  )
  // 参数实例化
  const paramInstance = paramTypes.map(param => {
    // 依赖的类必须全部进行注册
    if (classPool.includes(param)) throw new Error(`${param} 没有被注册`)
    // 参数还有依赖
    else if (param.length) return classFacory(param as any)
    else return new (param as any)()
  })
  return new _constructor(...paramInstance)
}

@Injectable()
class C {
  constructor() {}
  sayHello() {
    console.log('hello')
  }
}

@Injectable()
class B {
  constructor(private c: C) {}
  sayHello() {
    this.c.sayHello()
  }
}

@Injectable()
class A {
  constructor(private b: B) {
    b.sayHello()
  }
}

// 产生实例
const a: A = classFacory(A) // hello
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
61
62
63
64

1.5 使用装饰器封装一个 GET 请求

import axios from 'axios'

const Get = (url: string): MethodDecorator => {
  return (target, key, descriptor: PropertyDescriptor) => {
    const fnc = descriptor.value
    axios
      .get(url)
      .then(res => {
        fnc(res, {
          status: 200,
          success: true
        })
      })
      .catch(e => {
        fnc(e, {
          status: 500,
          success: false
        })
      })
  }
}

class Controller {
  constructor() {}
  @Get('https://jsonplaceholder.typicode.com/todos/1')
  getList(res: any, status: any) {
    console.log(res.data, status) // { userId: 1, id: 1, title: 'delectus aut autem', completed: false } { status: 200, success: true }
  }
}
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

1.6 vscode 行尾序列问题

.editorconfig

root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
1
2
3
4
5
6
7
8
9

2.提供者 provider

2.1 开始

  1. 安装:pnpm add -g @nestjs/cli nodemon ts-node
  2. 初始化:nest 命令可以查看快捷键
  3. 创建新项目:nest new nest-blog

2.2 基本文件介绍

app.module.ts

import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'

@Module({
  imports: [],
  // 控制器
  controllers: [AppController],
  // service
  providers: [AppService]
})
export class AppModule {}
1
2
3
4
5
6
7
8
9
10
11
12

app.controller.ts

import { Controller, Get } from '@nestjs/common'
import { AppService } from './app.service'

// 像这样,访问 localhost:3000/a/b 才能访问到
@Controller('a')
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('b')
  getHello(): string {
    return this.appService.getHello()
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

app.service.ts

import { Injectable } from '@nestjs/common'

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!'
  }
}
1
2
3
4
5
6
7
8

2.3 服务提供者注册

  1. 使用命令:nest g s hd --no-spec -d-d 是预览,加上 --flat 就不会有子目录
  2. 输入命令后,app.module.ts 会自动在 providers 里面添加 HdService
  3. 在命令新建的 hd.servece.ts 中写入逻辑:
import { Injectable } from '@nestjs/common'

@Injectable()
export class HdService {
  hd() {
    return 'hd service hd method'
  }
}
1
2
3
4
5
6
7
8
  1. app.service.ts 中调用:
import { Injectable } from '@nestjs/common'
import { HdService } from './hd/hd.service'

// @Injectable()和 private readonly hd: HdService 是相辅相成的
@Injectable()
export class AppService {
  constructor(private readonly hd: HdService) {}
  findOne() {
    return this.hd.hd()
  }
}
1
2
3
4
5
6
7
8
9
10
11

2.4 提供者类的注册方式

2.3 中,app.module.tsproviders(容器、提供者) 是这样写的:providers: [AppService],这其实是简化版

非简化版

providers: [
  {
    provide: AppService,
    useClass: AppService,
  },
],
1
2
3
4
5
6

这时候,如果将 provider 中的 AppService 改成 hd,那么 app.controller.ts 中就不能写 constructor(private readonly appService: AppService)了,因为这里的 AppService 有两层含义:1.ts 的类型;2.一个容器内提供者的名字

app.controller.ts

import { Controller, Get, Inject } from '@nestjs/common'

// 像这样,访问 localhost:3000/a/b 才能访问到
@Controller()
export class AppController {
  // AppService 的两种含义:1.ts 的类型;2.名字
  constructor(
    @Inject('hd')
    private readonly // 由于上面 inject 了名字为 hd 的对应的 useClass 为 AppService 的服务模块,所以这里的 appService 才能正常使用而不会报错(这整页代码都没有 AppService)
    appService
  ) {}

  @Get()
  getHello(): string {
    return this.appService.findOne()
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

上述代码中,下面的 this.appService.findOne() 没有代码提示与类型检查,所以最好还是给 appService 标上类型 。所以,AppService 单独命名十分不明智,脱裤子放屁

import { AppService } from './app.service';

constructor(
  @Inject('hd')
  private readonly appService: AppService,
) {}
1
2
3
4
5
6

2.5 基本类型的提供者注册

app.module.ts

providers: [
  AppService,
  {
    provide: 'appName',
    useValue: '后盾人'
  }
]
1
2
3
4
5
6
7

app.controller.ts

import { Controller, Get, Inject } from '@nestjs/common'
import { AppService } from './app.service'

// 像这样,访问 localhost:3000/a/b 才能访问到
@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    // appName 即是 useValue 里的值
    @Inject('appName') private appName: string
  ) {}

  @Get()
  getHello(): string {
    return this.appName // 输出后盾人
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

对象类型的提供者

app.module.ts

import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'

@Module({
  imports: [],
  // 控制器
  controllers: [AppController],
  // service
  providers: [
    AppService,
    {
      provide: 'appName',
      useValue: {
        name: 'yuanke',
        author: 'yuankeke'
      }
    }
  ]
})
export class AppModule {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

app.service.ts

import { Inject, Injectable } from '@nestjs/common'

@Injectable()
export class AppService {
  constructor(@Inject('appName') private config: Record<string, unknown>) {}
  findOne() {
    return 'app findOne method' + this.config
  }
}
1
2
3
4
5
6
7
8
9

2.6 动态注册服务提供者

  1. 安装:pnpm add dotenv
  2. 跟目录新建 .env,写入:NODE_ENV=development
  3. app.module.ts
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
// 要想使用 import dotenv from 'dotenv' 或 import path from 'path',需配置 tsconfig.json:"esModuleInterop": true
import { config } from 'dotenv'
import { join } from 'path'
import { DevService } from './dev.service'
config({ path: join(__dirname, '../.env') })
console.log(process.env.NODE_ENV) // development

const hdService = {
  provide: 'AppService',
  useClass: process.env.NODE_ENV === 'development' ? DevService : AppService
}

@Module({
  imports: [],
  // 控制器
  controllers: [AppController],
  // service
  providers: [hdService]
})
export class AppModule {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  1. app.controller.ts
import { Controller, Get, Inject } from '@nestjs/common'

// 像这样,访问 localhost:3000/a/b 才能访问到
@Controller()
export class AppController {
  constructor(
    @Inject('hdService')
    private hd
  ) {}

  @Get()
  getHello(): string {
    return this.hd.get()
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

2.7 使用工厂函数注册提供者

app.module.ts

import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { ConfigService } from './config.service'
import { DbService } from './db.service'

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    AppService,
    ConfigService,
    {
      // DbService 依赖 ConfigService 的值,这时候可以利用工厂函数传值
      provide: 'DbService',
      // 引入 ConfigService 依赖
      inject: ['ConfigService'],
      useFactory(configService) {
        return new DbService(configService)
      }
    }
  ]
})
export class AppModule {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

db.service.ts

import { Injectable } from '@nestjs/common'

@Injectable()
export class DbService {
  constructor(private options: Record<string, any>) {}
  public connect() {
    return `<h1 style="background: red">连接数据库 - ${this.options.url}</h1>`
  }
}
1
2
3
4
5
6
7
8
9

config.service.ts

import dotenv from 'dotenv'
import path from 'path'
import { developmentConfig } from './config/development.config'
import { productionConfig } from './config/production.config'

dotenv.config({ path: path.join(__dirname, '../.env') })

export const ConfigService = {
  provide: 'ConfigService',
  useValue:
    process.env.NODE_ENV === 'development' ? developmentConfig : productionConfig
}
1
2
3
4
5
6
7
8
9
10
11
12

2.8 模块共享服务

importsexports 都要分别进行设置

  1. 创建模块:nest g mo hd -d
  2. 创建服务:nest g s hd --no-spec -d
  3. 创建控制器:nest g co hd --no-spec -d

当一个模块想使用另一个模块的 service。使用方法如下:

提供模块的 test.module.ts

import { Module } from '@nestjs/common'
import { TestService } from './test.service'

@Module({
  providers: [
    TestService,
    {
      provide: 'test',
      useValue: '测试的 test 服务'
    }
  ],
  // 允许暴露 Test 模块
  exports: [TestService, 'test']
})
export class TestModule {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

引入模块的 hd.module.ts

import { Module } from '@nestjs/common'
import { HdService } from './hd.service'
import { HdController } from './hd.controller'
import { TestModule } from 'src/test/test.module'

@Module({
  // 引入 Test 模块
  imports: [TestModule],
  providers: [HdService],
  controllers: [HdController]
})
export class HdModule {}
1
2
3
4
5
6
7
8
9
10
11
12

使用另一模块的服务 hd.controller.ts

import { Controller, Get, Inject } from '@nestjs/common'
import { TestService } from 'src/test/test.service'

@Controller('hd')
export class HdController {
  constructor(private readonly test: TestService, @Inject('test') private testValue) {}
  @Get()
  show() {
    return this.test.get() + this.testValue
  }
}
1
2
3
4
5
6
7
8
9
10
11

2.9 异步服务提供者

app.module.ts

import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { ConfigService } from './config.service'
import { HdModule } from './hd/hd.module'
import { TestModule } from './test/test.module'

@Module({
  imports: [HdModule, TestModule],
  controllers: [AppController],
  providers: [
    AppService,
    ConfigService,
    {
      provide: 'DbService',
      inject: ['ConfigService'],
      useFactory: async () => {
        return new Promise(r => {
          setTimeout(() => {
            r('后盾人')
          }, 3000)
        })
      }
    }
  ]
})
export class AppModule {}
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

app.controller.ts

import { Controller, Get, Inject } from '@nestjs/common'
import { AppService } from './app.service'

// 像这样,访问 localhost:3000/a/b 才能访问到
@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    @Inject('DbService')
    private readonly dbService: string
  ) {}

  @Get()
  getHello(): string {
    return this.dbService
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

3.配置项模块

3.1 读取模块配置项

  1. 安装依赖包:pnpm add prisma-binding @prisma/client mockjs @nestjs/config class-validator class-transformer argon2 @nestjs/passport passport passport-local @nestjs/jwt passport-jwt lodash multer dayjs express
  2. 安装依赖包:pnpm add -D prisma typescript @types/node @types/mockjs @nestjs/mapped-types @types/passport-local @types/passport-jwt @types/express @types/lodash @types/multer

读取配置文件内容

config.service.ts

import { Injectable } from '@nestjs/common'
import { readdirSync } from 'fs'
import path from 'path'

@Injectable()
export class ConfigService {
  constructor() {
    // 读取 configure 中的 module.exports 导出的配置
    const config = { path: path.resolve(__dirname, '../configure') }
    readdirSync(config.path).map(async file => {
      if (file.slice(-2) === 'js') {
        const module = await import(path.resolve(config.path, file))
        console.log(module.default())
        /**
         * 读取结果
         * { app: { name: '后盾人' } }
         * { database: { host: 'localhost' } }
         */
      }
    })
  }
  get() {
    return 'abc'
  }
}
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

成功读取文件内容

app.config.ts 成功调用该服务,读出 configure 目录下的 app.ts 里的 app.name

config.service.ts

import { Injectable } from '@nestjs/common'
import { readdirSync } from 'fs'
import path from 'path'

@Injectable()
export class ConfigService {
  config = {} as any
  constructor() {
    // 读取 configure 中的 module.exports 导出的配置
    const config = { path: path.resolve(__dirname, '../configure') }
    readdirSync(config.path).map(async file => {
      if (file.slice(-2) === 'js') {
        const module = await import(path.resolve(config.path, file))
        this.config = { ...this.config, ...module.default() }
      }
    })
  }
  // get('app.name')
  get(path: string) {
    return path.split('.').reduce((config, name) => config[name], this.config)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

3.2 模块间的调用

  1. 局部组件:
  • 被调用模块需要 exports: true,调用组件需要 imports: [ConfigModule] 引入模块
  • 使用被调用模块的 service
import { Controller, Get } from '@nestjs/common'
import { ConfigService } from 'src/config/config.service'

@Controller('article')
export class ArticleController {
  // 引入 ConfigService,以便读取 configure 文件夹中的文件内容
  constructor(private readonly config: ConfigService) {}
  @Get()
  index() {
    return 'index article' + this.config.get('app.name')
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
  1. 全局组件:
  • 方法一:被调用组件的 xx.module.tsclass 上面添加 @Global() 且仍需 exports 服务,调用组件无需 imports 模块
  • 方法二:静态模块:
import { DynamicModule, Global, Module } from '@nestjs/common'
import { ConfigService } from './config.service'

@Global()
@Module({
  providers: [ConfigService],
  exports: [ConfigService]
})
export class ConfigModule {
  static register(options: { path: string }): DynamicModule {
    return {
      module: ConfigModule,
      providers: [
        {
          provide: 'CONFIG_OPTIONS',
          useValue: options
        }
      ],
      global: true
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

3.2 动态模块注册语法

  1. 将被调用组件的模块设置为静态模块,以便调用组件的模块能传入参数(在这里是传递要读取文件的目录)
import { DynamicModule, Global, Module } from '@nestjs/common'
import { ConfigService } from './config.service'

@Global()
@Module({
  providers: [ConfigService],
  exports: [ConfigService]
})
export class ConfigModule {
  static register(options: { path: string }): DynamicModule {
    return {
      module: ConfigModule,
      providers: [
        {
          provide: 'CONFIG_OPTIONS',
          useValue: options
        }
      ]
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  1. 调用组件的模块:
import { Module } from '@nestjs/common'
import path from 'path'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { ArticleModule } from './article/article.module'
import { ConfigModule } from './config/config.module'

@Module({
  imports: [
    ConfigModule.register({ path: path.resolve(__dirname, './configure') }),
    ArticleModule
  ],
  controllers: [AppController],
  providers: [AppService]
})
export class AppModule {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  1. 被调用模块在 config.module.ts 使用 providers 保存组件模块传来的 path,并在 config.service.ts 中使用:
import { Inject, Injectable, Optional } from '@nestjs/common'
import { readdirSync } from 'fs'
import path from 'path'

@Injectable()
export class ConfigService {
  // 有 @Injectable 后会默认把 config 当做服务进行实例化注册,故需要声明 @Optional() 避免
  constructor(
    @Inject('CONFIG_OPTIONS') options: { path: string },
    @Optional() private config = {}
  ) {
    readdirSync(options.path).map(async file => {
      if (file.slice(-2) === 'js') {
        const module = await import(path.resolve(options.path, file))
        this.config = { ...this.config, ...module.default() }
      }
    })
  }
  // get('app.name')
  get(path: string) {
    return path.split('.').reduce((config, name) => config[name], this.config)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  1. app.controller.ts
import { Controller, Get } from '@nestjs/common'
import { ConfigService } from './config/config.service'

@Controller()
export class AppController {
  constructor(private readonly config: ConfigService) {}

  @Get()
  getHello(): any {
    return this.config.get('app.name') // 后盾人
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

4.mysql 数据库

4.1 mysql

  1. 使用 npx prisma init 生成初始化目录
  2. .env 配置:
DATABASE_URL="mysql://root:123456@localhost:3306/nest-blog"
1
  1. 写入表:
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model user {
  // @id 声明主键 -- @default 声明默认值
  id        BigInt   @id @default(autoincrement()) @db.UnsignedBigInt
  email     String
  password  String
  avatar    String?
  github    String?
  douyin    String?
  weibo     String?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model category {
  id       BigInt    @id @default(autoincrement()) @db.UnsignedBigInt
  title    String
  // 主表 category
  articles article[]
}

model article {
  id         BigInt   @id @default(autoincrement()) @db.UnsignedBigInt
  title      String
  content    String   @db.Text
  thumb      String
  author     String
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt
  category   category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
  categoryId BigInt   @db.UnsignedBigInt
}
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
  1. 输入:npx prisma migrate dev
  2. 执行后,自动生成 nest-blog 数据库。(这里有个小坑,在数据库管理软件中使用 use 反引号nest-blog反引号 才能选中数据库)

4.2 数据填充的环境配置

数据库 seeding - Prisma 中文文档open in new window

  1. 配置 package.json
"prisma": {
  "seed": "ts-node prisma/seed.ts"
}
1
2
3
  1. 输入:npx prisma db seed 即可执行上述命令
  2. seed.ts 写入:
import { PrismaClient } from '@prisma/client'
import { Random } from 'mockjs'

const prisma = new PrismaClient()

// mock.js
async function run() {
  await prisma.user.create({
    data: {
      email: Random.email(),
      password: Random.string(),
      github: Random.url(),
      avatar: Random.image('300*300')
    }
  })
}

run()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  1. 输入:npx prisma migrate reset 即可修改数据库

4.3 mock 实现数据填充

  1. 新增帮助函数:
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()

export async function create(
  count = 1,
  callback: (prisma: PrismaClient) => Promise<void>
) {
  for (let i = 0; i < count; i += 1) {
    await callback(prisma)
  }
}
1
2
3
4
5
6
7
8
9
10
11
  1. seek.ts
import { article } from './seeds/article'
import { category } from './seeds/category'
import { user } from './seeds/user'

async function run() {
  user()
  // 要阻塞一下栏目,以免 article 表获取不到 categoryId
  await category()
  article()
}

run()
1
2
3
4
5
6
7
8
9
10
11
12
  1. user.ts
import { PrismaClient } from '@prisma/client'
import { Random } from 'mockjs'
import { create } from '../helper'

export function user() {
  create(30, async (prisma: PrismaClient) => {
    await prisma.user.create({
      data: {
        email: Random.email(),
        password: Random.string(),
        github: Random.url(),
        avatar: Random.image()
      }
    })
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  1. category.ts
import { PrismaClient } from '@prisma/client'
import { Random } from 'mockjs'
import { create } from '../helper'

export async function category() {
  await create(10, async (prisma: PrismaClient) => {
    await prisma.category.create({
      data: {
        title: Random.ctitle()
      }
    })
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  1. article.ts
import { PrismaClient } from '@prisma/client'
import { Random } from 'mockjs'
import { create } from '../helper'
import _ from 'lodash'

export function article() {
  create(30, async (prisma: PrismaClient) => {
    await prisma.article.create({
      data: {
        title: Random.ctitle(),
        content: Random.cparagraph(10, 50),
        categoryId: _.random(1, 10, false)
      }
    })
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

5.配置管理

5.1 ConfigModule

  1. 安装:pnpm add prisma-binding @prisma/client mockjs @nestjs/config class-validator class-transformer argon2 @nestjs/passport passport passport-local @nestjs/jwt passport-jwt lodash multer dayjs express
  2. 安装:pnpm add -D prisma typescript
  3. 安装:pnpm add -D prisma typescript @types/node @types/mockjs @nestjs/mapped-types @types/passport-local @types/passport-jwt @types/express @types/lodash @types/multer

基本使用

  1. app.module.ts 引入自带的 ConfigModule
import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { AppController } from './app.controller'
import { AppService } from './app.service'

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true
    })
  ],
  controllers: [AppController],
  providers: [AppService]
})
export class AppModule {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  1. .env 文件中写入:APP_NAME=后盾人,就可以在 app.controller.ts 中直接使用:
import { Controller, Get } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { AppService } from './app.service'

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    private readonly config: ConfigService
  ) {}

  @Get()
  getHello(): string {
    return this.config.get('APP_NAME')
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

读取文件配置

app.config.ts

export default () => ({
  app: {
    name: '后盾人'
  }
})
1
2
3
4
5

app.module.ts

import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import appConfig from './app.config'
import { AppController } from './app.controller'
import { AppService } from './app.service'

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      load: [appConfig]
    })
  ],
  controllers: [AppController],
  providers: [AppService]
})
export class AppModule {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

app.controller.ts

import { Controller, Get } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { AppService } from './app.service'

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    private readonly config: ConfigService
  ) {}

  @Get()
  getHello(): string {
    console.log(process.env)
    return this.config.get('app')
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

5.2 基于命名空间配置

database.config.ts

import { registerAs } from '@nestjs/config'

export default registerAs('database', () => ({
  host: 'localhost',
  port: 3306,
  password: 123
}))
1
2
3
4
5
6
7

index.ts

import appConfig from './app.config'
import databaseConfig from './database.config'
import uploadConfig from './upload.config'

export default [appConfig, uploadConfig, databaseConfig]
1
2
3
4
5

app.module.ts

import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import config from './config'

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      load: [...config]
    })
  ],
  controllers: [AppController],
  providers: [AppService]
})
export class AppModule {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

自己写的类型提示 - app.controller.ts

import { Controller, Get, Inject } from '@nestjs/common'
import databaseConfig from './config/database.config'

@Controller()
export class AppController {
  constructor(
    @Inject(databaseConfig.KEY)
    private database: any
  ) {}

  @Get()
  getHello(): string {
    type getType<T extends () => any> = T extends () => infer U ? U : T
    type databaseType = getType<typeof databaseConfig>
    return (this.database as databaseType).host
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

nestjs 自带的类型提示 - app.controller.ts

import { Controller, Get, Inject } from '@nestjs/common'
import { ConfigType } from '@nestjs/config'
import databaseConfig from './config/database.config'

@Controller()
export class AppController {
  constructor(
    @Inject(databaseConfig.KEY)
    private database: ConfigType<typeof databaseConfig>
  ) {}

  @Get()
  getHello(): any {
    return this.database.host
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

6.管道与验证

6.1 实现转换数值管道

app.controller.ts

import { Controller, Get, Param } from '@nestjs/common'
import { PrismaClient } from '@prisma/client'
import { AppService } from './app.service'
import { HdPipe } from './hd/hd.pipe'

@Controller()
export class AppController {
  prisma: PrismaClient
  constructor(private readonly appService: AppService) {
    this.prisma = new PrismaClient()
  }

  @Get(':id')
  getHello(@Param('id', HdPipe) id: number) {
    return this.prisma.article.findUnique({
      // 如果没有管道会报错,因为 id 默认是 string 类型数据
      where: { id }
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

hd.pipe.ts

import {
  ArgumentMetadata,
  BadRequestException,
  Injectable,
  PipeTransform
} from '@nestjs/common'

@Injectable()
export class HdPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    // throw new BadRequestException('参数错误');
    if (metadata.metatype === Number) {
      return +value
    }
    return value
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

使用 nestjs 自带的转换 int 的管道

@Get(':id')
getHello(@Param('id', ParseIntPipe) id: number) {
  return this.prisma.article.findUnique({
    // 如果没有管道会报错,因为 id 默认是 string 类型数据
    where: { id },
  });
}
1
2
3
4
5
6
7

在方法和类上也能使用管道

@Get(':id')
@UsePipes(HdPipe)
getHello(@Param('id') id: number) {
  return this.prisma.article.findUnique({
    // 如果没有管道会报错,因为 id 默认是 string 类型数据
    where: { id },
  });
}
1
2
3
4
5
6
7
8
import { Controller, Get, Param, UsePipes } from '@nestjs/common'
import { PrismaClient } from '@prisma/client'
import { AppService } from './app.service'
import { HdPipe } from './hd/hd.pipe'

@Controller()
@UsePipes(HdPipe)
export class AppController {
  prisma: PrismaClient
  constructor(private readonly appService: AppService) {
    this.prisma = new PrismaClient()
  }

  @Get(':id')
  getHello(@Param('id') id: number) {
    return this.prisma.article.findUnique({
      // 如果没有管道会报错,因为 id 默认是 string 类型数据
      where: { id }
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

甚至于定义到模块里也没问题

app.module.ts

import { Module } from '@nestjs/common'
import { APP_PIPE } from '@nestjs/core'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { HdPipe } from './hd/hd.pipe'

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: APP_PIPE,
      useClass: HdPipe
    }
  ]
})
export class AppModule {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

甚至在 main.ts 中也能使用

import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { HdPipe } from './hd/hd.pipe'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  app.useGlobalPipes(new HdPipe())
  await app.listen(3000)
}
bootstrap()
1
2
3
4
5
6
7
8
9
10

默认管道

@Get()
// 当 id 没有传参的时候,默认值就是 1
getHello(@Param('id', new DefaultValuePipe(1), ParseIntPipe) id: number) {
  return this.prisma.article.findUnique({
    // 如果没有管道会报错,因为 id 默认是 string 类型数据
    where: { id },
  });
}
1
2
3
4
5
6
7
8

6.2 使用管道实现验证

最简单的管道验证

app.controller.ts

@Post('store')
add(@Body(HdPipe) dto: Record<string, any>) {
  return dto;
}
1
2
3
4

hd.pipe.ts

import {
  ArgumentMetadata,
  BadRequestException,
  Injectable,
  PipeTransform
} from '@nestjs/common'

@Injectable()
export class HdPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    if (!value.title) {
      throw new BadRequestException('标题不能为空')
    }
    if (!value.content) {
      throw new BadRequestException('内容不能为空')
    }
    return value
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

使用 class-validator 实现验证

app.controller.ts

@Post('store')
add(@Body(HdPipe) dto: CreateArticleDto) {
  return dto;
}
1
2
3
4

src/dto/create.article.dto.ts

import { IsNotEmpty, Length } from 'class-validator'

export default class CreateArticleDto {
  @IsNotEmpty({ message: '标题不能为空' })
  @Length(10, 100, { message: '标题不能少于 10 个字' })
  title: string
  @IsNotEmpty({ message: '内容不能为空' })
  content: string
}
1
2
3
4
5
6
7
8
9

hd.pipe.ts

import {
  ArgumentMetadata,
  BadRequestException,
  Injectable,
  PipeTransform
} from '@nestjs/common'
import { plainToInstance } from 'class-transformer'
import { validate } from 'class-validator'

@Injectable()
export class HdPipe implements PipeTransform {
  async transform(value: any, metadata: ArgumentMetadata) {
    // Converts plain (literal) object to class (constructor) object. Also works with arrays.
    const obj = plainToInstance(metadata.metatype, value)
    const errors = await validate(obj)

    if (errors.length) {
      throw new BadRequestException('表单验证错误')
    }
    return value
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

hd.pipe.ts 更好写法

import {
  ArgumentMetadata,
  HttpException,
  HttpStatus,
  Injectable,
  PipeTransform
} from '@nestjs/common'
import { plainToInstance } from 'class-transformer'
import { validate } from 'class-validator'

@Injectable()
export class HdPipe implements PipeTransform {
  async transform(value: any, metadata: ArgumentMetadata) {
    // Converts plain (literal) object to class (constructor) object. Also works with arrays.
    const obj = plainToInstance(metadata.metatype, value)
    const errors = await validate(obj)

    if (errors.length) {
      const messages = errors.map(error => ({
        name: error.property,
        message: Object.values(error.constraints)
      }))
      throw new HttpException(messages, HttpStatus.BAD_REQUEST)
    }
    return value
  }
}
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

使用系统自带的 - main.ts

app.useGlobalPipes(new Validate())
1

validate.ts

import { ValidationError, ValidationPipe } from '@nestjs/common'

export class Validate extends ValidationPipe {
  protected mapChildrenToValidationErrors(
    error: ValidationError,
    parentPath?: string
  ): ValidationError[] {
    const errors = super.mapChildrenToValidationErrors(error, parentPath)
    errors.map(error => {
      for (const key in error.constraints) {
        error.constraints[key] = error.property + '-' + error.constraints[key]
      }
    })
    return errors
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

6.2 使用过滤器处理验证异常

  1. 使用 nest g f validate-exception --no-spec 生成 validate-exception.filter.ts 文件
  2. validate-exception.filter.ts
import {
  ArgumentsHost,
  BadRequestException,
  Catch,
  ExceptionFilter,
  HttpStatus
} from '@nestjs/common'

@Catch()
export class ValidateExceptionFilter<T> implements ExceptionFilter {
  catch(exception: T, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse()
    // 当是参数错误的时候就进行特殊返回
    if (exception instanceof BadRequestException) {
      const responseObject = exception.getResponse() as any
      return response.status(HttpStatus.UNPROCESSABLE_ENTITY).json({
        code: HttpStatus.UNPROCESSABLE_ENTITY,
        message: responseObject.message.map(error => {
          const info = error.split('-')
          return {
            field: [info[0], info[1]]
          }
        })
      })
    }
    return response
  }
}
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

6.4 自定义密码比对验证规则

auth.controller.ts

import { Body, Controller, Post } from '@nestjs/common'
import RegisterDto from './dto/register.dto'

@Controller('auth')
export class AuthController {
  @Post('register')
  register(@Body() dto: RegisterDto) {
    return dto
  }
}
1
2
3
4
5
6
7
8
9
10

is-confirmed.rules.ts

import {
  ValidationArguments,
  ValidatorConstraint,
  ValidatorConstraintInterface
} from 'class-validator'

@ValidatorConstraint()
export class IsConfirmed implements ValidatorConstraintInterface {
  async validate(value: string, args?: ValidationArguments) {
    // console.log(value, args);
    return value === args.object[args.property + '_confirmed']
  }
  defaultMessage(args?: ValidationArguments) {
    return '比对失败'
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

register.dto.ts

import { IsNotEmpty, Validate } from 'class-validator'
import { IsConfirmed } from 'src/rules/is-confirmed.rule'

export default class RegisterDto {
  @IsNotEmpty({ message: '用户名不能为空' })
  name: string
  @IsNotEmpty({ message: '密码不能为空' })
  @Validate(IsConfirmed, { message: '确认密码输入错误' })
  password: string
}
1
2
3
4
5
6
7
8
9
10

6.5 使用装饰器实现用户唯一验证

is-not-exists.rule.ts

import { PrismaClient } from '@prisma/client'
import {
  registerDecorator,
  ValidationArguments,
  ValidationOptions
} from 'class-validator'

export function isNotExistsRule(table: string, validationOptions?: ValidationOptions) {
  return function (object: Record<string, any>, propertyName: string) {
    registerDecorator({
      name: 'IsNotExistsRule',
      target: object.constructor,
      propertyName: propertyName,
      constraints: [table],
      options: validationOptions,
      validator: {
        async validate(value: string, args: ValidationArguments) {
          console.log(propertyName, args.value)

          const prisma = new PrismaClient()
          const user = await prisma[table].findFirst({
            where: {
              [propertyName]: args.value
            }
          })
          return !Boolean(user)
        }
      }
    })
  }
}
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

register.dto.ts

import { IsNotEmpty, Validate } from 'class-validator';
import { IsConfirmed } from 'src/rules/is-confirmed.rule';
import { isNotExistsRule } from 'src/rules/is-not-exists.rule';

export default class RegisterDto {
  @IsNotEmpty({ message: '用户名不能为空' })
  @isNotExistsRule('user', { message: '用户已经存在' })
  name: string;
  @IsNotEmpty({ message: '密码不能为空' })
  @Validate(IsConfirmed, { message: '确认密码输入错误' })
  password: string;
1
2
3
4
5
6
7
8
9
10
11

7.登录注册

7.1 实现简单的注册

register.dto.ts

import { IsNotEmpty } from 'class-validator'

export default class RegisterDto {
  @IsNotEmpty({ message: '用户名不能为空' })
  name: string
  @IsNotEmpty({ message: '密码不能为空' })
  password: string
}
1
2
3
4
5
6
7
8

auth.controller.ts

import { Body, Controller, Post } from '@nestjs/common'
import { AuthService } from './auth.service'
import RegisterDto from './dto/register.dto'

@Controller('auth')
export class AuthController {
  constructor(private readonly auth: AuthService) {}
  @Post('register')
  register(@Body() dto: RegisterDto) {
    return this.auth.register(dto)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

auth.controller.ts

import { Injectable } from '@nestjs/common'
import { PrismaService } from 'src/prisma/prisma.service'
import RegisterDto from './dto/register.dto'
import { hash } from 'argon2'

@Injectable()
export class AuthService {
  constructor(private prisma: PrismaService) {}
  async register(dto: RegisterDto) {
    const password = await hash(dto.password)
    const user = this.prisma.user.create({
      data: {
        name: dto.name,
        password
      }
    })
    // 返回给用户的密码需要删除掉
    delete (await user).password
    return user
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

7.2 实现简单的登录

校验规则在下面

auth.controller.ts

import { Body, Controller, Post } from '@nestjs/common'
import { AuthService } from './auth.service'
import LoginDto from './dto/login.dto'
import RegisterDto from './dto/register.dto'

@Controller('auth')
export class AuthController {
  constructor(private readonly auth: AuthService) {}
  @Post('register')
  register(@Body() dto: RegisterDto) {
    return this.auth.register(dto)
  }
  @Post('login')
  login(@Body() dto: LoginDto) {
    return this.auth.login(dto)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

auth.service.ts

import { BadRequestException, Injectable } from '@nestjs/common'
import { PrismaService } from 'src/prisma/prisma.service'
import RegisterDto from './dto/register.dto'
import { hash, verify } from 'argon2'
import LoginDto from './dto/login.dto'

@Injectable()
export class AuthService {
  constructor(private prisma: PrismaService) {}
  async register(dto: RegisterDto) {
    const password = await hash(dto.password)
    const user = this.prisma.user.create({
      data: {
        name: dto.name,
        password
      }
    })
    // 返回给用户的密码需要删除掉
    delete (await user).password
    return user
  }
  async login(dto: LoginDto) {
    const user = await this.prisma.user.findFirst({
      where: {
        name: dto.name
      }
    })
    // 校对密码
    if (!(await verify(user.password, dto.password))) {
      throw new BadRequestException('密码输入错误')
    }
    delete user.password
    return user
  }
}
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

register.dto.ts

import { IsNotEmpty } from 'class-validator'
import { isNotExistsRule } from './is-not-exists.rule'

export default class RegisterDto {
  @IsNotEmpty({ message: '用户名不能为空' })
  @isNotExistsRule('user', { message: '用户已经存在' })
  name: string
  @IsNotEmpty({ message: '密码不能为空' })
  password: string
}
1
2
3
4
5
6
7
8
9
10

is-not-exists.rule.ts

import { PrismaClient } from '@prisma/client'
import {
  registerDecorator,
  ValidationArguments,
  ValidationOptions
} from 'class-validator'

export function isNotExistsRule(table: string, validationOptions?: ValidationOptions) {
  // 这里返回的是属性装饰器
  return function (object: Record<string, any>, propertyName: string) {
    registerDecorator({
      name: 'IsNotExistsRule',
      target: object.constructor,
      propertyName,
      constraints: [table],
      options: validationOptions,
      validator: {
        async validate(value: string, args: ValidationArguments) {
          const prisma = new PrismaClient()
          const user = await prisma[table].findFirst({
            where: {
              [propertyName]: args.value
            }
          })
          return !Boolean(user)
        }
      }
    })
  }
}
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

login.dto.ts

// export default class LoginDto extends PartialType(RegisterDto) {}

import { IsNotEmpty, Validate } from 'class-validator'
import { CanFindRule } from './can-find.rule'

export default class LoginDto {
  @IsNotEmpty({ message: '用户名不能为空' })
  @Validate(CanFindRule, { message: '目标用户并不存在' })
  name: string
  @IsNotEmpty({ message: '密码不能为空' })
  password: string
}
1
2
3
4
5
6
7
8
9
10
11
12

can-find.rule.ts

import { PrismaClient } from '@prisma/client'
import {
  ValidationArguments,
  ValidatorConstraint,
  ValidatorConstraintInterface
} from 'class-validator'

@ValidatorConstraint()
export class CanFindRule implements ValidatorConstraintInterface {
  async validate(
    value: any,
    validationArguments?: ValidationArguments
  ): Promise<boolean> {
    const prisma = new PrismaClient()
    const user = await prisma.user.findFirst({
      where: {
        name: value
      }
    })
    return Boolean(user)
  }
  defaultMessage(validationArguments?: ValidationArguments): string {
    return '比对失败'
  }
}
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

7.3 配置日志

prisma.service.ts

import { Injectable } from '@nestjs/common'
import { PrismaClient } from '@prisma/client'

@Injectable()
export class PrismaService extends PrismaClient {
  constructor() {
    // 会打印查询日志
    super(process.env.NODE_ENV === 'development' ? { log: ['query'] } : {})
  }
}
1
2
3
4
5
6
7
8
9
10

7.4 使用 jwt 返回 token

app.module.ts

import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { AuthModule } from './auth/auth.module'
import { PrismaModule } from './prisma/prisma.module'

@Module({
  imports: [
    AuthModule,
    PrismaModule,
    ConfigModule.forRoot({
      isGlobal: true
    })
  ],
  controllers: [],
  providers: []
})
export class AppModule {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

app.module.ts

import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { AuthModule } from './auth/auth.module'
import { PrismaModule } from './prisma/prisma.module'

@Module({
  imports: [
    AuthModule,
    PrismaModule,
    // 设置全局配置,可以读取 .env 的内容
    ConfigModule.forRoot({
      isGlobal: true
    })
  ],
  controllers: [],
  providers: []
})
export class AppModule {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

auth.module.ts

将 jwt 封装成模块

import { Module } from '@nestjs/common'
import { AuthService } from './auth.service'
import { AuthController } from './auth.controller'
import { JwtModule } from '@nestjs/jwt'
import { ConfigModule, ConfigService } from '@nestjs/config'

@Module({
  imports: [
    // nestjs 只是将 jwt 封装成一个模块
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => {
        return {
          secret: config.get('TOKEN_SECRET'),
          signOptions: { expiresIn: '100d' }
        }
      }
    })
  ],
  providers: [AuthService],
  controllers: [AuthController]
})
export class AuthModule {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

auth.service.ts

import { Injectable } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { user } from '@prisma/client'
import { hash } from 'argon2'
import { PrismaService } from 'src/prisma/prisma.service'
import RegisterDto from './dto/register.dto'

@Injectable()
export class AuthService {
  constructor(private prisma: PrismaService, private jwt: JwtService) {}
  async register(dto: RegisterDto) {
    const user = await this.prisma.user.create({
      data: {
        name: dto.name,
        password: await hash(dto.password)
      }
    })
    return this.token(user)
  }
  async token({ name, id }: user) {
    return {
      token: await this.jwt.signAsync({
        name,
        sub: id
      })
    }
  }
}
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

7.5 使用 token 验证身份

  1. 注册 jwtStrategy 模块。auth.module.ts
import { Module } from '@nestjs/common'
import { AuthService } from './auth.service'
import { AuthController } from './auth.controller'
import { JwtModule } from '@nestjs/jwt'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { JwtStrategy } from './jwt.strategy'

@Module({
  imports: [
    // nestjs 只是将 jwt 封装成一个模块
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => {
        return {
          secret: config.get('TOKEN_SECRET'),
          signOptions: { expiresIn: '100d' }
        }
      }
    })
  ],
  providers: [AuthService, JwtStrategy],
  controllers: [AuthController]
})
export class AuthModule {}
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
  1. 鉴权模块 jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PrismaService } from 'src/prisma/prisma.service';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
  constructor(configService: ConfigService, private prisma: PrismaService) {
    super({
      // 解析用户提交的 Beater Token header 数据
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      // 加密码的 secret
      secretOrKey: configService.get('TOKEN_SECRET'),
    });
  }

  // 验证通过后结果用户资料
  async validate({ sub: id }) {
    return this.prisma.user.findUnique({
      where: { id },
    });
  }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  1. 使用 token 进行验证。auth.controller.ts
import { Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'
import { Request } from 'express'
import { AuthService } from './auth.service'
import LoginDto from './dto/login.dto'
import RegisterDto from './dto/register.dto'

@Controller('auth')
export class AuthController {
  constructor(private auth: AuthService) {}
  @Post('register')
  register(@Body() dto: RegisterDto) {
    return this.auth.register(dto)
  }

  @Post('login')
  login(@Body() dto: LoginDto) {
    return this.auth.login(dto)
  }

  @Get('all')
  @UseGuards(AuthGuard('jwt'))
  all(@Req() req: Request) {
    return req.user
  }
}
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

7.6 聚合装饰器

聚合装饰器的代码片段

import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'

export function Auth() {
  // 聚合装饰器
  return applyDecorators(
    SetMetadata('roles', roles),
    UseGuards(AuthGuard, RolesGuard),
    ApiBearerAuth(),
    ApiUnauthorizedResponse({ description: 'Unauthorized' })
  )
}
1
2
3
4
5
6
7
8
9
10
11
12

使用聚合装饰器简化 7.6 的验证身份的装饰器

auth.decorator.ts

import { applyDecorators, UseGuards } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'

export function Auth() {
  // 聚合装饰器
  return applyDecorators(UseGuards(AuthGuard('jwt')))
}
1
2
3
4
5
6
7

auth.controller.ts

import { Body, Controller, Get, Post, Req } from '@nestjs/common'
import { Request } from 'express'
import { AuthService } from './auth.service'
import { Auth } from './decorator/auth.decorator'
import LoginDto from './dto/login.dto'
import RegisterDto from './dto/register.dto'

@Controller('auth')
export class AuthController {
  constructor(private auth: AuthService) {}
  @Post('register')
  register(@Body() dto: RegisterDto) {
    return this.auth.register(dto)
  }

  @Post('login')
  login(@Body() dto: LoginDto) {
    return this.auth.login(dto)
  }

  @Get('all')
  @Auth()
  all(@Req() req: Request) {
    return req.user
  }
}
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

使用属性装饰器简化代码

原先的 auth.controller.ts

@Get('all')
@Auth()
all(@Req() req: Request) {
  return req.user;
}
1
2
3
4
5

使用属性装饰器简化代码 - user.decorator.ts

import { createParamDecorator, ExecutionContext } from '@nestjs/common'

export const User = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
  const request = ctx.switchToHttp().getRequest()
  return request.user
})
1
2
3
4
5
6

改造后的 auth.controller.ts

@Get('all')
@Auth()
all(@User() user: UserType) {
  return user;
}
1
2
3
4
5

8.文件上传

8.1 完成文件上传

TransformInterceptor.ts

import {
  CallHandler,
  ExecutionContext,
  Injectable,
  Logger,
  NestInterceptor
} from '@nestjs/common'
import { Request } from 'express'
import { map } from 'rxjs/operators'

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    console.log('拦截器前')
    const request = context.switchToHttp().getRequest() as Request
    const startTime = Date.now()
    return next.handle().pipe(
      map(data => {
        const endTime = Date.now()
        new Logger().log(
          `Time:${endTime - startTime}\tURL:${request.path}\tMETHOD:${request.method}`
        )
        return { data }
      })
    )
  }
}
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

upload.controller.ts

import { Controller, Get, Post, UseInterceptors } from '@nestjs/common'
import { TransformInterceptor } from 'src/TransformInterceptor'

@Controller('upload')
@UseInterceptors(new TransformInterceptor())
export class UploadController {
  @Get('image')
  image() {}
}
1
2
3
4
5
6
7
8
9

接下来请求 upload/image,将会打印:Time:3 URL:/upload/image METHOD:GET,这个拦截器是在 controller 处理之前进行拦截的。


利用拦截器对返回数据进行包裹

请求 upload/image 将会返回 { data: 'abc' },返回结果用 data 进行了包裹

TransformInterceptor.ts

import {
  CallHandler,
  ExecutionContext,
  Injectable,
  Logger,
  NestInterceptor
} from '@nestjs/common'
import { Request } from 'express'
import { map } from 'rxjs/operators'

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    console.log('拦截器前')
    // const request = context.switchToHttp().getRequest() as Request;
    // const startTime = Date.now();
    return next.handle().pipe(
      map(data => {
        // const endTime = Date.now();
        // new Logger().log(
        //   `Time:${endTime - startTime}\tURL:${request.path}\tMETHOD:${
        //     request.method
        //   }`,
        // );
        return { data }
      })
    )
  }
}
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

upload.controller.ts

import { Controller, Get, Post, UseInterceptors } from '@nestjs/common'
import { TransformInterceptor } from 'src/TransformInterceptor'

@Controller('upload')
@UseInterceptors(new TransformInterceptor())
export class UploadController {
  @Get('image')
  image() {
    return 'abc'
  }
}
1
2
3
4
5
6
7
8
9
10
11

完成文件上传

upload.controller.ts

import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common'
import { FileInterceptor } from '@nestjs/platform-express'
import { TransformInterceptor } from 'src/TransformInterceptor'

@Controller('upload')
// 将返回值使用 data 进行包裹
@UseInterceptors(new TransformInterceptor())
export class UploadController {
  @Post('image')
  // file 为前端传来的字段名,这里的拦截器是获取上传的图片
  @UseInterceptors(
    FileInterceptor('file', {
      // 限定文件大小
      limits: { fileSize: Math.pow(1024, 2) * 2 }
    })
  )
  // 通过参数装饰器获取得到的文件
  image(@UploadedFile() file: Express.Multer.File) {
    return file.originalname
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

8.2 上传类型验证

下面代码就能实现图片上传类型验证,但是略显麻烦,8.3 将简化操作

upload.controller.ts

import {
  Controller,
  MethodNotAllowedException,
  Post,
  UploadedFile,
  UseInterceptors
} from '@nestjs/common'
import { FileInterceptor } from '@nestjs/platform-express'
import { TransformInterceptor } from 'src/TransformInterceptor'

@Controller('upload')
// 将返回值使用 data 进行包裹
@UseInterceptors(new TransformInterceptor())
export class UploadController {
  @Post('image')
  // file 为前端传来的字段名,这里的拦截器是获取上传的图片
  @UseInterceptors(
    FileInterceptor('file', {
      // 限定文件大小
      limits: { fileSize: Math.pow(1024, 2) * 2 },
      fileFilter(req, file, callback) {
        // file.mimetype 一般是 image/png、image\jpeg 等
        if (!file.mimetype.includes('image')) {
          callback(new MethodNotAllowedException('文件类型错误'), false)
        }
        callback(null, true)
      }
    })
  )
  // 通过参数装饰器获取得到的文件
  image(@UploadedFile() file: Express.Multer.File) {
    return file
  }
}
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

8.3 使用装饰器优化代码

upload.controller.ts

import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common'
import { TransformInterceptor } from 'src/TransformInterceptor'
import { ImageUpload } from './decorator/upload.decorator'

@Controller('upload')
// 将返回值使用 data 进行包裹
@UseInterceptors(new TransformInterceptor())
export class UploadController {
  @Post('image')
  @ImageUpload()
  image(@UploadedFile() file: Express.Multer.File) {
    return file
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

upload.decorator.ts

import {
  applyDecorators,
  MethodNotAllowedException,
  UseInterceptors
} from '@nestjs/common'
import { FileInterceptor } from '@nestjs/platform-express'
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface'

// 文件过滤
export function fileFilter(type: string) {
  return (
    req: any,
    file: Express.Multer.File,
    callback: (error: Error | null, acceptable: boolean) => void
  ) => {
    if (!file.mimetype.includes(type)) {
      callback(new MethodNotAllowedException('文件类型错误'), false)
    }
    callback(null, true)
  }
}

// 上传
export function Upload(field = 'file', option?: MulterOptions) {
  return applyDecorators(UseInterceptors(FileInterceptor(field, option)))
}

// 上传图片
export function ImageUpload(field = 'file') {
  return Upload(field, {
    limits: { fileSize: Math.pow(1024, 2) * 3 },
    fileFilter: fileFilter('image')
  })
}
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

8.4 根据不同类型单独控制上传

upload.controller.ts

import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common'
import { TransformInterceptor } from 'src/TransformInterceptor'
import { DocumentUpload, UploadFile } from './decorator/upload.decorator'

@Controller('upload')
// 将返回值使用 data 进行包裹
@UseInterceptors(new TransformInterceptor())
export class UploadController {
  @Post('image')
  @UploadFile('file', ['image'])
  image(@UploadedFile() file: Express.Multer.File) {
    return file
  }

  @Post('document')
  @DocumentUpload()
  document(@UploadedFile() file: Express.Multer.File) {
    return file
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

upload.decorator.ts

import {
  applyDecorators,
  MethodNotAllowedException,
  UseInterceptors
} from '@nestjs/common'
import { FileInterceptor } from '@nestjs/platform-express'
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface'

// 文件过滤
export function fileFilter(type: string[]) {
  return (
    req: any,
    file: Express.Multer.File,
    callback: (error: Error | null, acceptable: boolean) => void
  ) => {
    const check = type.some(t => file.mimetype.includes(t))
    if (!check) {
      callback(new MethodNotAllowedException('文件类型错误'), false)
    }
    callback(null, true)
  }
}

// 上传
export function Upload(field = 'file', option?: MulterOptions) {
  return applyDecorators(UseInterceptors(FileInterceptor(field, option)))
}

// 上传图片
export function ImageUpload(field = 'file') {
  return Upload(field, {
    limits: { fileSize: Math.pow(1024, 2) * 3 },
    fileFilter: fileFilter(['image'])
  })
}

// 上传文档
export function DocumentUpload(field = 'file') {
  return Upload(field, {
    limits: { fileSize: Math.pow(1024, 2) * 3 },
    fileFilter: fileFilter(['document'])
  })
}

// 上传文件
export function UploadFile(field = 'file', type: string[]) {
  return Upload(field, {
    limits: { fileSize: Math.pow(1024, 2) * 3 },
    fileFilter: fileFilter(type)
  })
}
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

8.5 通过 URL 访问文件

main.ts

import { NestFactory } from '@nestjs/core'
import { NestExpressApplication } from '@nestjs/platform-express'
import { AppModule } from './app.module'

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule)
  app.useStaticAssets('uploads', { prefix: '/uploads' })
  await app.listen(3000)
}
bootstrap()
1
2
3
4
5
6
7
8
9
10

9.博客项目

9.1 初始化设置

  1. 安装 5.1 节的包
  2. 使用 npx prisma init 进行初始化,在 prisma 文件夹新建 seed.ts,并且在 package.json 中写入:
"prisma": {
  "seed": "ts-node prisma/seed.ts"
},
1
2
3
  1. 配置 .envprisma/schema.prisma,其中 schema.prisma 内容:
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model user {
  id       Int    @id @default(autoincrement()) @db.UnsignedInt
  name     String @unique
  password String
}

model article {
  id      Int    @id @default(autoincrement()) @db.UnsignedInt
  title   String
  content String @db.Text
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  1. npx prisma migrate dev
  2. seed.ts 中写入 mock 数据:
import { PrismaClient } from '@prisma/client'
import { hash } from 'argon2'
import { Random } from 'mockjs'

const prisma = new PrismaClient()

async function run() {
  await prisma.user.create({
    data: {
      name: 'admin',
      password: await hash('admin888')
    }
  })

  for (let i = 0; i < 50; i += 1) {
    await prisma.article.create({
      data: {
        title: Random.ctitle(10, 30),
        content: Random.cparagraph(30, 50)
      }
    })
  }
}

run()
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
  1. npx prisma migrate reset
  2. nest g mo authnest g s auth --no-specnest g co auth --no-spec
  3. auth 文件夹中新建 dto/register.dto.ts
import { IsNotEmpty } from 'class-validator'

export default class RegisterDto {
  @IsNotEmpty({ message: '用户名不能为空' })
  name: string
  @IsNotEmpty({ message: '密码不能为空' })
  password: string
}
1
2
3
4
5
6
7
8

9.2 验证用户是否存在 & 规范返回验证失败信息

main.ts

import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import Validate from './common/validate'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  app.useGlobalPipes(new Validate())
  await app.listen(3000)
}
bootstrap()
1
2
3
4
5
6
7
8
9
10

auth/dto/register.dto.ts

import { IsNotEmpty } from 'class-validator'
import { IsNotExistsRule } from 'src/common/rules/is-not-exists.rule'

export default class RegisterDto {
  @IsNotEmpty({ message: '用户名不能为空' })
  @IsNotExistsRule('user', { message: '用户已经注册' })
  name: string
  @IsNotEmpty({ message: '密码不能为空' })
  password: string
}
1
2
3
4
5
6
7
8
9
10

common/rules/is-not-exists.rule.ts

import { PrismaClient } from '@prisma/client'
import { registerDecorator, ValidationOptions } from 'class-validator'
export function IsNotExistsRule(table: string, validationOptions?: ValidationOptions) {
  return function (object: Record<string, any>, propertyName: string) {
    registerDecorator({
      name: 'IsNotExistsRule',
      target: object.constructor,
      propertyName,
      constraints: [table],
      options: validationOptions,
      validator: {
        async validate(value, args) {
          const prisma = new PrismaClient()
          const res = await prisma[table].findFirst({
            where: {
              [args.property]: value
            }
          })
          return !Boolean(res)
        }
      }
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

common/validate.ts

import {
  HttpException,
  HttpStatus,
  ValidationError,
  ValidationPipe
} from '@nestjs/common'

// 将错误信息更加规范地输出出去
export default class Validate extends ValidationPipe {
  protected flattenValidationErrors(validationErrors: ValidationError[]): string[] {
    const messages = {}
    validationErrors.forEach(error => {
      messages[error.property] = Object.values(error.constraints)[0]
    })
    throw new HttpException(
      {
        code: 422,
        messages
      },
      HttpStatus.UNPROCESSABLE_ENTITY
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

9.3 完成用户注册