# nestJs **Repository Path**: jseven68/nest-js ## Basic Information - **Project Name**: nestJs - **Description**: nestJs学习记录 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 3 - **Forks**: 0 - **Created**: 2024-10-30 - **Last Updated**: 2025-02-26 ## Categories & Tags **Categories**: Uncategorized **Tags**: Nestjs, TypeScript ## README ## 基础 dist文件是实时编译的js文件,也就是ts等经过编译后能运行的文件,图片上传的图片也是在dist里面查看的。 ['./流程.png'] ## 基础命令 ```bash nest g [文件类型] [文件名] [文件目录] 1.生成controller.ts nest g co user 2.生成service.ts nest g s user 3.生成module.ts nest g mo user 4.生成dto.ts nest g class user.dto 5.生成entity.ts nest g class user.entity 6.生成guard.ts nest g gu user 7.生成pipe.ts nest g pi user 8.生成middleware.ts nest g mi user 9.生成interceptor.ts nest g in user 10.生成filter.ts # 生成标准模板,第一次使用这个命令的时候,除了生成文件之外还会自动使用 npm 帮我们更新资源,安装一些额外的插件,后续再次使用就不会更新了 nest g resource 名称 注意: 只有src/使用命令生成的文件会自动在app.module.ts中引入,如果是src/modules/这种自己创建了新的文件夹形似和的,使用命令生成的文件,需要手动引入到app.module.ts中 ``` ## 项目结构 ```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是应用程序的根模块,根模块提供了用来启动应用的引导机制,可以包含很多功能模块。 .mudule文件需要使用一个@Module() 装饰器的类,装饰器可以理解成一个封装好的函数,其实是一个语法糖(对装饰器不了解的,可以看走近MidwayJS:初识TS装饰器与IoC机制)。@Module() 装饰器接收四个属性:providers、controllers、imports、exports。 providers:Nest.js注入器实例化的提供者(服务提供者),处理具体的业务逻辑,各个模块之间可以共享; controllers:处理http请求,包括路由控制,向客户端返回响应,将具体业务逻辑委托给providers处理; imports:导入模块的列表,如果需要使用其他模块的服务,需要通过这里导入; exports:导出服务的列表,供其他模块导入使用。如果希望当前模块下的服务可以被其他模块共享,需要在这里配置导出; ```bash controller.ts # 常见功能就是用来处理http请求以及调用service层的处理方法 service.ts # 封装通用业务逻辑的、与数据层交互(如数据库),其他额外的一些第三方请求 module.ts # 模块,用来组织controller、service、dto、entity、guard、pipe、middleware、interceptor、filter等文件,处理模块之间的依赖关系,共享和引用等 ``` ## 控制器 nestjs 提供了方法参数装饰器 用来帮助我们快速获取参数 如下 | Decorator | Description | | ------------------------- | ----------------------------- | | `@Request()/@Req` | req | | `@Response()/@Res` | res | | `@Next()` | next | | `@Session()` | req.session | | `@Param(key?: string)` | req.params/req.params[key] | | `@Body(key?: string)` | req.body/req.body[key] | | `@Query(key?: string)` | req.query/req.query[key] | | `@Headers(name?: string)` | req.headers/req.headers[name] | | `@HttpCode` | | ## providers使用方式 ```ts // 1. 直接注入service // 2. 在controller钟使用直接用 constructor(private readonly userService: UserService) { } @Module({ providers: [MsgCodeService], }) // 2. 通过useClass注入service // 在controller钟使用需要 通过@inject() 注入 //constructor(@Inject('UserService') private readonly userService: UserService) { } @Module({ providers: [ { provide: 'MsgCodeService', // 自定义名称 useClass: MsgCodeService, }, ], }) /** * 3. 通过useValue自定义 * 在controller钟使用需要 通过@inject('custom') 注入 * @Inject('custom') private readonly custom: any */ @Module({ providers: [ { provide: 'custom', // useValue: ['1','2'], useValue: { name: 'custom', } }, ], }) /** * 4. 通过工厂模式 * 如果服务 之间有相互的依赖 或者逻辑处理 可以使用 useFactory * 在controller钟使用需要 通过@inject('') 注入 * */ @Module({ providers: [ { provide: 'factory', inject: userService, useFactory: (userService: UserService) =>{ return '通过Inject可以注入其他服务,然后参数中获取' } }, // 异步的工厂函数 // 异步注入只是服务端异步,当注入后,每次前端获取都是立刻得到的,不是每次都是异步 { provide: 'factory2', useFactory: async () => { return await new Promise((r) => { setTimeout(() => { r('异步的工厂函数') }, 3000) }) } } ], }) ``` ## 模块 ```ts // 1. 共享模块 // 通过exports导出,其他需要用的模块导入 @Module({ // providers: [UserService], // 通过这种useClass的方式,exports出去也要对应的useClass,因为自定义了名称,必须要一致才能共享出去 providers: [ { provide: 'user', useClass: UserService, }, ], controllers: [UserController], // 导出给其他模块使用 // exports: [UserService] exports: [ { provide: 'user', useClass: UserService, }, ], }) export class UserModule {} /** * 2. 全局模块 * 通过@Global()装饰器,全局模块,其他模块不需要导入 */ import { Global, Module } from '@nestjs/common'; import { GlobalService } from './global.service'; import { GlobalController } from './global.controller'; // 全局模块装饰器 @Global() // 全局模块 @Module({ controllers: [GlobalController], providers: [GlobalService], exports: [GlobalService], }) export class GlobalModule {} /** * 3. 动态模块 * 动态模块主要就是为了给模块传递参数 可以给该模块添加一个静态方法 用来接受参数 */ import { Global, Module, DynamicModule } from '@nestjs/common'; interface Config { path: string; } // 动态模块主要就是为了给模块传递参数 可以给该模块添加一个静态方法 用来接受参数 @Global() @Module({}) export class ConfigModule { static forRoot(option: Config): DynamicModule { return { module: ConfigModule, providers: [ { provide: 'CONFIG', useValue: 'baseUrl=' + option.path, }, ], exports: [ { provide: 'CONFIG', useValue: 'baseUrl=' + option.path, }, ], }; } } ``` ## 中间件 中间件是在路由处理程序 之前 调用的函数。 中间件函数可以访问请求和响应对象 中间件函数可以执行以下任务: 执行任何代码。 对请求和响应对象进行更改。 结束请求-响应周期,中间件响应了就不会往下走了。 调用堆栈中的下一个中间件函数。 如果当前的中间件函数没有结束请求-响应周期, 它必须调用 next() 将控制传递给下一个中间件函数。否则, 请求将被挂起。 ### 创建一个中间件 ```ts import { NestMiddleware } from '@nestjs/common'; // express的类型 import type { NextFunction, Request, Response } from 'express'; /** * 日志中间件 * NestMiddleware要通过nestjs的中间件约束,实现use方法 * * 中间可以指定用在某个类或者全局使用 */ export class Logger implements NestMiddleware { constructor() {} use(req: Request, res: Response, next: NextFunction) { console.log('Request...', '经过中间件'); next(); // 中间件响应也不会经过其他路由了 // res.send('响应-中间件') } } ``` ### 使用中间件 1. 某个组件使用 ```ts import { Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common'; import { MiddleWareTestService } from './middle-ware-test.service'; import { MiddleWareTestController } from './middle-ware-test.controller'; import { Logger } from '../middlewares/logger'; @Module({ controllers: [MiddleWareTestController], providers: [MiddleWareTestService], }) export class MiddleWareTestModule { // configure(consumer: MiddlewareConsumer) { consumer .apply(Logger) // 指定路由 // .forRoutes('middleWt'); // 指定路由和方法 // .forRoutes({ path: 'middleWt', method: RequestMethod.GET }); // 还可以直接丢整个控制器,这样这个组件的所有路由都会被中间件拦截 // .forRoutes(MiddleWareTestController) .forRoutes('*'); } } consumer.apply(Logger).forRoutes('cats'); // 指定路由中间件 ``` 2. 全局使用 注意全局中间件只能使用函数模式 案例可以做白名单拦截之类的 ```ts // main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { Logger } from './middlewares/logger'; async function bootstrap() { const app = await NestFactory.create(AppModule); // 全局中间件 app.use(Logger); await app.listen(3000); } bootstrap(); ``` ### 接入第三方中间件 例如 cors 处理跨域 ```bash npm install cors npm install @types/cors -D ``` ```ts // main.ts import * as cors from 'cors'; async function bootstrap() { const app = await NestFactory.create(AppModule); // 全局中间件 app.use(cors()); await app.listen(3000); } bootstrap(); ``` ## 图片上传和下载 ### 上传图片 1. 装包 2. 配置上传路径,上传保存文件夹 3. 创建上传接口,调用multer的接口,返回上传的图片地址 4. 设置全局静态文件目录,这样前端可以直接访问到图片 5. 装包 ```bash # 需要安装两个包 npm install multer @types/multer # 还需要一个@nestjs/platform-express 但是这个内置了不需要安装 ``` 2. 配置上传配置文件,设置上传文件夹和设置文件名 ```ts import { MulterModuleOptions } from '@nestjs/platform-express'; import * as fs from 'fs'; import { diskStorage } from 'multer'; import { extname, basename, join } from 'path'; export const storageConfig: MulterModuleOptions = { storage: diskStorage({ destination: (req, file, cb) => { // 配置上传目录 const dir = join(__dirname, '../uploads'); if (!fs.existsSync(dir)) { fs.mkdirSync(dir); } cb(null, dir); }, filename: (req, file, cb) => { // index.html -> .html 就是拿到文件后缀名 const ext = extname(file.originalname); // path.basename('/foo/bar/baz/asdf/quux.html'); // Returns: 'quux.html' const name = basename(file.originalname, ext); const filename = `${name}_${Date.now()}${ext}`; return cb(null, filename); }, }), }; ``` 3. 在控制器中调用配置,并用上传文件的装饰器处理图片 使用 UseInterceptors 装饰器 FileInterceptor是单个 读取字段名称 FilesInterceptor是多个 参数 使用 UploadedFile 装饰器接受file 文件 ```ts // modules先注册配置 import { Module } from '@nestjs/common'; import { UploadService } from './upload.service'; import { UploadController } from './upload.controller'; import { MulterModule } from '@nestjs/platform-express'; import { storageConfig } from '../../config/multerUploadConfig'; @Module({ // 注册MulterModule模块,并传入配置 imports: [MulterModule.register(storageConfig)], controllers: [UploadController], providers: [UploadService], }) export class UploadModule {} ``` ```ts // controller import { Controller, Get, Post, Body, Res, Param, Delete, UseInterceptors, UploadedFile, } from '@nestjs/common'; import { UploadService } from './upload.service'; import { FileInterceptor } from '@nestjs/platform-express'; @Controller('upload') export class UploadController { constructor(private readonly uploadService: UploadService) {} /** * 使用 UseInterceptors 装饰器 FileInterceptor是单个 读取字段名称 * FilesInterceptor是多个参数 * 使用 UploadedFile 装饰器接受file 文件 * @returns */ @Post('image') @UseInterceptors(FileInterceptor('file')) create(@UploadedFile() file: Express.Multer.File, @Res() res) { console.log('文件:', file); res.status(200).json({ message: '上传成功', file }); } } ``` 4. 配置静态文件访问 Express 方法 更加通用,适合需要高度定制化的情况。 NestJS 方法 更加简洁和类型安全,适合大多数 NestJS 应用的静态文件服务需求。 ```ts // main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { join } from 'path'; import * as express from 'express'; import { NestExpressApplication } from '@nestjs/platform-express'; async function bootstrap() { const app = await NestFactory.create(AppModule); // 全局中间件 // 前端访问http://localhost:3002/uploads/46d35e1f3fc2dc571428039836c5f9c_1731067193471.jpg app.useStaticAssets(join(__dirname, '..', 'uploads'), { prefix: '/uploads', }); // express方式 // app.use(express.static(join(__dirname, '..', 'uploads')) await app.listen(3000); } bootstrap(); ``` ### 下载图片 图片下载的方式有多种 #### download直接下载 ```ts @Get('download') download(@Res() res: Response) { // 正常下载图片路径数据库保存了,这里直接写死路径测试 const url = path.join(__dirname, '../../uploads/46d35e1f3fc2dc571428039836c5f9c_1731067307749.jpg'); // 直接使用 res.download 下载文件 // res.download(url); // 指定文件名 res.download(url, 'test.jpg'); } ``` #### stream流式下载 ```ts @Get('stream') download(@Res() res: Response) { // 正常下载图片路径数据库保存了,这里直接写死路径测试 const url = path.join(__dirname, '../../uploads/46d35e1f3fc2dc571428039836c5f9c_1731067307749.jpg'); // 使用 res.download 下载文件 // res.download(url); // 指定文件名 // res.download(url, 'test.jpg'); // 使用 stream 流式下载 const stream = fs.createReadStream(url); stream.pipe(res); } @Get('stream-zip') downloadByStreamZip(@Res() res: Response) { // 正常下载图片路径数据库保存了,这里直接写死路径测试 const url = path.join(__dirname, '../../uploads/46d35e1f3fc2dc571428039836c5f9c_1731067307749.jpg'); // 使用 stream 流式下载,前端需要使用 arraybuffer 接收,然后a标签下载 const stream = new zip.Stream(); stream.addEntry(url); res.setHeader('Content-Type', 'application/octet-stream'); res.setHeader('Content-Disposition', 'attachment; filename=example.zip'); stream.pipe(res); } ``` #### 删除图片 ```ts @Delete(':id') @Delete('remove/:id') deleteImg(@Param('id') id: string, @Res() res: Response) { try { fs.unlinkSync(path.join(__dirname, `../../uploads/${id}`)) return res.status(200).json({ message: '删除成功' + id }) } catch (error) { return res.status(500).json({ message: '删除失败' + id }) } } ``` ## nestjs 和 RxJs RxJs 使用的是观察者模式,用来编写异步队列和事件处理。 Observable 可观察的物件 Subscription 监听Observable Operators 纯函数可以处理管道的数据 如 map filter concat reduce 等 ```ts import { Observable, interval, take } from 'rxjs'; import { map, filter, reduce, find, findIndex } from 'rxjs/operators'; //类似于迭代器 next 发出通知 complete通知完成 const observable = new Observable((subscriber) => { subscriber.next(1); subscriber.next(2); subscriber.next(3); // subscriber.complete(); setTimeout(() => { subscriber.next(4); subscriber.complete(); }, 2000); }); observable.subscribe({ next: (value) => console.log('输出: ', value), error: (err) => console.log(err), complete: () => console.log('complete'), }); // test2 // interval(1000) 创建了一个 Observable,它会每隔1秒钟发出一个递增的整数值。这个值从0开始,每次增加1。 // interval 五百毫秒执行一次 pipe 就是管道的意思 // 管道里面也是可以去掉接口的支持处理异步数据 去处理数据 这儿展示 了 map 和 filter 跟数组的方法是一样的 最后 通过观察者 subscribe 接受回调 const subs = interval(500) .pipe( map((v) => ({ num: v })), filter((v) => v.num > 2), ) .subscribe((e) => { console.log('e: ', e); if (e.num > 10) { // 停止 subs.unsubscribe(); } }); // take控制输出次数 const subs2 = interval(500) .pipe(take(10)) .subscribe((e) => { console.log('e2: ', e); }); ``` ## session配置 1. 装包 > npm install express-session @nestjs/platform-express 如果是基于express的框架,不需要安装 @nestjs/platform-express了,内置了 > npm i @types/express-session -D // 类型声明文件 2. 配置 > 在main.ts中配置 ```ts import * as session from 'express-session'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { NestExpressApplication } from '@nestjs/platform-express'; async function bootstrap() { const app = await NestFactory.create(AppModule); // 配置session app.use( session({ secret: 'wwl', // 生成服务端session 签名 可以理解为加盐 name: 'wwl.session', // 生成客户端cookie 的名字 默认 connect.sid cookie: { // 设置返回到前端 key 的属性,默认值为{ path: ‘/’, httpOnly: true, secure: false, maxAge: null }。 httpOnly: true, maxAge: 1000 * 60 * 60 * 24 * 30, }, rolling: true, // 在每次请求时强行设置 cookie,这将重置 cookie 过期时间(默认:false) resave: false, saveUninitialized: false, // resave 为 true 是每次访问都会更新 session,不管有没有修改 session 的内容,而 false 是只有 session 内容变了才会去更新 session。 // saveUninitalized 设置为 true 是不管是否设置 session,都会初始化一个空的 session 对象。比如你没有登录的时候,也会初始化一个 session 对象,这个设置为 false 就好。 }), ); await app.listen(3000); } bootstrap(); ``` 3. 使用 > 在控制器中使用 ```ts import { Controller, Get, Req, Res, Session } from '@nestjs/common'; import { Request, Response } from 'express'; @Controller('session') export class SessionController { @Get('set') setSession(@Req() req: Request, @Res() res: Response) { req.session.name = '张三'; res.send('设置session'); } @Get('get') getSession(@Req() req: Request, @Res() res: Response) { res.send(req.session.name); } } ``` ## 拦截器 拦截器是使用 @Injectable() 装饰器注解的类。拦截器应该实现 NestInterceptor 接口。且具有以下功能: 在函数执行之前/之后绑定额外的逻辑 转换从函数返回的结果 转换从函数抛出的异常 扩展基本函数行为 根据所选条件完全重写函数 (例如, 缓存目的) ### 响应拦截 处理返回结果格式 注意通过 直接使用res.send()方法,会直接返回,不会经过拦截器 拦截器的执行顺序分为两个部分: 第一个部分在管道和自定义逻辑(next.handle()方法)之前。 第二个部分在管道和自定义逻辑(next.handle()方法)之后。 1. 创建拦截器 > 在src目录下common下,创建response.interceptor.ts文件 ```ts /** * 响应拦截器 * 这是nestjs内置的拦截 * 如果直接使用res.send()方法,会直接返回,不会经过拦截器 * * 拦截器的执行顺序分为两个部分: 第一个部分在管道和自定义逻辑(next.handle()方法)之前。 第二个部分在管道和自定义逻辑(next.handle()方法)之后。 */ import { Injectable, NestInterceptor, CallHandler, ExecutionContext, } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @Injectable() export class ResponseInterceptor implements NestInterceptor { intercept( context: ExecutionContext, next: CallHandler, ): Observable | Promise> { const ctx = context.switchToHttp(); // 获取http上下文 const res = ctx.getResponse(); // 获取响应对象 const req = ctx.getRequest(); // 获取请求对象 console.log('...全局响应拦截...'); return next.handle().pipe( map((data) => { console.log('...全局响应拦截返回内容...'); return { code: 200, data, message: 'success', }; }), ); } } ``` 2. 注册拦截器 可以在全局注册,也可以在模块中注册,同一路由注册多个拦截器时候,优先执行模块中绑定的拦截器,然后其拦截器转换的内容将作为全局拦截器的内容,即包裹两次返回内容 a. 全局注册 ```ts // 响应拦截 import { ResponseInterceptor } from './common/response'; // 全局响应拦截 app.useGlobalInterceptors(new ResponseInterceptor()); ``` b. 模块注册 在需要注册的controller控制器中导入ResponseInterceptor 然后从@nestjs/common中导入UseInterceptors装饰器 最后直接放置在对应的@Controller()或者@Post/@Get…等装饰器之下即可 ```ts import { Controller, Get, Req, Res, Session, UseInterceptors } from '@nestjs/common'; import { ResponseInterceptor } from '../../common/response.interceptor'; @Controller('user') @UseInterceptors(new ResponseInterceptor()) // 放整个模块 export class SessionController { @Get('set') @UseInterceptors(ResponseInterceptor) // 或者放单独的请求 getList(@Req() req: Request, @Res() res: Response) { ... } } ``` ### 请求拦截 ## 全局异常处理 简单来讲就是捕获系统抛出的所有异常,然后自定义修改异常内容,抛出友好的提示。 ### 创建异常过滤器 ```ts // common/filter import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger, } from '@nestjs/common'; import { Request, Response } from 'express'; /** * 全局异常过滤器 */ @Catch(HttpException) export class HttpExceptionFilter implements ExceptionFilter { // exception 当前正在处理的异常对象,例如 BadRequestException, NotFoundException 等 // ArgumentsHost 对象包含了异常发生时的所有上下文信息。它是一个包含原始函数调用参数的包装器, 是传递给原始处理程序的参数的一个包装(Response/Request)的引用 catch(exception: HttpException, host: ArgumentsHost) { console.log('进入全局异常过滤器...', exception); const ctx = host.switchToHttp(); // 获取请求上下文,因为 NestJS 支持多种传输层(如 WebSocket、GraphQL 等),而不仅仅限于 HTTP。通过调用 switchToHttp(),你可以确保获得的是与 HTTP 请求相关的上下文 const response = ctx.getResponse(); const request = ctx.getRequest(); // const status = exception.getStatus(); // 获取异常状态码 // data: (exception as any).response.message || exception.message // HttpException 属于基础异常类,可自定义内容 // 如果是自定义的异常类则抛出自定义的status // 否则就是内置HTTP异常类,然后抛出其对应的内置Status内容 const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; // 抛出错误信息 const message = (exception as any).response.message || exception.message; let msgLog = { statusCode: status, // 系统错误状态 timestamp: new Date().toISOString(), // 错误日期 path: request.url, // 错误路由 message: '请求失败', data: message, // 错误消息内容体(争取和拦截器中定义的响应体一样) }; // 打印错误综合日志 Logger.error('错误信息', JSON.stringify(msgLog), 'HttpExceptionFilter'); // 返回错误信息 response.status(status).json(msgLog); } } ``` ### 注册方式 > 同一路由注册多个管道的时候,只会执行一个异常过滤器,优先执行模块中绑定的异常过滤器,如果模块中无绑定异常过滤则执行全局异常过滤器 1、全局注册: 在main.ts中导入需要的模块如:HttpExceptionFilter 然后使用 app.useGlobalFilters(new HttpExceptionFilter()) 即可 ```ts import { HttpExceptionFilter } from './common/filter'; // 全局异常过滤器 app.useGlobalFilters(new HttpExceptionFilter()); ``` 2、模块注册: 在需要注册的controller控制器中导入HttpExceptionFilter 然后从@nestjs/common中导入UseFilters装饰器 最后直接放置在对应的@Controller()或者@Post/@Get…等装饰器之下即可 ```ts import { Controller, Get, Req, Res, Session, UseInterceptors } from '@nestjs/common'; import { HttpExceptionFilter } from '../../common/filter'; @Controller('user') @UseFilters(new HttpExceptionFilter()) // 放整个模块 export class SessionController { @Get('set') @UseFilters(new HttpExceptionFilter()) // 或者放单独的请求 getList(@Req() req: Request, @Res() res: Response) { ... } } ``` ### 内置异常类 系统提供了不少内置的系统异常类,需要的时候直接使用throw new XXX(描述,状态)这样的方式即可抛出对应的异常,一旦抛出异常,当前请求将会终止。 注意每个异常抛出的状态码有所不同。如: ```ts BadRequestException — 400 UnauthorizedException — 401 ForbiddenException — 403 NotFoundException — 404 NotAcceptableException — 406 RequestTimeoutException — 408 ConflictException — 409 GoneException — 410 PayloadTooLargeException — 413 UnsupportedMediaTypeException — 415 UnprocessableEntityException — 422 InternalServerErrorException — 500 NotImplementedException — 501 BadGatewayException — 502 ServiceUnavailableException — 503 GatewayTimeoutException — 504 ``` ## 管道 管道 可以做两件事 1.转换,可以将前端传入的数据转成成我们需要的数据 2.验证 类似于前端的rules 配置验证规则 ### 内置的api ```ts ValidationPipe:基于class-validator和class-transformer这两个npm包编写的一个常规的验证管道,可以从class-validator导入配置规则,然后直接使用验证(当前不需要了解ValidationPipe的原理,只需要知道从class-validator引规则,设定到对应字段,然后使用ValidationPipe即可) ParseIntPipe; ParseFloatPipe; ParseBoolPipe; ParseArrayPipe; ParseUUIDPipe; ParseEnumPipe; DefaultValuePipe, ParseFilePipe; ``` **第三方包:** class-validator:用装饰器和非装饰器两种方式对 class 属性做验证的库 class-transformer:把普通对象转换为对应的 class 实例的包 > npm install class-validator -S npm install class-transformer -S uuid包: > npm install uuid -S npm install @types/uuid -D ### 使用 1. 参数格式化 ```ts import { Controller, Get, Post, Body, Patch, Param, Delete, ParseIntPipe, ParseFilePipe, ParseUUIDPipe, Query, } from '@nestjs/common'; import { PipeTestService } from './pipe-test.service'; import { CreatePipeTestDto } from './dto/create-pipe-test.dto'; import * as uuId from 'uuid'; /** * 管道测试 */ @Controller('pipe-test') export class PipeTestController { constructor(private readonly pipeTestService: PipeTestService) {} @Post() create(@Body() createPipeTestDto: CreatePipeTestDto) { return this.pipeTestService.create(createPipeTestDto); } @Get() getIndex() { console.log('uuid--', uuId.v4()); return 'pipe-test'; } @Get('/uuid') findAll(@Query('uuid', ParseUUIDPipe) uuid: string) { // 参数uuid校验通过会往下执行,2dc44875-d813-4b5e-9849-9760abf4cc18 return this.pipeTestService.findAll(); } @Get(':id') findOne(@Param('id', ParseIntPipe) id: number) { console.log('id--', typeof id); return this.pipeTestService.findOne(+id); } @Get('/test2/:id') find( @Param( 'id', new ParseIntPipe({ errorHttpStatusCode: 404, // 可以自定义错误码 exceptionFactory: (errors) => { console.log(errors); return new HttpException('自定义错误信息', 400); }, // 可以自定义错误信息 }), ) id: number, ) { console.log('id--', typeof id); return this.pipeTestService.findOne(+id); } } ``` 2. dto文件使用校验 get请求可以直接校验参数,但是post请求参数很多就要使用dto ```ts /** post请求用dto校验 dto设置类型,然后引入管道new ValidationPipe() */ @Post('/save') // @UsePipes(new ValidationPipe()) save(@Body(new ValidationPipe()) createPipeTestDto: CreatePipeTestDto) { return createPipeTestDto.name } ``` ### 全局管道 #### 注册方式 1、全局注册: 在main.ts中导入需要的模块如:ValidationPipe 然后使用 app.useGlobalPipes(new ValidationPipe()) 即可 2、模块注册: 在需要注册的controller控制器中导入ValidationPipe 然后从@nestjs/common中导入UsePipes装饰器 最后直接放置在对应的@Controller()或者@Post/@Get…等装饰器之下即可,管道还允许注册在相关的参数上如:@Body/@Query… 等 ```ts import { Controller, Get, Req, Res, Session, UseInterceptors,UsePipes,ValidationPipe } from '@nestjs/common'; @Controller('user') @UsePipes(new ValidationPipe()) // 放整个模块,不需要new也可以 export class SessionController { @Get('set') @UsePipes(new ValidationPipe()) // 或者放单独的请求 getList(@Req() req: Request, @Res() res: Response) { ... } @Post('/save') save(@Body(new ValidationPipe()) body: any) { } } ``` ## 全局路由守卫 (guard) 守卫有一个单独的责任。它们根据运行时出现的某些条件(例如权限,角色,访问控制列表等)来确定给定的请求是否由路由处理程序处理。这通常称为授权。在传统的 Express 应用程序中,通常由中间件处理授权(以及认证)。中间件是身份验证的良好选择,因为诸如 token 验证或添加属性到 request 对象上与特定路由(及其元数据)没有强关联。 > tips 守卫在每个中间件之后执行,但在任何拦截器或管道之前执行。 > 同一路由注册多个守卫的执行顺序为,先是全局守卫执行,然后是模块中守卫执行 ### 创建守卫 > nest g gu [name] 完善文件 ```ts // src/guards/guard.guard.ts import { CanActivate, ExecutionContext, Injectable, HttpException, HttpStatus, } from '@nestjs/common'; import { Observable } from 'rxjs'; import { Request, Response } from 'express'; /** * 路由守卫 * 以拦截请求,验证token的合理性为例子 */ @Injectable() export class GuardGuard implements CanActivate { canActivate( context: ExecutionContext, ): boolean | Promise | Observable { console.log('路由守卫...'); const ctx = context.switchToHttp(); const request = ctx.getRequest(); // 获取请求头中的token字段 // const token = context.switchToRpc().getData().headers.token; const token = request.headers.cookie; console.log('request', request.headers.cookie); console.log('request', request.headers); // 如果白名单内的路由就不拦截直接通过 if (this.hasUrl(this.urlList, request.url)) { return true; } // 验证token的合理性以及根据token做出相应的操作 if (token) { try { // 这里可以添加验证逻辑 return true; } catch (e) { throw new HttpException( '没有授权访问,请先登录', HttpStatus.UNAUTHORIZED, ); } } else { throw new HttpException('没有授权访问,请先登录', HttpStatus.UNAUTHORIZED); } } // 白名单数组 private urlList: string[] = ['/user/login']; // 验证该次请求是否为白名单内的路由 private hasUrl(urlList: string[], url: string): boolean { let flag: boolean = false; if (urlList.indexOf(url) >= 0) { flag = true; } return flag; } } ``` ### 注册守卫 1、全局注册: 在main.ts中导入需要的守卫模块如:AuthGuard 然后使用 app.useGlobalGuards(new AuthGuard()) 即可 ```ts import { HttpExceptionFilter } from './common/filter'; import { GuardGuard } from './guard/guard.guard'; // 全局路由守卫 app.useGlobalGuards(new GuardGuard()); ``` 2、模块注册: 在需要注册的controller控制器中导入AuthGuard 然后从@nestjs/common中导入UseGuards装饰器 最后直接放置在对应的@Controller()或者@Post/@Get…等装饰器之下即可 ```ts import { Controller, Get, Req, Res, Session, UseInterceptors,UseGuards } from '@nestjs/common'; import { GuardGuard } from './guard/guard.guard'; @Controller('user') @UseGuards(new GuardGuard()) // 放整个模块,不需要new也可以 export class SessionController { @Get('set') @UseGuards(new GuardGuard()) // 或者放单独的请求 getList(@Req() req: Request, @Res() res: Response) { ... } } ``` ## 连接数据库 1. 安装依赖 > npm install --save @nestjs/typeorm typeorm mysql2 2. 配置数据库连接 > 在app.module.ts中配置数据库连接 ```ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from './user/user.entity'; import { UserController } from './user/user.controller'; import { UserService } from './user/user.service'; @Module({ imports: [ TypeOrmModule.forRoot({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'admin123', database: 'nest_demo', // 数据库命名:命名是小写,不要用驼峰命名法 // entities: [User], // 手动引入实体 entities: [__dirname + '/**/*.entity{.ts,.js}'], // 实体文件 synchronize:true, //synchronize字段代表是否自动将实体类同步到数据库 retryDelay:500, //重试连接数据库间隔 retryAttempts:10,//重试连接数据库的次数 autoLoadEntities:true, //如果为true,将自动加载实体 forFeature()方法注册的每个实体都将自动添加到配置对象的实体数组中 }) ] }) ``` 如果报错,确认不是连接问题,数据库也没问题,就去查看是否存在重名实体或者字段为空的实体 ## typeOrm操作 typeOrm 在一对一关系中,主实体是定义了 @OneToOne 关系并使用 @JoinColumn 注解的一方。 在一对关系中,主实体是定义了 @OneToMany 的一方。 在多对多关系中,主实体是定义了 @ManyToMany 关系并使用 @JoinTable 注解的一方。 如果开启级联关系,主实体保存,从实体也会保存,没有定义需要手动保存。 ### 实体 1. 实体使用@entity('user')或者@Entity({name:'user'})装饰器标记 2. 实体的表名一定要全部小写或者使用\_连接,不能用驼峰命名法d ```ts import { Entity, Column, PrimaryGeneratedColumn, OneToOne } from 'typeorm'; import { IdCard } from './idCard.entity'; // 用@Entity装饰器标记User类为实体类 @Entity({ name: 'user' }) export class User { //自增列 @PrimaryGeneratedColumn() id: number; //普通列 @Column({ type: 'varchar', length: 50, name: 'name', }) name: string; // 身份证和人一对一 @OneToOne(() => IdCard, (idCard) => idCard.user, {}) idCard: IdCard; // @OneToOne(() => IdCard, idCard => idCard.user,{ cascade: true, eager: true, onDelete: 'CASCADE', onUpdate: 'CASCADE' }) // idCard: IdCard } ``` ### 实体关系 这种关系只需要设置在两个实体的一边就可以了 > { cascade: true, eager: true, onDelete: 'CASCADE', onUpdate: 'CASCADE' } > cascade: true,级联保存,级联删除,级联更新 > eager: true,懒加载,开启后,查询主表时,会自动查询关联表 > onDelete: 'CASCADE',级联删除 > onUpdate: 'CASCADE',级联更新 ```ts cascade: true 作用范围:应用程序层面,由 TypeORM 控制。 触发时机:当你在应用程序中调用 save、update 或 remove 方法时,这些操作会自动应用到相关的子实体上。 具体行为: 保存:当你保存父实体时,相关的子实体也会被自动保存。 更新:当你更新父实体时,相关的子实体也会被自动更新。 删除:虽然 cascade: true 会处理保存和更新操作,但默认情况下它不会处理删除操作。你需要显式地调用 remove 方法来删除子实体。 onDelete: 'CASCADE' 作用范围:数据库层面,由数据库引擎控制。 触发时机:当你在应用程序中调用 remove 方法或直接在数据库中执行删除操作时,相关的子实体也会被自动删除。 具体行为: 删除:当你删除父实体时,相关的子实体也会被自动删除。 onUpdate: 'CASCADE' 作用范围:数据库层面,由数据库引擎控制。 触发时机:当你在应用程序中调用 update 方法或直接在数据库中执行更新操作时,相关的子实体也会被自动更新。 具体行为: 更新:当你更新父实体的主键或其他重要字段时,相关的子实体也会被自动更新 // cascade:true和onUpdate都能级联更新 // 上面设置的cascade:true和onUpdate只是关联更新时候,只是把外键置空,并没有删除 // 默认是从实体没有关联主实体才能删除,两边都设置onDelete: 'CASCADE'就可以删除,主实体删除了从实体关联也会删除 eager: true:在定义关系时设置 eager: true,TypeORM 会在每次查询主实体时自动加载关联实体。 relations 选项:在查询时使用 relations 选项手动指定要加载的关联关系,提供更大的灵活性和性能控制。 ``` ### 模块中使用 1. 使用 `TypeOrmModule.forFeature` 方法注册实体 ```ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from './user/user.entity'; import { UserController } from './user/user.controller'; import { UserService } from './user/user.service'; @Module({ imports: [TypeOrmModule.forFeature([User])], controllers: [UserController], providers: [UserService], }) export class UserModule {} ``` 2. 在服务中使用 `@InjectRepository` 装饰器注入 `Repository` 实例 引入 InjectRepository typeOrm 依赖注入 接受一个实体 引入类型 Repository 接受实体泛型 ```ts import { Injectable } from '@nestjs/common'; import { User } from '../../entity/user.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; @Injectable() export class SqlTestService { constructor( @InjectRepository(User) private readonly userRepository: Repository, ) {} } ``` ### 增删改查 **注意**: sql方法都是异步的 1. 新增用户用save方法 想那种需要关联的,比如books关联多个tags,创建book时候会传多个tags,这时候如果传的id格式,那么直接全部查出来 ```ts /** * 创建书籍 * 先创建的tag标签,然后再创建书,书籍可以选择多个标签 * @param createSqlMtmDto */ async create(createSqlMtmDto: CreateSqlMtmDto) { try { const book = new Book() book.name = createSqlMtmDto.name // 如果有标签,则创建标签 if (createSqlMtmDto.tags) { // 全部查出覆盖 const tags = await this.tagsRepository.find({ where: createSqlMtmDto.tags.map(id => ({ id })), }); book.tags = tags } return this.bookRepository.save(book) } catch (error) { throw new HttpException('创建失败', 500) } } ``` 如果传的完整参数,那么直接创建实体 ```ts /** * 关联 * @param createSqlOTmDto * @returns */ async create(createSqlOTmDto: CreateSqlOTmDto,) { // return 'This action adds a new sqlOTm'; const person = new People(); person.name = createSqlOTmDto.name; person.phones = [] if (createSqlOTmDto.phones) { // 支持新增时候设置手机 for (let i = 0; i < createSqlOTmDto.phones.length; i++) { const phoneEntity = new Phone(); phoneEntity.name = createSqlOTmDto.phones[i].name; phoneEntity.number = createSqlOTmDto.phones[i].number; person.phones.push(phoneEntity); // 不设置级联需要手动保存 // await this.phoneRepository.save(phoneEntity); } } return await this.peopleRepository.save(person); } ``` 2. 修改用户也可以用save方法,有主键就变成更新,或者用Update方法,update方法不需要一个实体就可以更新 ```ts await this.idCardRepository.save(user.idCard); // const card = new IdCard() // update更新不需要创建一个新的实体 const card = { cardId: updateSqlTestDto.cardId, }; // card.user = user1; 不能加,否则会报错 await this.idCardRepository.update(user.idCard.id, card); ``` 3. 查找 查找所有find, 查找一个 findOne ```ts this.userRepository.find({ // 关联查询 relations: ['idCard'], order: { id: 'DESC', }, // skip: (query.page - 1)* query.pageSize, // take:query.pageSize, }); this.userRepository.findOne({ where: { id: id }, }); ``` 4. 删除方法 ```ts const user = await this.userRepository.findOne({ relations: ['idCard'], where: { id: id }, }); // 直接传入实体 await this.userRepository.remove(user); ``` ### 注意 1. 如果某些报错没进入全局异常过滤器,排查是不是异步的数据库操作没写`await`,如 ```ts async remove(id: number) { try { if (!id) { throw new HttpException('id不能为空', 500) } // 要加await,不能失败没有进入全局异常过滤器 return await this.bookRepository.delete(id) } catch (error) { throw new HttpException('删除失败' + error.message, 500) } } ``` ## nestjs在vscode中调试 1. 在launch.json中添加配置 点击左侧栏目的调试,创建launch.json文件,添加配置 ```json { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Launch Program", "skipFiles": ["/**"], "program": "${file}", "preLaunchTask": "tsc: build - tsconfig.json", "outFiles": ["${workspaceFolder}/dist/**/*.js"] }, // 通过npm run start:debug 启动 // "start:debug": "nest start --debug --watch", // 然后在代码中打 debugger 断点,然后点击调试按钮,选择Attach,就可以调试了 { "name": "Attach", "port": 9229, "request": "attach", "skipFiles": ["/**"], "type": "node" }, // 运行npm脚本命令的方式逆行调试, 创建一个via npm的方式 // 这种直接点左侧 调试里面的 RUN And DEBUG 的绿色启动就行了,不需要手动终端跑命令,这里配置了命令了 { "type": "pwa-node", "request": "launch", "name": "debug nest", "runtimeExecutable": "npm", // 代表执行什么命令 "args": ["run", "start:dev"], "skipFiles": ["/**"], "console": "integratedTerminal" // 用 vscode 的内置终端来打印日志 } ] } ``` 2. 通过vscode自带的调试终端 终端选择 javascript Debug Terminal,然后输入命令 npm run start:dev ,就可以 但是要先安装 插件 JavaScript Debugger (Nightly) ## Project setup ```bash $ pnpm install ``` ## Compile and run the project ```bash # development $ pnpm run start # watch mode $ pnpm run start:dev # production mode $ pnpm run start:prod ```