NestJS 中文网:nest.nodejs.cn
RxJS 中文文档:cn.rx.js.org/manual/over...
1、NestJS介绍
1-1 简介
NestJS 是一个用于构建高效可扩展的一个基于 Node js 服务端的应用程序开发框架,并且完全支持 TS,结合了 AOP 面向切面的编程方式。
- 面向过程编程OPP:Procedure Oriented Programming,是一种以事物为中心的编程思想。主要关注"怎么做",即完成任务的具体细节。
- 面向对象编程OOP:Object Oriented Programming,是一种以对象为基础的编程思想。主要关注"谁来做",即完成任务的对象。
- 面向切面编程AOP:Aspect Oriented Programming,基于OOP延伸出来的编程思想。主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。
NestJS 还是一个 Spring MVC 的风格的框架(甚至可以看作 Spring Boot 的一个 JS 版本),其中的控制反转(IOC)、依赖注入(DI)等思想都是借鉴了Angualr的设计。
NestJS 有一个自己的底层 HTTP 平台抽象层,称为 @nestjs/common 中的 NestFactory,它允许开发者选择不同的平台(例如 Express 或 Fastify)来启动 NestJS 应用程序。在他们的基础上提供了一定程度的抽象,同时也将它们的 API 直接暴露给开发人员。这样可以轻松使用每个平台的无数第三方模块。
1-2 采用的设计模式
- IOC:控制反转(Inversion of Control),具体定义是高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。
- DI:依赖注入(Dependency Injection)其实和IOC是同根生,这两个原本就是一个东西,只不过由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物Martin Fowler又给出了一个新的名字:"依赖注入"。 类A依赖类B的常规表现是在A中使用B的instance。
- 案例讲解如下:
为了便于我们的学习,可以先初始化全局的TS环境。
- 默认携带tsc编译器,能将ts文件编译成js文件:npm install typescript -g,使用:tsc 文件路径
- tsc --init的作用是用来初始化一个TypeScript项目,并自动生成一个tsconfig.json配置文件。
- tsc -w的作用是在TypeScript项目中开启监视模式。
- ts-node可以直接编译并在node运行js文件:npm install -g ts-node,使用:ts-node 文件路径
ts
// 未使用IOC和DI之前的代码
class A {
name;
constructor(name) {
this.name = name;
}
}
class B {
age;
entity;
constructor(age) {
this.age = age;
this.entity = new A('FE');
}
}
const b = new B(18);
console.log(b.entity.name);
我们可以看到,B 中代码的实现是需要依赖 A 的,两者的代码耦合度非常高。当两者之间的业务逻辑复杂程度增加的情况下,维护成本与代码可读性都会随着增加,并且很难再多引入额外的模块进行功能拓展。
为了解决这个问题可以使用IOC容器:
ts
// 使用IOC和DI之后的代码
class A1 {
name;
constructor(name) {
this.name = name;
}
}
class A2 {
name;
constructor(name) {
this.name = name;
}
}
// 中间件用于解耦
class MiddleWare {
modules;
constructor() {
this.modules = {};
}
provide(key, module) {
this.modules[key] = module;
}
get(key) {
return this.modules[key];
}
}
const mw = new MiddleWare();
mw.provide('a1', new A1('FE1'));
mw.provide('a2', new A2('FE2'));
class B {
a1;
a2;
constructor(middleware) {
this.a1 = middleware.get('a1');
this.a2 = middleware.get('a2');
}
}
const b = new B(mw);
console.log(b.a1.name);
console.log(b.a2.name);
1-3 装饰器
装饰器是一种特殊的类型声明,他可以附加在类,方法,属性,参数上面,开启该写法需要配置 tsconfig.js 文件中的:"experimentalDecorators": true。
可以使得装饰器持有装饰对象、增强装饰对象。
1-3-1 装饰器类型
1、类装饰器
主要是通过@符号添加装饰器,会自动把class的构造函数传入到装饰器的第一个参数 target ,然后通过prototype可以自定义添加或修改属性和方法:
ts
// 类装饰器
const cd: ClassDecorator = target => {
console.log(target); // 输出: [class FE],因为此时target指向类(Class)
target.prototype.name = '明远湖之鱼';
};
@cd
// 相当于:
// cd(FE);
class FE {
constructor() {}
}
// 可以不破坏原有的类,添加一些方法或属性
const fe: any = new FE(); // 实例要为any类型,否则会提醒实例的属性未定义
console.log(fe.name); // 输出:明远湖之鱼
2、属性装饰器
同样使用@符号给属性添加装饰器,会返回两个参数:1. 类的原形对象 2. 属性的名称。
ts
// 属性装饰器
const pd: PropertyDecorator = (target, propertyKey) => {
console.log(target, propertyKey); // 输出: {} name,因为此时target指向类的原型对象(Prototype)
target[propertyKey] = '明远湖之鱼';
};
class FE {
@pd
public name: string;
constructor() {}
}
const fe: any = new FE();
console.log(fe.name); // 输出:明远湖之鱼
3、参数装饰器
同样使用@符号给方法的参数添加装饰器,会返回三个参数:1. 原形对象 2. 方法的名称 3. 参数的位置(从0开始)。
ts
// 参数装饰器
const pd: ParameterDecorator = (target, propertyKey, parameterIndex) => {
console.log(target, propertyKey, parameterIndex); // 输出: {} sayHello 1
};
class FE {
public name: string;
constructor() {}
sayHello(name: string, @pd age: number) {
console.log(name, age);
}
}
const fe: any = new FE();
fe.sayHello('明远湖之鱼', 18);
4、方法装饰器
同样使用@符号给方法添加装饰器,会返回三个参数:1. 原形对象 2. 方法的名称 3. 属性描述符(可写对应writable,可枚举对应enumerable,可配置对应configurable)。
ts
// 方法装饰器
const md: MethodDecorator = (target, propertyKey, descriptor) => {
console.log(target, propertyKey, descriptor); // 输出: {} sayHello { value: [Function], writable: true, enumerable: false, configurable: true }
};
class FE {
public name: string;
constructor() {}
@md
sayHello(name: string, age: number) {
console.log(name, age);
}
}
const fe: any = new FE();
fe.sayHello('明远湖之鱼', 18);
1-3-2 用装饰器实现一个GET请求
ts
// 用装饰器封装一个GET请求方法
import axios from 'axios';
// 为了使得传入该装饰器的参数直接是URL,而不是默认的target、key、descriptor,所以需要使用装饰器工厂(高阶函数封装)
const Get = (url: string): MethodDecorator => {
return (target, key, descriptor: PropertyDescriptor) => {
const fn = descriptor.value; // fnc指向需要装饰的方法,此处为getList方法
axios
.get(url)
.then(res => {
fn(res, {
status: 200
});
})
.catch(e => {
fn(e, {
status: 500
});
});
};
};
// 定义控制器
class Controller {
constructor() {}
@Get('https://api.apiopen.top/api/getHaoKanVideo?page=0&size=10')
getList(res: any, status: any) {
console.log(res.data.result.list, status);
}
}
2、NestJS工程
2-1 项目初始化
先全局安装脚手架:
bash
npm i -g @nestjs/cli
初始化项目:
bash
nest new nestjs-demo
Vscode爆红,Delete ␍
eslint(prettier/prettier)错误的解决方法
2-2 项目目录介绍
- main.ts:入口文件主文件,类似于 vue 的 main.ts,通过 NestFactory.create(AppModule) 创建一个app,类似于绑定一个根组件。app.listen(3000); 表示监听一个端口。
ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
其中AppModule的实现如下:
ts
// src\app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
利用装饰器实现AppModule类的封装,其中:
- AppController表示应用程序的控制器,负责处理客户端请求;
- AppService表示应用程序的服务,通常包含业务逻辑。
- Controller.ts 控制器,负责构建好接口路由。
ts
// src\app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
// 修改前代码,此时该接口路由为/
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
// 修改后代码,此时该接口路由为/api/get
@Controller('/api')
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('/get')
getHello(): string {
return this.appService.getHello();
}
}
其中,private readonly appService: AppService 这一行代码就是依赖注入,不需要通过new实例化即可通过this.appService.getHello()调用相关方法。
运行后可以看到该接口路由生效:
- app.service.ts:这个文件主要实现业务逻辑的,同时可以利于逻辑复用。
ts
// src\app.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
2-3 nestjs cli 常用命令
nest --help
可以查看nestjs所有的命令(命令风格和angular很像):
生成一个用户模块:
ts
// 1、生成controller.ts
nest g co user
// 2、生成module.ts
nest g mo user
// 3、生成service.ts
nest g s user
可以看到生成成功:
以上步骤一个一个生成的太慢了,我们可以直接使用一个命令生成一套标准的CURD 模板:
bash
nest g resource friend
第一次使用这个命令的时候,除了生成文件之外还会自动使用 npm 帮我们更新资源,安装一些额外的插件,后续再次使用就不会更新了。
2-4 RESTful API
RESTful API是一种基于HTTP协议和资源的接口设计风格,具有高效、易用、扩展性等优点。
2-4-1 接口url
举例如下:
- 传统接口:
- RESTful 接口:
- http://localhost:8080/api/get_list/1
- RESTful 风格一个接口就会完成 增删改差 的工作,通过不同的请求方式(类型/传参)来区分的:
其中常用的请求类型有:
- 查询GET
- 提交POST
- 更新 PUT PATCH
- 删除 DELETE
2-4-2 RESTful 版本控制
一共有三种,我们一般用第一种,更加语义化:
URI Versioning | 版本将在请求的 URI 中传递(默认) |
---|---|
Header Versioning | 自定义请求标头将指定版本 |
Media Type Versioning | 请求的Accept标头将指定版本 |
首先要到src\main.ts中开启版本控制:
ts
// src\main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { VersioningType } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableVersioning({
type: VersioningType.URI,
});
await app.listen(3000);
}
bootstrap();
然后在 controller 配置版本:
ts
import { Controller, Get, Version } from '@nestjs/common';
import { AppService } from './app.service';
// 对整个控制器进行版本控制
// @Controller({
// path: 'api',
// version: '1', // 此时接口变为 /v1/api/xxx
// // ... other options
// })
@Controller('/api')
export class AppController {
constructor(private readonly appService: AppService) {}
// 也可以对某个接口进行版本控制
@Get('/get')
@Version('1') // 此时接口变为 /v1/api/get
getHello(): string {
return this.appService.getHello();
}
}
2-4-3 Code码规范
- 200 OK
- 304 Not Modified 协商缓存了
- 400 Bad Request 参数错误
- 401 Unauthorized token错误
- 403 Forbidden referer origin 验证失败
- 404 Not Found 接口不存在
- 500 Internal Server Error 服务端错误
- 502 Bad Gateway 上游接口有问题或者服务器问题
3、NestJS功能点介绍
3-1 控制器的常用装饰器
NestJS 提供了参数装饰器,可以用来帮助我们快速获取参数(在controller中定义的请求处理方法中使用):
装饰器名称 | 装饰的内容 | 代表含义/作用 |
---|---|---|
@Request() | req | 请求消息,可用于获取传参(query或body或param) |
@Response() | res | 响应消息 |
@Next() | next | |
@Session() | req.session | 获取session |
@Param(key?: string) | req.params/req.params[key] | 可用于动态路由param传参 |
@Query(key?: string) | req.query/req.query[key] | 可用于query传参 |
@Headers(name?: string) | req.headers/req.headers[name] | 读取header信息 |
@Body(key?: string) | req.body/req.body[key] | 读取body信息(比如post请求传递的参数) |
@HttpCode | 控制接口返回的状态码 |
3-2 Session
前面提到,NestJS默认支持 express 的插件,所以可以直接安装express的session:
bash
npm i express-session --save
npm i @types/express-session -D
在代码中使用:
ts
import { NestFactory } from '@nestjs/core';
import { VersioningType } from '@nestjs/common';
import { AppModule } from './app.module';
import * as session from 'express-session';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableVersioning({
type: VersioningType.URI,
});
app.use(
session({
secret: 'xxxx',
name: 'xc.session',
rolling: true,
cookie: { maxAge: null },
}),
);
await app.listen(3000);
}
bootstrap();
其中里面的参数配置详解如下:
secret | 生成服务端session 签名 可以理解为加盐 |
---|---|
name | 生成客户端cookie 的名字 默认 connect.sid |
cookie | 设置返回到前端 key 的属性,默认值为{ path: '/', httpOnly: true, secure: false, maxAge: null }。 |
rolling | 在每次请求时强行设置 cookie,这将重置 cookie 过期时间(默认:false) |
3-3 NestJS提供者Providers
Providers 是 NestJS 的一个基本概念。许多基本的 Nest 类可能被视为 provider - service, repository, factory, helper 等等。 他们都可以通过 constructor 注入依赖关系。 这意味着对象可以彼此创建各种关系,并且"连接"对象实例的功能在很大程度上可以委托给 NestJS 运行时系统。 Provider 只是一个用 @Injectable() 装饰器注释的类。
3-3-1 基本用法
首先用 @Injectable() 装饰需要注入的类:
然后在 module 引入 service,在 providers 注入:
最后在 Controller 就可以直接使用注入好的 service 了(不需要实例化):
3-3-2 自定义名称
在上面的基本用法中,其实是一个语法糖简写,在 module 引入 service,在 providers 注入时可使用全称:
此时在 Controller 需要通过 @Inject 获取注入好的 service 并使用:
3-3-3 自定义注入值
在 module 中可以使用 useValue 注入自定义值:
然后在 Controller 中同样需要通过 @Inject 获取注入好的值并使用:
3-3-4 工厂模式
如果服务之间有相同的依赖或者逻辑处理,可以使用 useFactory 进行解耦。
使用 async useFactory()
可以变成异步工厂。
3-4 模块
3-4-1 基本用法
当使用 nest g res xxx
新增一个模块时,NestJS会自动帮我们引入模块:
3-4-2 共享模块
如果 user 的 Service 想暴露给其他模块使用,就需要在 user 模块中使用 exports 导出该服务,变成共享模块:
然后由于 app.modules 已经引入过该模块,可以直接使用 user 模块的 Service:
3-4-3 全局模块
可以使用 @Global() 装饰某个注册模块使其成为全局模块:
在根模块 app.module 中注册后:
此时无论是 user 模块,还是friend 模块,都无须在module import 导入,即可在 controller 中直接使用该全局模块,用法类似下面这样:
3-4-4 动态模块
动态模块主要就是为了给模块传递参数,可以给该模块添加一个静态方法 forRoot,用来接受参数:
在根模块 app.module 中注册并传入参数:
3-5 中间件
中间件是在路由处理程序之前调用的函数。中间件函数可以访问请求和响应对象,执行以下任务:
- 执行任何代码。
- 对请求和响应对象进行更改。
- 结束请求-响应周期。
- 调用堆栈中的下一个中间件函数。
- 如果当前的中间件函数没有结束请求-响应周期, 它必须调用 next() 将控制传递给下一个中间件函数。否则, 请求将被挂起
可以使用 nest g mi xxx
命令创建一个中间件模板。
3-5-1 创建一个依赖注入中间件
要求我们实现 use 函数,返回 req res next 参数,如果不调用 next,则程序将被挂起:
ts
// src\middleware\index.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class Logger implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('这个是中间件函数的处理逻辑');
// res.send('Hello World!我被拦截了!);
next();
}
}
使用方法:在模块里面实现 configure 方法,并返回一个消费者 consumer,其中通过 apply 注册中间件,通过forRoutes 指定 Controller 路由:
此时调用该接口,即可看到该中间件处理函数发挥了作用:
也可以指定 拦截的方法,比如拦截GET、POST 等 forRoutes 使用对象配置:
ts
// src\app.module.ts
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
// consumer.apply(Logger).forRoutes('/api/get');
consumer
.apply(Logger)
.forRoutes({ path: '/api/get', method: RequestMethod.GET });
}
}
也可以直接把整个 Controller 塞进去进行中间件处理:
ts
// src\app.module.ts
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
// consumer.apply(Logger).forRoutes('/api/get');
// consumer
// .apply(Logger)
// .forRoutes({ path: '/api/get', method: RequestMethod.GET });
consumer.apply(Logger).forRoutes(AppController);
}
}
3-5-2 全局中间件
全局中间件只能使用函数模式,可以用于白名单拦截、token校验之类的场景。
同时全局中间件也可以是第三方的,如cors,也是像上面一样通过app.use()使用。
bash
npm install cors
npm install @types/cors -D
3-6 静态资源托管
首先需要安装两个依赖:
bash
npm install multer
npm install @types/multer -D
然后可以生成一个用于图片上传的模块:
bash
nest g res upload
首先在 upload.module.ts 定义相应的模块:
ts
// src\upload\upload.module.ts
import { Module } from '@nestjs/common';
import { UploadService } from './upload.service';
import { UploadController } from './upload.controller';
import { MulterModule } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { extname, join } from 'path';
@Module({
imports: [
// MulterModule 是一个用于处理文件上传的 NestJS 模块。它提供了一些方便的方法,可以轻松地将文件上传功能集成到 NestJS 应用程序中。
// 里面包含register方法或registerAsync方法,该方法接收一个对象作为参数,该对象包含了一些配置信息,例如文件上传的存储位置、文件名生成规则等。
MulterModule.register({
storage: diskStorage({
destination: join(__dirname, '../images'),
filename: (_, file, callback) => {
const fileName = `${new Date().getTime() + extname(file.originalname)}`;
return callback(null, fileName);
},
}),
}),
],
controllers: [UploadController],
providers: [UploadService],
})
export class UploadModule {}
此时打包后可以看到dist目录下生成了一个images目录:
在 controller 中使用:
ts
// src\upload\upload.controller.ts
import {
Controller,
Post,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { UploadService } from './upload.service';
import { FileInterceptor } from '@nestjs/platform-express';
@Controller('upload')
export class UploadController {
// 使用 UseInterceptors 装饰器进行文件处理,其中 FileInterceptor 表示上传单个文件,FilesInterceptor 是多个
// 使用 UploadedFile 装饰器接受 file 文件
constructor(private readonly uploadService: UploadService) {}
@Post('album')
@UseInterceptors(FileInterceptor('file')) // 这里 file 对应前端传递的参数名
upload(@UploadedFile() file) {
console.log(file);
return true;
}
}
此时去 Apifox 发起请求可以看到请求成功,图片也已存入服务器:
此时上传后的图片是不能直接通过URL访问的,还需要NestJS帮我们实现相应托管:
ts
// src\main.ts
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.enableVersioning({
type: VersioningType.URI,
});
app.useStaticAssets(join(__dirname, 'images'), {
prefix: '/images',
});
await app.listen(3000);
}
bootstrap();
此时便可访问了:
3-7 拦截器
拦截器具有一系列有用的功能,这些功能受面向切面编程(AOP)技术的启发。它们可以:
- 在函数执行之前/之后绑定额外的逻辑
- 转换从函数返回的结果
- 转换从函数抛出的异常
- 扩展基本函数行为
- 根据所选条件完全重写函数 (例如, 缓存目的)
3-7-1 响应拦截器
下面以实现接口响应格式统一这个功能为例介绍拦截器的应用。
我们需要给接口返回一个标准的 json 格式,就要给我们的数据做一个全局format:
ts
{
data,
status:0,
message:"成功",
success:true
}
新建 src\common\response.ts 文件,Nest Js 配合 Rxjs 格式化数据:
ts
// src\common\response.ts
import { Injectable, NestInterceptor, CallHandler } from '@nestjs/common';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
interface data<T> {
data: T;
}
// IOC,它可以在 NestJS 应用中被注入到其他服务、控制器或模块中。
@Injectable()
// NestInterceptor 接口定义了拦截器的基本结构。
export class Response<T = any> implements NestInterceptor {
// intercept方法是拦截器的核心方法,它接收一个上下文对象和一个next函数作为参数。
// 返回的Observable对象将作为拦截器的响应。Observable 源自于 RxJS,用于创建和操作数据流,它允许你订阅数据源,并且可以对数据流进行各种操作,比如过滤、映射、组合等。
intercept(context, next: CallHandler): Observable<data<T>> {
// 调用next的handle方法来获取原始的响应Observable,然后通过pipe方法连接一系列的RxJS操作符。
return next.handle().pipe(
map((data) => {
return {
data,
status: 0,
success: true,
message: '牛逼',
};
}),
);
}
}
此时去 src\main.ts 去注册:
ts
// src\main.ts
import { Response } from './common/response';
app.useGlobalInterceptors(new Response());
此时可以看到效果:
3-7-2 异常拦截器
新建 src\common\exception.ts 文件,创建一个异常过滤器,它负责捕获作为HttpException类实例的异常,并为它们设置自定义响应逻辑。为此,我们需要访问底层平台 Request和 Response。我们将访问Request对象,以便提取原始 url并将其包含在日志信息中。我们将使用 Response.json()方法,使用 Response对象直接控制发送的响应。
ts
// src\common\exception.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
const status = exception.getStatus();
response.status(status).json({
data: exception.message,
time: new Date().getTime(),
success: false,
path: request.url,
status,
});
}
}
注册全局异常过滤器:
ts
// src\main.ts
import { HttpFilter } from './common/exception';
app.useGlobalFilters(new HttpFilter());
此时调用一个不存在的API可以看到过滤成功:
3-8 管道
管道可以做两件事:
- 转换,可以将前端传入的数据转成成我们需要的数据;
- 验证,类似于前端的 rules 配置验证规则。
3-8-1 管道转化
NestJS 提供了八个内置转换API:
- ValidationPipe
- ParseIntPipe
- ParseFloatPipe
- ParseBoolPipe
- ParseArrayPipe
- ParseUUIDPipe
- ParseEnumPipe
- DefaultValuePipe
案例一 我们接受的动态参数希望是一个number 类型,现在是string:
这时候就可以通过内置的整型转化管道去做转换:
案例2 验证UUID:
首先安装uuid:
bash
npm install uuid -S
npm install @types/uuid -D
如果接口需要传递一个uuid作为参数,则需要在后端中对参数进行校验:
如果传递的不是,则会返回提示:
是的话则正常请求通过。
3-8-2 管道验证
DTO的全称是Data Transfer Object,是一个用于客户端与后端服务传输数据的一种对象形式,在nestjs中更细节一点,就是前端传输的数据会在Controller控制层使用前,被转换成DTO数据对象,其实就是class类通过一些方式将数据赋值到这个类的实例对象上。
1、首先在某个模块下面创建一个pipe 验证管道和dto目录(里面包含xx.dto.ts文件):
bash
nest g pi 【管道名称】
此时会出现相应的文件:
2、安装验证器并书写我们的dto类:
bash
npm i --save class-validator class-transformer
ts
// src\pipe\dto\pipe.dto.ts
import { IsNotEmpty, IsString } from 'class-validator';
export class CreatePDto {
@IsNotEmpty() //验证是否为空
@IsString() //是否为字符串
name: string;
@IsNotEmpty()
age: number;
}
3、此时去 controller 使用管道和定义类型:
4、此时打印管道类(src\pipe\pipe.pipe.ts)里面的数据,可以看到 value 就是前端传过来的数据,metaData 就是元数据,通过 metatype 可以去实例化这个类:
5、此时去实例化DTO:
6、然后通过 validate 验证 DTO 返回一个promise 的错误信息,如果有错误抛出:
ts
// src\pipe\pipe.pipe.ts
import {
ArgumentMetadata,
HttpException,
HttpStatus,
Injectable,
PipeTransform,
} from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
@Injectable()
export class PipePipe implements PipeTransform {
async transform(value: any, metadata: ArgumentMetadata) {
// console.log(value, metadata);
const DTO = plainToInstance(metadata.metatype, value);
const errors = await validate(DTO);
if (errors.length > 0) {
console.log(`校验失败,不符合src\pipe\dto\pipe.dto.ts中的DTO校验规则`);
throw new HttpException(errors, HttpStatus.BAD_REQUEST);
}
console.log(DTO); // 输出:CreatePDto { name: '小明', age: 18 }
return value;
}
}
此时传递不符合校验规则的参数可以看错误被抛出:
7、注册全局DTO验证管道,可以选择自己写一个DTO验证通道,也可以用自带的(指的是不用自己写pipe文件了,但是dto文件还是要写),都是在src\main.ts中注册:
ts
// src\main.ts
import { ValidationPipe } from '@nestjs/common';
app.useGlobalPipes(new ValidationPipe());
3-9 守卫
守卫(guard) 有一个单独的责任。它们根据运行时出现的某些条件(例如权限,角色,访问控制列表等)来确定给定的请求是否由路由处理程序处理,这通常称为授权。在传统的 Express 应用程序中,通常由中间件处理授权(以及认证)。中间件是身份验证的良好选择,因为诸如 token 验证或添加属性到 request 对象上与特定路由(及其元数据)没有强关联。守卫在每个中间件之后执行,但在任何拦截器或管道之前执行。
1、创建一个守卫:
bash
nest g res guard
cd src/guard
nest g gu [权限名称]
可以看到,守卫要求实现函数 canActivate :给定参数执行上下文context,返回布尔值。
2、在 Controller 使用守卫(使用 @UseGuards 装饰器):
此时可以看到:
3、全局守卫,在src\main.ts通过
ts
app.useGlobalGuards(new RoleGuard())
实现。
4、针对角色控制守卫,通过 SetMetadata 装饰器 (第一个参数为key,第二个参数自定义我们的例子是数组存放的权限):
ts
import { Controller, Get, SetMetadata, UseGuards } from '@nestjs/common';
import { GuardService } from './guard.service';
import { RoleGuard } from './role/role.guard';
import { Reflector } from '@nestjs/core';
@Controller('guard')
@UseGuards(new RoleGuard(new Reflector()))
export class GuardController {
constructor(private readonly guardService: GuardService) {}
@Get('/role')
@SetMetadata('role', ['admin'])
roleGuard() {
return this.guardService.roleGuard();
}
}
此时去定义我们的守卫(guard 使用 Reflector 反射读取 setMetaData的值 去做判断这边例子是从url 判断有没有admin权限):
ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';
import type { Request } from 'express';
@Injectable()
export class RoleGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
console.log('经过了守卫');
// 使用Reflector服务来获取当前处理的处理器(handler)上的role元数据
const admin = this.reflector.get<string[]>('role', context.getHandler());
const request = context.switchToHttp().getRequest<Request>();
return admin.includes(request.query.role as string);
}
}
此时重新调用接口,可以看到如果当前没有不是值为admin的role,请求被拦截,否则请求成功:
3-10 自定义装饰器
在Nestjs 中我们使用了大量装饰器 decorator ,所以Nestjs 也允许我们去自定义装饰器。
1、案例1 自定义权限装饰器:
生成装饰器:
bash
nest g d [name]
可以看到生成一个src\guard\role\role.decorator.ts文件:
此时去controller中使用,和 @SetMetadata('role', ['admin']) 效果一致:
2、案例2 自定义参数装饰器返回一个url:
ts
// src\guard\role\role.decorator.ts
// 自定义参数装饰器返回一个url,通过createParamDecorator创建
export const ReqUrl = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
console.log(data); // 打印:传入的data
// 获取请求对象
const req = ctx.switchToHttp().getRequest<Request>();
return req.url;
},
);
在controller中使用:
3-11 swagger接口文档导出
首先安装需要的依赖:
bash
npm install @nestjs/swagger swagger-ui-express
然后在src\main.ts进行相应配置:
然后打开对应路径即可看到接口文档:
现在发现并没有分组很乱,可以使用 @ApiTags 添加分组,使用 @ApiOperation 添加接口描述:
此时可以看到生效:
还有很多其它文档导出相关的装饰器:
3-12 连接数据库
typeOrm 是 TypeScript 中最成熟的对象关系映射器( ORM )。因为它是用 TypeScript 编写的,所以可以很好地与 Nest 框架集成:
bash
npm install --save @nestjs/typeorm typeorm mysql2
建好数据库后再在src\app.module.ts里面注册:
ts
mysql -u root -p
CREATE DATABASE `nestjs-demo`;
ts
// src\app.module.ts
TypeOrmModule.forRoot({
type: 'mysql', // 数据库类型
username: 'root', // 账号
password: '123456', // 密码
host: 'localhost', // host
port: 3306, //
database: 'nestjs-demo', // 库名
entities: [__dirname + '/**/*.entity{.ts,.js}'], // 实体文件
synchronize: true, // synchronize字段代表是否自动将实体类同步到数据库
retryDelay: 500, // 重试连接数据库间隔
retryAttempts: 10, // 重试连接数据库的次数
autoLoadEntities: true, // 如果为true,将自动加载实体 forFeature()方法注册的每个实体都将自动添加到配置对象的实体数组中
}),
定义实体:
ts
// src\database\entities\database.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class DataBaseDemo {
//自增列
@PrimaryGeneratedColumn()
id: number;
//普通列
@Column()
name: string;
}
然后在module里面关联实体:
ts
// src\database\database.module.ts
imports: [TypeOrmModule.forFeature([DataBaseDemo])],
此时可以看到表被创建:
3-13 事务
3-13-1 事务的基本概念
-
事务的概念:事务是用户定义的一个数据库操作序列,这些操作要么全做要么全不做,是一个不可分割的工作单位。例如在关系数据库中,一个事务可以是一条SQL语句、一组SQL语句。
-
事务是恢复和并发控制的基本单位。
-
事务通常是以BEGIN TRANSACTION开始,以COMMIT或ROLLBACK结束。COMMIT表示提交,ROLLBACK表示回滚,在事务运行的过程中发生某种故障事务不能继续执行,系统就会将事务对数据库的已完成操作全部撤销,从而回滚到事务开始时的状态。
-
保证事务ACID特性是事务处理的重要任务。
-
事务的特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持续性(Durability),简称ACID
- 原子性:事务是数据库的逻辑工作单位,事务中包括的操作要么都做,要么都不做
- 一致性:事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。事务执行过程中出现故障则称这时的数据库处于不一致性状态。
- 隔离性:一个事务的执行不能被其他事务干扰,并发执行的各个事务之间不能互相干扰
- 持续性(永久性):一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。
-
事务的ACID特性可能遭到破坏的因素有:
- 多个事务并行运行时,不同事务的操作交叉执行
- 事务在运行过程中被强制停止
3-13-2 一个例子
例如用户A要给用户B转账300块 ,转账需要两步:1、首先用户A-300;2、用户B+300,只要任何一个步骤失败了都算失败。
默认数据库表已建好:
先去定义DTO用于校验参数:
然后在controller里面定义接口:
最后便可以在service里面定义具体的处理逻辑了:
ts
// src\database\database.service.ts
async transferMoney(tradeDTO: TradeDTO) {
console.log(tradeDTO); // 打印出{ fromId: 1, toId: 2, money: 300 }
try {
// this.trade_table.manager.transaction将返回该表的实体类
return this.trade_table.manager.transaction(async (trade_table) => {
const fromUser = await this.trade_table.findOne({
where: { id: tradeDTO.fromId },
});
const toUser = await this.trade_table.findOne({
where: { id: tradeDTO.toId },
});
/*
如果抛出异常,事务将回滚,
如果成功,事务将提交
*/
if (fromUser.money < tradeDTO.money) {
return '余额不足';
} else {
fromUser.money -= tradeDTO.money;
toUser.money += tradeDTO.money;
await trade_table.save(fromUser);
await trade_table.save(toUser);
return '转账成功';
}
});
} catch (e) {
throw new Error(e);
}
}
此时可以生效。