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
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
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
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'
}
}
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 // 可配置
// }
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) {}
}
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
:返回值类型
- 安装:
yarn add reflect-metadata
- 设置 tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
2
3
4
5
6
- 引入:
import 'reflect-metadata'
- 实例:
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] ]
*/
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:type
、design:paramtypes
、design: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
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
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 }
}
}
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
2
3
4
5
6
7
8
9
2.提供者 provider
2.1 开始
- 安装:
pnpm add -g @nestjs/cli nodemon ts-node
- 初始化:
nest
命令可以查看快捷键 - 创建新项目:
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 {}
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()
}
}
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!'
}
}
2
3
4
5
6
7
8
2.3 服务提供者注册
- 使用命令:
nest g s hd --no-spec -d
:-d
是预览,加上--flat
就不会有子目录 - 输入命令后,app.module.ts 会自动在
providers
里面添加HdService
- 在命令新建的 hd.servece.ts 中写入逻辑:
import { Injectable } from '@nestjs/common'
@Injectable()
export class HdService {
hd() {
return 'hd service hd method'
}
}
2
3
4
5
6
7
8
- 在 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()
}
}
2
3
4
5
6
7
8
9
10
11
2.4 提供者类的注册方式
2.3 中,app.module.ts 的
providers
(容器、提供者) 是这样写的:providers: [AppService]
,这其实是简化版
非简化版:
providers: [
{
provide: AppService,
useClass: AppService,
},
],
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()
}
}
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,
) {}
2
3
4
5
6
2.5 基本类型的提供者注册
app.module.ts:
providers: [
AppService,
{
provide: 'appName',
useValue: '后盾人'
}
]
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 // 输出后盾人
}
}
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 {}
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
}
}
2
3
4
5
6
7
8
9
2.6 动态注册服务提供者
- 安装:
pnpm add dotenv
- 跟目录新建 .env,写入:
NODE_ENV=development
- 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 {}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 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()
}
}
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 {}
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>`
}
}
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
}
2
3
4
5
6
7
8
9
10
11
12
2.8 模块共享服务
imports
和exports
都要分别进行设置
- 创建模块:
nest g mo hd -d
- 创建服务:
nest g s hd --no-spec -d
- 创建控制器:
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 {}
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 {}
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
}
}
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 {}
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
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
3.配置项模块
3.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
- 安装依赖包:
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'
}
}
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)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
3.2 模块间的调用
- 局部组件:
- 被调用模块需要
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')
}
}
2
3
4
5
6
7
8
9
10
11
12
- 全局组件:
- 方法一:被调用组件的 xx.module.ts 的
class
上面添加@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
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
3.2 动态模块注册语法
- 将被调用组件的模块设置为静态模块,以便调用组件的模块能传入参数(在这里是传递要读取文件的目录)
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
}
]
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- 调用组件的模块:
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 {}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 被调用模块在 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)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 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') // 后盾人
}
}
2
3
4
5
6
7
8
9
10
11
12
4.mysql 数据库
4.1 mysql
- 使用
npx prisma init
生成初始化目录 .env
配置:
DATABASE_URL="mysql://root:123456@localhost:3306/nest-blog"
- 写入表:
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
}
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
- 输入:
npx prisma migrate dev
- 执行后,自动生成
nest-blog
数据库。(这里有个小坑,在数据库管理软件中使用use 反引号nest-blog反引号
才能选中数据库)
4.2 数据填充的环境配置
- 配置 package.json:
"prisma": {
"seed": "ts-node prisma/seed.ts"
}
2
3
- 输入:
npx prisma db seed
即可执行上述命令 - 在 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()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 输入:
npx prisma migrate reset
即可修改数据库
4.3 mock 实现数据填充
- 新增帮助函数:
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)
}
}
2
3
4
5
6
7
8
9
10
11
- 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()
2
3
4
5
6
7
8
9
10
11
12
- 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()
}
})
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 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()
}
})
})
}
2
3
4
5
6
7
8
9
10
11
12
13
- 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)
}
})
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
5.配置管理
5.1 ConfigModule
- 安装:
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
- 安装:
pnpm add -D prisma typescript
- 安装:
pnpm add -D prisma typescript @types/node @types/mockjs @nestjs/mapped-types @types/passport-local @types/passport-jwt @types/express @types/lodash @types/multer
基本使用:
- 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 {}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 在 .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')
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
读取文件配置:
app.config.ts:
export default () => ({
app: {
name: '后盾人'
}
})
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 {}
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')
}
}
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
}))
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]
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 {}
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
}
}
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
}
}
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 }
})
}
}
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
}
}
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 },
});
}
2
3
4
5
6
7
在方法和类上也能使用管道:
@Get(':id')
@UsePipes(HdPipe)
getHello(@Param('id') id: number) {
return this.prisma.article.findUnique({
// 如果没有管道会报错,因为 id 默认是 string 类型数据
where: { id },
});
}
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 }
})
}
}
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 {}
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()
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 },
});
}
2
3
4
5
6
7
8
6.2 使用管道实现验证
最简单的管道验证:
app.controller.ts:
@Post('store')
add(@Body(HdPipe) dto: Record<string, any>) {
return dto;
}
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
}
}
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;
}
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
}
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
}
}
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
}
}
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())
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
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
6.2 使用过滤器处理验证异常
- 使用
nest g f validate-exception --no-spec
生成 validate-exception.filter.ts 文件 - 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
}
}
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
}
}
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 '比对失败'
}
}
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
}
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)
}
}
})
}
}
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;
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
}
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)
}
}
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
}
}
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)
}
}
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
}
}
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
}
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)
}
}
})
}
}
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
}
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 '比对失败'
}
}
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'] } : {})
}
}
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 {}
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 {}
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 {}
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
})
}
}
}
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 验证身份
- 注册
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 {}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- 鉴权模块 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 },
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- 使用 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
}
}
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' })
)
}
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')))
}
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
}
}
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;
}
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
})
2
3
4
5
6
改造后的 auth.controller.ts:
@Get('all')
@Auth()
all(@User() user: UserType) {
return user;
}
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 }
})
)
}
}
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() {}
}
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 }
})
)
}
}
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'
}
}
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
}
}
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
}
}
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
}
}
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')
})
}
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
}
}
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)
})
}
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()
2
3
4
5
6
7
8
9
10
9.博客项目
9.1 初始化设置
- 安装 5.1 节的包
- 使用
npx prisma init
进行初始化,在 prisma 文件夹新建 seed.ts,并且在 package.json 中写入:
"prisma": {
"seed": "ts-node prisma/seed.ts"
},
2
3
- 配置 .env 和 prisma/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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 跑
npx prisma migrate dev
- 在 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()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- 跑
npx prisma migrate reset
- 跑
nest g mo auth
、nest g s auth --no-spec
、nest g co auth --no-spec
- 在 auth 文件夹中新建 dto/register.dto.ts:
import { IsNotEmpty } from 'class-validator'
export default class RegisterDto {
@IsNotEmpty({ message: '用户名不能为空' })
name: string
@IsNotEmpty({ message: '密码不能为空' })
password: string
}
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()
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
}
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)
}
}
})
}
}
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
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23