# nestjs-study **Repository Path**: wu-chibiao/nestjs-study ## Basic Information - **Project Name**: nestjs-study - **Description**: nestjs 学习 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2024-02-27 - **Last Updated**: 2024-03-14 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 安装及相关命令 ```shell npm i -g @nestjs/cli 安装nestjs nest new project-name 创建一个nestjs项目 nest g mo module-name 创建模块 nest g s service-name 创建服务 nest g co controller-name 创建控制器 --flat 在app同级目录下创建模块/服务/控制器(后缀) nest g co controller-name --flat --no-spec 不创建测试文件(后缀) nest g co controller-name --no-spec -d 演示创建后的文件的位置,不会真的创建文件(后缀) nest g co controller-name -d ``` 安装相关依赖 ```shell npm i prisma-binding @prisma/client mockjs @nestjs/config class-validator class-transformer argon @nestjs/passport passport passport-local @nestjs/jwt passport-jwt lodash multer dayjs express ``` ## 服务提供者 ### 提供类的注册方式 ![0](./images/2-1-1.jpg) ![0](./images/2-1-2.jpg) app.module.ts ```typescript providers: [ AppService, {provide:'wcbservice',useClass:WcbService} ] ``` app.service.ts ```typescript @Inject('wcbservice') ``` ### 基本类型的提供者注册 ![0](./images/2-2-1.jpg) ![0](./images/2-2-2.jpg) ```typescript providers: [ AppService, {provide:'wcbservice',useValue:'...'} ] ``` ### 动态注入服务提供者 ![0](./images/2-3-1.jpg) ![0](./images/2-3-2.jpg) ### 通过服务动态加载配置项 ![0](./images/2-4-1.jpg) ![0](./images/2-4-2.jpg) ### 使用工厂函数注册提供者 ![0](./images/2-5-1.jpg) ```typescript useFactory(){ } ``` ### 模块共享 现在有 test 和 wcb 两个模块,目的:在 wcb 的控制器中使用 test 模块的服务 第一步,需要在 test.module.ts 中暴露出 xxx 服务 ```typescript @Module({ providers: [ TestService, { provide:'testData', useValue:'test data' } ], controllers: [TestController,], exports:[TestService,'testData'] //这是需要暴露出去的 }) ``` ![0](./images/2-6-1.jpg) 第二步,在 wcb.module.ts 引入 test 模块 ```typescript @Module({ imports:[TestModule], // 这里把 test 模块引进来了 providers: [WcbService], controllers: [WcbController] }) ``` ![0](./images/2-6-2.jpg) ### 异步服务提供者 ![0](./images/2-7-1.jpg) ## 模块定义 ### 初始化配置模块并读取 新增 config 模块 ![0](./images/3-1-1.png) 创建配置文件 ![0](./images/3-1-2.png) 读取模块配置项 ![0](./images/3-1-4.png) ### 动态配置项模块 config/config.module.ts ```typescript import { DynamicModule, Global, Module } from '@nestjs/common'; import { ConfigService } from './config.service'; @Global() @Module({ providers: [ConfigService], exports:[ConfigService] }) export class ConfigModule { static resgister(options:{path:string}):DynamicModule{ return { module:ConfigModule, providers:[{provide:'test',useValue:options}], exports:['test'] } } } ``` config/config.service.ts ```typescript import { Inject, Injectable, Optional } from '@nestjs/common'; import { readdirSync } from 'fs'; import path from 'path'; @Injectable() export class ConfigService { constructor( @Inject('test') 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(path:string){ return path.split('.').reduce((config,name) => config[name], this.config) } } ``` app.module.ts ```typescript import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ConfigModule } from './config/config.module'; import { ArticleModule } from './article/article.module'; import path from 'path'; const configPath = path.resolve(__dirname,'./configure') @Module({ imports: [ConfigModule.resgister({path:configPath}), ArticleModule], controllers: [AppController], providers: [AppService], }) export class AppModule {} ``` app.controller.ts ```typescript import { Controller, Get, Inject } 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('database.url') } } ``` ## Prisma 管理 Mysql 数据库 ### 安装及命令 ```shell npm i prisma -D Set up a new Prisma project $ prisma init Generate artifacts (e.g. Prisma Client) $ prisma generate Browse your data $ prisma studio Create migrations from your Prisma schema, apply them to the database, generate artifacts (e.g. Prisma Client) $ prisma migrate dev Pull the schema from an existing database, updating the Prisma schema $ prisma db pull Push the Prisma schema state to the database $ prisma db push Validate your Prisma schema $ prisma validate Format your Prisma schema $ prisma format Display Prisma version info $ prisma version Display Prisma debug info $ prisma debug ``` ### 定义模型 ``` generator client { provider = "prisma-client-js" } datasource db { provider = "mysql" url = env("DATABASE_URL") } model user{ id Int @id @default(autoincrement()) @db.Int email String @db.Char(50) password String? avatar String? github String? douyin String? weibo String? waketime String? createAt DateTime @default(now()) updateAt DateTime @updatedAt } model category { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt title String articles article[] } model article{ id BigInt @id @default(autoincrement()) @db.UnsignedBigInt title String content String @db.Text thumb String? createAt DateTime @default(now()) updateAt DateTime @updatedAt category category? @relation( fields: [categoryId],references: [id],onDelete: Cascade) categoryId BigInt? @db.UnsignedBigInt } ``` ### 填充数据 1. 创建 seeds 文件夹 放置各个模型的填充逻辑 seeds.ts help.ts工具 ![0](./images/4-3-1.png) 2. seeds.ts ```typescript import { user } from "./seeds/user"; import { category } from "./seeds/category"; import { article } from "./seeds/article"; // mock.js async function run(){ await user() await category() await article() } run() ``` 3. help.ts 工具 ```typescript import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); export async function create( count: number = 1, callback:(prisma:PrismaClient)=>void) { for(let i = 0; i{ await prisma.article.create({ data:{ title:Random.ctitle(), content:Random.cparagraph(10,50), categoryId:2 } }) }) } ``` 5. 运行命令 ```shell npx prisma migrate reset ``` ## 配置管理 - 缺点:获取的时候没有提示信息 ### 使用插件 ```she npm i nestjs/config ``` app.module.ts ConfigModule.forRoot({ isGlobal:true }) 注册配置,默认使用.env 文件中的配置信息 ```typescript import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ConfigModule } from '@nestjs/config'; // 这里引入ConfigModule配置 import appConfig from './app.config'; @Module({ imports: [ConfigModule.forRoot({ // ConfigModule.forRoot({ isGlobal:true }) 注册配置 isGlobal:true, })], controllers: [AppController], providers: [AppService], }) export class AppModule {} ``` .env ——在这个文件中写你的配置 ```env APP_NAME = wcb NODE_ENV = development ``` app.controller.ts ```typescript import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; import { ConfigService } from '@nestjs/config'; @Controller() export class AppController { constructor( private readonly appService: AppService, private readonly config: ConfigService // 依赖注入 ) {} @Get() getHello(): string { return this.config.get('APP_NAME'); // 此时就可以访问到.env文件中的配置项了 } } ``` ### 自定义配置文件 app.config.ts 暴露配置对象 ```typescript export default ()=>({ app:{ name:'wcb', isDev:process.env.NODE_ENV === 'development' ?true:false }, upload:{ exts:'jpeg png' } }) ``` app.module.ts 使用 load 选项引入**自定义配置文件** ```typescript import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ConfigModule } from '@nestjs/config'; import appConfig from './app.config'; @Module({ imports: [ConfigModule.forRoot({ isGlobal:true, load:[appConfig] })], controllers: [AppController], providers: [AppService], }) export class AppModule {} ``` app.controller.ts 利用 `this.config.get('upload.exts')` 这种方式获取配置信息 ```typescript import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; import { ConfigService } from '@nestjs/config'; @Controller() export class AppController { constructor( private readonly appService: AppService, private readonly config: ConfigService ) {} @Get() getHello(): string { console.log(this.config.get('upload.exts')); console.log(this.config.get('app.name')); return this.config.get('APP_NAME'); } } ``` ### 多配置文件定义 ![0](./images/5-3-1.png) config/app.config ```typescript export default ()=>({ app:{ name:'wcb', isDev:process.env.NODE_ENV === 'development' ?true:false }, }) ``` config/index.ts ```typescript import appConfig from "./app.config"; import uploadConfig from "./upload.config"; export default [appConfig,uploadConfig] ``` app.module.ts ```typescript import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ConfigModule } from '@nestjs/config'; import config from './config'; @Module({ imports: [ConfigModule.forRoot({ isGlobal:true, load:[...config] })], controllers: [AppController], providers: [AppService], }) export class AppModule {} ``` ### 基于命名空间配置 config/database.config.ts ```typescript import { registerAs } from "@nestjs/config"; export default registerAs('database',()=>({ host: 'localhost', port: 3306, })) ``` config/index.ts ```typescript import appConfig from "./app.config"; import databaseConfig from "./database.config"; import uploadConfig from "./upload.config"; export default [appConfig,uploadConfig,databaseConfig] ``` app.module.ts ```typescript import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ConfigModule } from '@nestjs/config'; import config from './config'; @Module({ imports: [ConfigModule.forRoot({ isGlobal:true, load:[...config] })], controllers: [AppController], providers: [AppService], }) export class AppModule {} ``` app.controller.ts 利用 `'@nestjs/config'`中的 `ConfigType` 使用 `@Inject(databaseConfig.KEY) private database:ConfigType` ```typescript // 也可自定义类型工具 // type getType any> = T extends () => infer U ? U : T; // type databaseType = getType; ``` ```ty import { Controller, Get, Inject } from '@nestjs/common'; import { AppService } from './app.service'; import { ConfigService, ConfigType } from '@nestjs/config'; import databaseConfig from './config/database.config'; @Controller() export class AppController { constructor( private readonly appService: AppService, private readonly config: ConfigService, @Inject(databaseConfig.KEY) private database:ConfigType ) {} @Get() getHello(): string { // console.log(this.config.get('upload.exts')); // console.log(this.config.get('app.name')); // return this.config.get('APP_NAME'); // type getType any> = T extends () => infer U ? U : T; // type databaseType = getType; return this.database.host } } ``` ## 管道与验证 ### 初识管道 ```shell nest g pi pipeName --no-spec --flat ``` - 我的案例将 pipeName 取名为 hd hd.pippe.ts 自定义管道文件 ```typescript import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common'; @Injectable() export class HdPipe implements PipeTransform { transform(value: any, metadata: ArgumentMetadata) { console.log(value,metadata); // value 参数数据 metadata 数据其他信息 return metadata.metatype === Number ? +value : value; } } ``` app.controller.ts ```typescript import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common'; import { AppService } from './app.service'; import { PrismaClient } from '@prisma/client'; import { HdPipe } from './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({ where:{ id, } }) } } ``` nest 也有许多内置的管道 ![0](./images/6-1-1.png) ### 管道多种注册方式 管道不止可以注册在 param 中,也可以注册在 `方法层面` `控制器层面` `模块层面` ``` @usePipe(hdPipe) //`方法层面` `控制器层面` ``` 模块层面 app.module.ts ```typescript import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { HdPipe } from './hd.pipe'; @Module({ imports: [], controllers: [AppController], providers: [ AppService, { provide:'APP_PIPE', // 名称固定式 APP_PIPE useClass:HdPipe } ], }) export class AppModule {} ``` 全局管道注册 main.ts ```typescript import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { HdPipe } from './hd.pipe'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes(new HdPipe()) //全局管道注册 await app.listen(3000); } bootstrap(); ``` 默认值管道 ![0](./images/6-1-2.png) ### dto管道验证 定义管道文件 ![0](./images/7-1-1.png) 下载包 ```shell npm i class-validator class-transformer ``` create.article.dto.ts ```typescript import { Length, IsNotEmpty } from "class-validator"; export default class CreateArticleDto { @IsNotEmpty({message:'标题不能为空'}) @Length(10,100,{message:'标题不能少于十个字'}) title:string; @IsNotEmpty({message:'内容不能为空'}) content:string; } ``` hd.pipe.ts 利用 plainToInstance 和 validate 验证数据 ```typescript import { ArgumentMetadata, BadRequestException, 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) { const object = plainToInstance(metadata.metatype,value) // 为参数赋予 dto 规定的类型 const errors = await validate(object) // 获取错误信息数组 if(errors.length > 0){ const messages = errors.map((error)=>( { name:error.property, message:Object.values(error.constraints).map(v=>v) } )) throw new HttpException(messages,HttpStatus.UNPROCESSABLE_ENTITY) } return value } } ``` app.controller.ts ```typescript import { Body, Controller, DefaultValuePipe, Get, Param, ParseIntPipe, Post, UsePipes } from '@nestjs/common'; import { AppService } from './app.service'; import { PrismaClient } from '@prisma/client'; import { HdPipe } from './hd.pipe'; import CreateArticleDto from './dto/create.article.dto'; @Controller() export class AppController { prisma:PrismaClient constructor(private readonly appService: AppService) { this.prisma = new PrismaClient(); } @Post('store') async add(@Body(HdPipe) dto:CreateArticleDto){ const article = await this.prisma.article.create({ data:{ title:dto.title, content:dto.content } }) console.log(article); } } ``` ### 系统验证管道 创建 validate.ts ,定义 Validate 类 继承内置的 ValidationPipe 管道类 ,重写 mapChildrenToValidationErrors 方法。 ```typescript import { ValidationPipe } from "@nestjs/common"; import { ValidationError } from "class-validator"; export class Validate extends ValidationPipe{ protected mapChildrenToValidationErrors(error: ValidationError, parentPath?: string): ValidationError[]{ const errors = super.mapChildrenToValidationErrors(error,parentPath) console.log(errors); errors.map(error=>{ for(const key in error.constraints){ error.constraints[key] = error.property + '-' + error.constraints[key] } }) return errors } } ``` 这里选择在 main.ts 全局注册自定义的 validate ```typescript import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { Validate } from './validate'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes(new Validate()) await app.listen(3000); } bootstrap(); ``` ### 过滤器处理验证异常 创建 validate-exception.filter.ts ```typescript import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common'; @Catch() export class ValidateExceptionFilter 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],message:info[1] }; }) }) } return response } } ``` 全局注册 main.ts ```typescript app.useGlobalFilters(new ValidateExceptionFilter()) ``` ![0](./images/8-1-1.png) ### 自定义验证规则 rules/is-confirmed.rule.ts 使用类的方式 ```typescript import { ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from "class-validator"; @ValidatorConstraint({ name:'IsConfirm',async:true }) export class IsConfirmed implements ValidatorConstraintInterface{ async validate(value: any, args?: ValidationArguments) { return value === args.object[args.property + '_confirmed']; } defaultMessage(args?: ValidationArguments): string { return '比对失败' } } ``` is-not-exists.rule.ts 使用装饰器的方式 ```typescript import { registerDecorator,ValidationArguments,ValidationOptions } from "class-validator"; import { PrismaClient } from "@prisma/client"; // 表字段是否唯一 export function IsNotExistsRule( table:string, validationOptions?:ValidationOptions ){ return function (object:Record,propertyName:string){ registerDecorator({ name:'IsNotExistsRule', target:object.constructor, propertyName: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, } }) console.log(user); return !Boolean(user); } } }) } } ``` /auth/dto/register.dto.ts ![0](./images/9-1-1.png) ## 登录流程 jwt认证 ### 登录获取token auth/dto/login.dto.ts ```typescript import { IsNotEmpty } from "class-validator" export default class LoginDto{ @IsNotEmpty({message:"用户名不能为空"}) name:string @IsNotEmpty({message:"密码不能为空"}) password:string } ``` ![0](./images/10-1-1.png) auth/auth.service.ts ```typescript ... import { hash,verify } from 'argon2' ... @Injectable() export class AuthService { constructor(private prisma:PrismaService,private jwt:JwtService){} ... async login(dto:LoginDto){ const user = await this.prisma.user.findUnique({ where:{ name:dto.name } }) if(!(await verify(user.password,dto.password))){ throw new BadRequestException('密码输入错误') } return this.token(user) } ... } ``` ### 应用 token auth/jwt.strategy.ts 定义策略 ```typescript import { Injectable } from "@nestjs/common"; import { Strategy,ExtractJwt } from 'passport-jwt' import { PassportStrategy } from "@nestjs/passport"; import { ConfigService } from "@nestjs/config"; import { PrismaService } from "src/prisma/prisma.service"; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy,'jwt'){ // 策略名字为 jwt constructor(configService:ConfigService,private prisma:PrismaService){ super({ // 解析用户提交的 Bearer Token header 数据 jwtFromRequest:ExtractJwt.fromAuthHeaderAsBearerToken(), // 加密码的 secret secretOrKey:configService.get('TOKEN_SECRET') }) } // 验证通过后结果用户资料 async validate({sub:id}){ return this.prisma.user.findUnique({ where:{ id } }) } } ``` auth模块注册策略 ![0](./images/10-2-1.png) 在需要 token 的f方法中注入 ![0](./images/10-2-2.png) 获得身份标识 ![0](./images/10-2-3.png) ### 使用装饰器简化操作 auth/decorator/auth.decorator.ts 聚合装饰器 操作jwt验证 ```typescript import { applyDecorators,UseGuards } from "@nestjs/common"; // 聚合装饰器 import { AuthGuard } from "@nestjs/passport"; export function Auth(){ return applyDecorators(UseGuards(AuthGuard('jwt'))) } ``` auth/decorator/user.decorator.ts 参数装饰器 ```typescript import { ExecutionContext, createParamDecorator } from "@nestjs/common"; export const User = createParamDecorator((data:unknown,ctx:ExecutionContext)=>{ // 参数装饰器 const request = ctx.switchToHttp().getRequest() return request.user }) ``` 应用 ![0](./images/10-3-1.png)