侧边栏壁纸
博主头像
xuesheng博主等级

分享web知识,学习就是取悦自己!

  • 累计撰写 118 篇文章
  • 累计创建 14 个标签
  • 累计收到 3 条评论

目 录CONTENT

文章目录

NestJs Tidy记录

xuesheng
2022-10-05 / 0 评论 / 2 点赞 / 329 阅读 / 6,026 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-11-26,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

环境文件配置

一、简单的使用.env文件实现配置文件

  • 1、安装依赖包
pnpm add dotenv
pnpm add @types/dotenv -D
  • 2、根目录下创建一个.env的文件,如果是线上的项目将env加入到.gitignore
  • 3、在.env文件中写入端口号及项目前缀
# 项目前缀
PREIFX = api/v1
# 端口号
PORT = 8000
  • 4、在main.ts中使用
import 'dotenv/config';//加载环境文件.env
import { Logger, VersioningType } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { getConfig, IS_DEV } from './utils';

export const config = getConfig();
const PORT = config.PORT || 8080;
const PREFIX = config.PREFIX || '/';
console.log(`Port: ${PORT}`, IS_DEV);

async function bootstrap() {
  const logger: Logger = new Logger('main.ts');
  const app = await NestFactory.create(AppModule, {
    // 开启日志级别打印
    logger: IS_DEV ? ['log', 'debug', 'error', 'warn'] : ['error', 'warn'],
  });
  //允许跨域请求
  app.enableCors();

  // 启动版本管理
  app.enableVersioning({
    defaultVersion: '1', // 不指定默认版本为v1
    type: VersioningType.URI,
  });

  // 给请求添加prefix
  app.setGlobalPrefix(PREFIX);
  await app.listen(PORT, () => {
    logger.log(`服务已经启动,接口请访问:http://wwww.localhost:${PORT}/${PREFIX}`);
  });
}
bootstrap();
  • 5、这种方式使用简单,只要安装两个依赖包就可以了,但是不足之处在于不能区分开发环境和生产环境配置,都统一写在了.env文件中,每次要使用的时候还要process.env.xx的方式来使用,下面会介绍如何区分开发环境和生产环境使用不同的配置文件,方便环境中的变量管理

二、使用config的依赖包来方便获取配置文件中的配置

pnpm add @nestjs/config
  • 2、在app.module.ts中使用
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath,
    }),
  ],
})
export class AppModule {}

  • 3、在需要获取配置的地方使用ConfigService提供的一个get方法来获取
const PORT = this.configService.get<string>('PORT');
console.log('当前的端口:',PORT);

  • 4、如果使用了@nestjs/config这个包,在main.ts文件中首部可以不用引入import 'dotenv/config'

三、使用.env的方式来区分环境

  • 1、在项目根目录下分别创建.env.env.dev.env.prod文件丙炔在ConfigModule中加载

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [ConfigModule.forRoot(
  		isGlobal: true,
    	envFilePath:['.env.dev','.env', '.env.prod'],
  )], 
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
  • 2、根据官网上的介绍,前面的优先级比后面的优先级高,简单理解,就是获取的时候会先查找envFilePath数组的第一项,其次之后。
  • 3、安装依赖包来配置并设置当前环境、上面使用process.env.NODE_ENV已经不能判断当前是什么环境了
pnpm add cross-env	
  • 4、手动配置启动环境,修改package.json文件启动命令
···
"scripts": {
    "prebuild": "rimraf dist",
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "cross-env RUNNING_ENV=dev nest start",
    "start:dev": "cross-env RUNNING_ENV=dev nest start --watch",
    "start:debug": "nest start --debug --watch",
    "start:prod": "cross-env RUNNING_ENV=prod node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json",
    "pm2:prod": "cross-env RUNNING_ENV=prod pm2-runtime start ecosystem.config.js",
    "pm2:dev": "pm2 start ecosystem.config.js --env development",
    "pm2:test": "pm2 start ecosystem.config.js --env test",
    "prepare": "husky install"
  }
···
  • 5、重新定义获取当前环境的变量,注意不能第一在main.ts中,必须在app.module.ts文件中
export const IS_DEV = process.env.NODE_ENV !== 'production';
  • 6、根据环境来切换配置
// app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule } from 'nestjs-config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
const envFilePath = ['.env']
export const IS_DEV = process.env.NODE_ENV !== 'production';
if (IS_DEV){
  envFilePath.unshift('.env.dev')
}else {
  envFilePath.unshift('.env.pord')
}
@Module({
  imports: [ConfigModule.forRoot(
    isGlobal: true,
    envFilePath:['.env'],
  )],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
  • 7、关于.env.env.dev.env.prod几个文件的说明
    • .env文件存放一些通用的配置,在开发环境和生产环境都保持一样的
    • .env.dev文件存放开发环境的配置
    • .env.pord文件存放生产部署需要的配置

四、使用yml文件来做配置文件

pnpm add yaml
  • 3、在根目录下创建application.dev.ymlapplication.prod.yml两个文件来区分开发环境和生产环境,并且添加值gitignore文件中
  • 4、创建一个文件用来读取配置utils/config.ts
import { parse } from 'yaml';
import * as path from 'path';
import * as fs from 'fs';

// 获取项目运行环境
export const getEnv = () => {
  return process.env.RUNNING_ENV;
};

export const IS_DEV = getEnv() === 'dev';
// 读取项目配置
export const getConfig = () => {
  const environment = getEnv();
  console.log(environment, '当前运行的环境');
  const yamlPath = path.join(process.cwd(), `./application.${environment}.yml`);
  const file = fs.readFileSync(yamlPath, 'utf8');
  const config = parse(file);
  return config;
};

这时候的application.dev.ymlapplication.prod.yml

PORT: 8080
PREFIX: api

基础配置

一、项目版本管理

  • 1、直接在main.ts中配置版本号,可能项目迭代会出现V2等版本
// 启动版本管理
app.enableVersioning({
  defaultVersion: [VERSION_NUM,'1'], // 默认版本为v1
  type: VersioningType.URI,
});
  • 2、如果整个控制器,全部接口都是一个版本的,可以直接在控制层设置
@Controller({path: 'home', version: '1'})
export class HomeController {
  ...
}
  • 3、这时候浏览器上访问的地址就是 http://localhost:8080/v1/home,域名/版本号[v1|v2]/项目名/控制器/方法

  • 4、单独某一个接口要设置,可以直接在这个接口方法单独加上,如果方法和类都加了,会以方法的优先级更高

@Controller({path: 'home', version: '1'})
export class AppController {
  constructor(private readonly appService: AppService) {}
 
  @Get()
  @Version('2')
  getHello(): string {
    return this.appService.getHello();
  }
}
  • 5、这时候浏览器上访问的地址就是 http://localhost:8080/v2/home

二、项目前戳配置

  • 1、在main.ts中配置
const PREFIX = process.env.PREFIX || 'api';
// 给请求添加prefix
app.setGlobalPrefix(PREFIX);
  • 2、这时候浏览器访问地址http://localhost:8080/api/...

三、跨域请求

const app = await NestFactory.create(AppModule);
// 一般添加到前面
app.enableCors();

四、配置模块前缀或模块项目名称

@Module({
  imports: [
    DashboardModule,
    RouterModule.register([
      {
        path: 'dashboard',
        module: DashboardModule,
      },
    ]),
  ],
})
export class AppModule {}
  • 3、这时候浏览器访问地址 http:localhost:8080/api/v1/dashboard/...

  • 4、注意:灵活使用module注册router

五、helmet 保护

安全相关的HTTP标头的较小中间件函数的集合

pnpm add helmet
  • 3、在main.ts中使用
import helmet from 'helmet';
// somewhere in your initialization file
app.use(helmet());

六、热部署

  • 1、为什么要热部署

在nestjs在开发环境中,要先将ts编译成js文件,然后来执行,如果项目大的时候就会出现编译时间过长,影响性能和效率,如果是小项目就无所谓配置

七、限流Rate Limiting

  • 1、为什么要限流?

为保护应用程序免受暴力攻击、直接限制访问的速率;

Guards 守卫、登录鉴权

一、关于守卫

守卫其实也是中间件的一种,只是在NestJs中对中间件更加具体的划分,将不同于职能的部分给了不同的称谓,一般守卫是用来做登录鉴权,权限类的,但是官网上写的比较复杂

二、uuid 生成 token 方法

jwt鉴权和token+redis的区别

jwt方案

1、jwt提供了2个方法,一个是将用户信息加密到一个字符串中

2、另一个方法是将字符串中的信息解密出用户信息

推荐方法:跨语言使用,v1 -> node , v2 -> go

token + redis 方案

1、使用一些算法生成一个token当做key存储,存储到redis中,值为当前用户用户信息(用户id,用户名等)

2、使用uuid简单生成token

3、根据前端传递token(uuid )从redis中读取数据,获取用户信息

这里使用uuid的方式生成一个token储存到redis中,只要是唯一的值就可以

  • 1、安装依赖包
pnpm add uuid
pnpm add @types/uuid -D
  • 2、创建一个工具包或者使用服务
nest g s plugin/tools
  • 3、直接定义生成一个tokenget属性
// tools.service.ts

import { Injectable } from '@nestjs/common';
import { v4 as uuidv4 } from 'uuid';

@Injectable()
export class ToolsService {
  // 去除uuid的中划线
  public get uuidToken(): string {
    return uuidv4().replace(/-/g, '');
  }
}

三、在Nestjs中使用redis来缓存数据

  • 1、安装依赖包
pnpm add ioredis
  • 2、在插件中创建一个redis的服务
nest g s plugin/redis
  • 3、在redis插件中写几个常用的方法,有需要可以自行在里面扩展
// redis.service.ts

import { Injectable } from '@nestjs/common';
import Redis, { ClientContext, Result } from 'ioredis';

import { ObjectType } from '@src/types';
import { isObject } from '@src/utils';

@Injectable()
export class RedisService {
  public redisClient!: Redis;

  // 模块加载的时候就缓存创建redis句柄
  onModuleInit() {
    if (!this.redisClient) {
      this.getClient();
    }
  }

  private getClient() {
    this.redisClient = new Redis({
      port: 6379, // Redis port
      host: 'localhost', // redisDb Redis host
      username: '', // needs Redis >= 6
      password: '', // 密码
      db: 0, // redis是几个数据库的,使用第一个
    });
  }


  public async set(key: string, value: unknown): Promise<Result<'OK', ClientContext>>;
  public async set(
    key: string,
    value: unknown,
    second: number
  ): Promise<Result<'OK', ClientContext>>;
  public async set(key: string, value: any, second?: number): Promise<Result<'OK', ClientContext>> {
    value = isObject(value) ? JSON.stringify(value) : value;
    if (!second) {
      return await this.redisClient.set(key, value);
    } else {
      return await this.redisClient.set(key, value, 'EX', second);
    }
  }


  public async incr(key: string): Promise<Result<number, ClientContext>> {
    return await this.redisClient.incr(key);
  }


  public async get(key: string): Promise<Result<string | null, ClientContext>> {
    try {
      const data = await this.redisClient.get(key);
      if (data) {
        return JSON.parse(data);
      } else {
        return null;
      }
    } catch (e) {
      return await this.redisClient.get(key);
    }
  }

  public async del(key: string): Promise<Result<number, ClientContext>> {
    return await this.redisClient.del(key);
  }

  async hset(key: string, field: ObjectType): Promise<Result<number, ClientContext>> {
    return await this.redisClient.hset(key, field);
  }


  async hget(key: string, field: string): Promise<Result<string | null, ClientContext>> {
    return await this.redisClient.hget(key, field);
  }


  async hgetall(key: string): Promise<Result<Record<string, string>, ClientContext>> {
    return await this.redisClient.hgetall(key);
  }

  public async flushall(): Promise<Result<'OK', ClientContext>> {
    return await this.redisClient.flushall();
  }
}

  • 4、测试在app服务层使用,储存一个数据导redis中然后取出来
// app.service.ts
@Injectable()
export class AppService {
  constructor(private readonly redisService: RedisService) {}
  async getHello(): Promise<string> {
    const result = await this.redisService.set(name, 'name');
    console.log('存储成功');
    const result1 = await this.redisService.get('name');
    console.log('获取成功', result1);
    return 'Hello World!';
  }
}
  • 5、本地查看redis数据库

![image-20221005232524290](/Users/xuesheng/Library/Application Support/typora-user-images/image-20221005232524290.png)

四、守卫实现token拦截,获取用户信息

# 使用命令创建一个守卫
nest g gu guard/auth
  • 2、其实简单理解为中间件,只是在nest中给这类的中间件赋予了特殊的称呼,如果守卫中返回true表示放行,如果返回false表示就要拦截,我们要做的就是根据请求头上token去redis中查询到数据,如果有就返回 true,如果没有就返回false

  • 3、一般需要配置,登录白名单等,建议设置在yaml中

  • 4、从get url中获取token,并创建utils/url.ts

pnpm add url
import { URL } from 'url';

// 根据key从一段url中获取query值
export const getUrlQuery = (urlPath: string, key: string): string | null => {
  const url = new URL(urlPath, 'https://www.');
  const params = new URLSearchParams(url.search.substring(1));
  return params.get(key);
};
  • 5、在守卫中获取用户传递的token
// auth.guard.ts
import {
  CanActivate,
  ExecutionContext,
  HttpException,
  HttpStatus,
  Injectable,
} from '@nestjs/common';
import { RedisService } from '@src/plugin/redis/redis.service';
import { getUrlQuery } from '@src/utils';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private readonly redisService: RedisService) {}
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token =
    context.switchToRpc().getData().headers.token ||
    context.switchToHttp().getRequest().body.token ||
    getUrlQuery(request.url, 'token');
    console.log(token, '当前token----');
    const currentUrl = request.route.path;
		// 登录白名单 login
    if(currentUrl.include('/login')){
      return true
    }
    request.user = null
    if(token){
        // 如果传递了token的话就要从redis中查询是否有该token
        const result = await this.redisService.get(token);
        if (result) {
          // 这里我们知道result数据类型就是我们定义的直接断言
          request.user = result as unkonwn;
          return true;
        }else{
          throw new HttpException('你传递token错误', HttpStatus.FORBIDDEN);
        }
    }
    throw new HttpException('请传递token', HttpStatus.FORBIDDEN);
  }
}

上面的守卫,实现所token的判断

不需要被守卫的接口请求

没有携带token的时候,直接返回false

携带了token但是携带的是过时的或者不存在,返回false

携带了正确的token,返回true

  • 6、分别可以从url headersbody 中使用token请求守卫

  • 7、在接口上使用守卫

import { AuthGuard } from '@src/guard/auth.guard';

@UseGuards(AuthGuard) // 可直接在类上使用,下面的全部方法将都受到守卫的保护
@Controller('account')
export class AccountController {
  
  @Get()
  @UseGuards(AuthGuard) // 可单独设置一个接口来使用
  async findAccount(@Body() findAccountDto: FindAccountDto): Promise<string> {
    ...
  }
  ...
}
  • 8、全局使用 app.module.ts
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from '@src/guard/auth.guard';

... 
providers: [
  {
    provide: APP_GUARD, 
    useClass: AuthGuard,
  },
];
...
// 注意:APP_GUARD 这个由@nestjs/core指定 ,其同类型的还有APP_FILTER, APP_INTERCEPTOR, APP_PIPE , APP_GUARD

当前用户装饰器

创建自定义的装饰器来实现获取当前用户信息

nest g d decorator/current-user
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const CurrentUser = createParamDecorator(
  (data: string, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    if(data && request.user ){
    	return request.user[data];
    }else{
      return request.user;
    }
  },
);
  • 3、在守卫代码中,将用户信息挂载到request上,这个地方定义的装饰器的匹配值要一致是user
// auth.guard.ts 
... 
if(token){
        // 如果传递了token的话就要从redis中查询是否有该token
        const result = await this.redisService.get(token);
        if (result) {
          // 这里我们知道result数据类型就是我们定义的直接断言
          request.user = result;
          return true;
        }else{
          throw new HttpException('你传递token错误', HttpStatus.FORBIDDEN);
        }
}
  • 4、在controller中使用
@Get()
async getHello(@CurrentUser() user): Promise<string> {
   console.log('user', user); // 这里打印出来的就是用户信息
   return await this.appService.getHello();
}
  • 5、获取user中的userId
@Get()
async getHello(@CurrentUser('userId') userId): Promise<string> {
   console.log('userId', userId); // 这里打印出来的就是用户userId
   return await this.appService.getHello();
}

配置typeorm操作mysql

一、在Nestjs中配置typeorm链接mysql

pnpm install --save typeorm mysql2
  • 3、创建database.providers.ts
import { DataSource } from 'typeorm';

export const databaseProviders = [
  {
    provide: 'DATA_SOURCE',
    useFactory: async () => {
      const dataSource = new DataSource({
        type: 'mysql',
        host: 'localhost',
        port: 3306,
        username: 'root',
        password: 'root',
        database: 'test',
        entities: [
            __dirname + '/../**/*.entity{.ts,.js}',
        ],
        synchronize: true,
      });

      return dataSource.initialize();
    },
  },
];
  • 4、在application.dev.yarm文件中添加关于mysql的配置信息
# 数据库配置
datasource:
  driverName: mysql
  host: localhost
  port: 3306
  database: nestjs-mysql-api
  username: root
  password: 123456
  charset: utf8mb4
  loc: Asia/Shanghai
  logging: true
  • 5、修改database.providers.ts

// mysql的连接
{
      provide: 'DATA_SOURCE',
      useFactory: (configService: ConfigService) => ({
        type: 'mysql',
        host: String(configService.get('datasource.host')),
        port: Number.parseInt(configService.get('datasource.port') ?? '3306'),
        username: String(configService.get('datasource.username')),
        password: String(configService.get('datasource.password')),
        database: String(configService.get('datasource.database')),
        entities: [__dirname + '/**/*.entity{.ts,.js}'],
        logging: configService.get('datasource.logging'),
        timezone: '+08:00', // 东八区
        cache: {
          duration: 60000, // 1分钟的缓存
        },
        extra: {
          poolMax: 32,
          poolMin: 16,
          queueTimeout: 60000,
          pollPingInterval: 60, // 每隔60秒连接
          pollTimeout: 60, // 连接有效60秒
        },
      }),
}

database.module.ts

import { Module } from '@nestjs/common';
import { databaseProviders } from './database.providers';

@Module({
  providers: [...databaseProviders],
  exports: [...databaseProviders],
})
export class DatabaseModule {}

二、关于数据库的创建

  • 1、在typeorm中已经帮我们实现了是否将实体类同步到数据库表,注意:数据库还是要手动创建的,只是typeorm会根据项目中的实体类同步的在数据库中生成对应的数据表,并不是数据库也可以自动创建。
  • 2、开启自动同步数据表功能
// 自动同步开启
synchronize: true,

// 开启控制器的日志输出
logging:true

注意:synchronize 如果是开发环境可以设置为true,如果是生产环境建议设置成false

配置Prisma操作数据库

Prisma 是一个面向 Node.js 和 TypeScript 的开源 ORM。它可以作为编写普通 SQL 或使用其他数据库访问工具(如 SQL 查询构建器(如 knex.js)或 ORM (如 TypeORM 和 Sequelize))的替代方法。Prisma 目前支持 PostgreSQL、 MySQL、 SQLServer、 SQLite、 MongoDB 和 CockroachDB (预览)。

一、安装配置

pnpm install prisma --save-dev
  • 3、本地调用 CLI ,使用 Prisma CLI 的 init 命令创建初始 Prisma 设置:
npx prisma init

这个命令创建一个新的 Prisma 目录,其内容如下:

  • schema.prisma: 指定数据库连接并包含数据库模式
  • .env: dotenv 文件,通常用于在一组环境变量中存储数据库凭据,schema读取DATABASE_URL地址
datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}
  • 4、使用 Prisma Client
 pnpm install @prisma/client
  • 5、在 src 目录中,创建一个名为 prisma.service.ts 的新文件,并向其添加以下代码:
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  async onModuleInit() {
    // 连接数据库成功
    await this.$connect();
  }

  async enableShutdownHooks(app: INestApplication) {
    this.$on('beforeExit', async () => {
      // 关闭连接
      await app.close();
    });
  }
}
  • 6、在service中使用
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { User, Prisma } from '@prisma/client';

@Injectable()
export class UserService {
  constructor(private prisma: PrismaService) {}

  async user(
    userWhereUniqueInput: Prisma.UserWhereUniqueInput,
  ): Promise<User | null> {
    return this.prisma.user.findUnique({
      where: userWhereUniqueInput,
    });
  }

  async users(params: {
    skip?: number;
    take?: number;
    cursor?: Prisma.UserWhereUniqueInput;
    where?: Prisma.UserWhereInput;
    orderBy?: Prisma.UserOrderByWithRelationInput;
  }): Promise<User[]> {
    const { skip, take, cursor, where, orderBy } = params;
    return this.prisma.user.findMany({
      skip,
      take,
      cursor,
      where,
      orderBy,
    });
  }

  async createUser(data: Prisma.UserCreateInput): Promise<User> {
    return this.prisma.user.create({
      data,
    });
  }

  async updateUser(params: {
    where: Prisma.UserWhereUniqueInput;
    data: Prisma.UserUpdateInput;
  }): Promise<User> {
    const { where, data } = params;
    return this.prisma.user.update({
      data,
      where,
    });
  }

  async deleteUser(where: Prisma.UserWhereUniqueInput): Promise<User> {
    return this.prisma.user.delete({
      where,
    });
  }
}

表单验证 Dto

表单验证使用管道操作,所以你需要pipe管道有所了解。

一、安装配置

pnpm add class-validator class-transformer
pnpm add -D @nestjs/mapped-types
  • 3、然后在main.ts中注册
async function bootstrap() {
  ...
  app.useGlobalPipes(new ValidationPipe());
	...
}
bootstrap();

二、验证实现

  • 1、首先创建一个dto 文件auth/dto/create-user.dto.ts,对请求的数据进行验证规则声明

$property 指当前表单字段

import { IsNotEmpty } from 'class-validator';
export class CreateUserDto {
  @IsNotEmpty({message: '$property:用户名不能为空'})
  name: string;
  
  //存在price时才验证
  @ValidateIf((o) => o.price)
  //将类型转换为数值
  @Type(() => Number)
  price:number
}
  • 2、然后创建 validate.pipe.ts 验证管道
import {
  ArgumentMetadata,
  BadRequestException,
  Injectable,
  PipeTransform,
} from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';

@Injectable()
export class ValidatePipe implements PipeTransform {
  async transform(value: any, metadata: ArgumentMetadata) {
    const { metatype } = metadata;
    //前台提交的表单数据没有类型,使用 plainToClass 转为有类型的对象用于验证
    const object = plainToInstance(metatype, value);

    //根据 DTO 中的装饰器进行验证
    const errors = await validate(object);
    if (errors.length) {
      throw new BadRequestException('表单数据错误');
    }
    return value;
  }
}
  • 3、然后在控制器方法中使用验证管道进行验证
@Post('add')
add(@Body(ValidatePipe) dto: UserDto): any {
  return dto;
}

三、内置验证

  • 1、NestJs 提供了开箱即用的验证,不需要我们自己来实现验证,我们现在来体验

  • 2、首先在 main.ts 全局注册验证管道

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();
  • 3、创建user资源用于进行验证实验
nest g res user
  • 4、创建dto文件 auth/dto/create-user.dto.ts ,用于定义验证规则
import { IsNotEmpty } from 'class-validator';
export class CreateUserDto {
  @IsNotEmpty({message: '用户名不能为空'})
  name: string;
}
  • 5、在需要验证的控制器方法中使用 DTO
import { Body, Controller, Get, Post } from '@nestjs/common';
import { RegisterDto } from './dto/register.dto';

@Controller('user')
export class AuthController {
  @Post()
  register(@Body() dto: CreateUserDto) {
    return dto;
  }
}
  • 6、在api测试工具中对user进行测试
{
    "statusCode": 400,
    "message": [
        "用户名不能为空"
    ],
    "error": "Bad Request"
}

其他配置

一、自动装换

  • 1、ValidationPipe 可以根据对象的 DTO 类自动将有效数据转换为对象类型。

如果不使用自动转换时,下面的id为string

@Get(':id')
index(@Param('id') id: number) {
  console.log(typeof id);
}
  • 2、在main.ts中设置全局自动转换后,上面的**id****类型自动转换为 number
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
  new ValidationPipe({
    transform: true,
    whitelist: true,
  }),
);

二、白名单

  • 1、想过滤掉在 Dto 中没有声明的字段,可以在 main.ts 文件中对 ValidationPipe 管道进行配置。
async function bootstrap() {
 	...
  app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
	...
}

登录注册

  • 1、安装依赖包
pnpm add argon2
  • 2、然后创建资源
nest g res auth

用户注册

  • 1、修改create-auth.dto.ts对字段进行验证
import { IsEmail, IsNotEmpty } from 'class-validator';

export class CreateAuthDto {
  @IsNotEmpty({ message: '用户名不能为空' })
  @IsEmail({}, { message: '用户名必须是邮箱' })
  email: string;
  @IsNotEmpty({ message: '密码不能为空' })
  password: string;
}
  • 2、然后修改 auth.service.ts 完善业务
import { PrismaClient } from '@prisma/client';
import { Injectable } from '@nestjs/common';
import { CreateAuthDto } from './dto/create-auth.dto';
import { UpdateAuthDto } from './dto/update-auth.dto';
import * as argon2 from 'argon2';

@Injectable()
export class AuthService extends PrismaClient {
  async register(createAuthDto: CreateAuthDto) {
  	//加密用户密码
    const password = await argon2.hash(createAuthDto.password);
    const user = await this.users.create({
      data: {
        email: createAuthDto.email,
        password,
      },
    });
    
    //返回的数据不显示密码
    delete user.password;
    return user;
  }
  ...
}
  • 3、然后控制器auth.controller.ts中使用
@Controller()
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('register')
  register(@Body() createAuthDto: CreateAuthDto) {
    return this.authService.register(createAuthDto);
  }
  ...
}

用户登录

  • 1、创建 login-dto.ts 内容如下。
  • 2、其中 IsExist 自定义的验证,请查看管道章节,你也可以把这行删除
import { IsEmail, IsNotEmpty } from 'class-validator';
import { IsExist } from 'src/rules/is-exist.rule';

export class LoginDto {
  @IsNotEmpty({ message: '用户名不能为空' })
  @IsEmail({}, { message: '用户名必须是邮箱' })
  @IsExist({ field: 'email', table: 'users' }, { message: '用户不存在' })
  email: string;
  @IsNotEmpty({ message: '密码不能为空' })
  password: string;
}
  • 3、修改 auth.service.ts,添加login方法
import { PrismaClient } from '@prisma/client';
import { Injectable, BadRequestException } from '@nestjs/common';
import { CreateAuthDto } from './dto/create-auth.dto';
import * as argon2 from 'argon2';

@Injectable()
export class AuthService extends PrismaClient {
  ...
  async login({ email, password }: CreateAuthDto) {
    const user = await this.users.findUnique({
      where: { email },
    });
    if (!user) throw new BadRequestException('用户不存在');

    const psMatch = await argon2.verify(user.password, password);

    if (!psMatch) throw new BadRequestException('密码输入错误');

    delete user.password;
    return user;
  }
}

控制器代码

@Controller()
export class AuthController {
  constructor(private readonly authService: AuthService) {}
  ...
  @Post('login')
  login(@Body() data: LoginDto) {
    return this.authService.login(data);
  }
}
2

评论区