地毯式学习nestjs(基础篇)

945 阅读18分钟

Nest (NestJS) 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的开发框架。它利用 JavaScript 的渐进增强的能力,使用并完全支持 TypeScript (仍然允许开发者使用纯 JavaScript 进行开发),并结合了 OOP (面向对象编程)、FP (函数式编程)和 FRP (函数响应式编程)。

  • 在底层,Nest 构建在强大的 HTTP 服务器框架上,例如 Express (默认),并且还可以通过配置从而使用 Fastify !
  • Nest 在这些常见的 Node.js 框架 (Express/Fastify) 之上提高了一个抽象级别,但仍然向开发者直接暴露了底层框架的 API。这使得开发者可以自由地使用适用于底层平台的无数的第三方模块。

本文基于nest8演示

创建项目

$ npm i -g @nestjs/cli 

nest new project-name 创建一个项目

$ tree . 
├── README.md 
├── nest-cli.json 
├── package.json 
├── src 
│   ├── app.controller.spec.ts 
│   ├── app.controller.ts 
│   ├── app.module.ts 
│   ├── app.service.ts 
│   └── main.ts 
├── test 
│   ├── app.e2e-spec.ts 
│   └── jest-e2e.json 
├── tsconfig.build.json 
└── tsconfig.json  2 directories, 12 files

以下是这些核心文件的简要概述

  • app.controller.ts 带有单个路由的基本控制器示例。
  • app.module.ts 应用程序的根模块。
  • main.ts 应用程序入口文件。它使用 NestFactory 用来创建 Nest 应用实例。

main.ts 包含一个异步函数,它负责引导我们的应用程序:

import { NestFactory } from '@nestjs/core'; 
import { ApplicationModule } from './app.module';          
async function bootstrap() {   
    const app = await NestFactory.create(ApplicationModule);   
    await app.listen(3000); 
} 
bootstrap(); 
  • NestFactory 暴露了一些静态方法用于创建应用实例
  • create() 方法返回一个实现 INestApplication 接口的对象, 并提供一组可用的方法

nest有两个支持开箱即用的 HTTP 平台:express 和 fastify。 您可以选择最适合您需求的产品

  • platform-express Express 是一个众所周知的 node.js 简约 Web 框架。 这是一个经过实战考验,适用于生产的库,拥有大量社区资源。 默认情况下使用 @nestjs/platform-express 包。 许多用户都可以使用 Express ,并且无需采取任何操作即可启用它。
  • platform-fastify Fastify 是一个高性能,低开销的框架,专注于提供最高的效率和速度。

Nest控制器

Nest中的控制器层负责处理传入的请求, 并返回对客户端的响应。

控制器的目的是接收应用的特定请求。路由机制控制哪个控制器接收哪些请求。通常,每个控制器有多个路由,不同的路由可以执行不同的操作

通过NestCLi创建控制器:

nest -h 可以看到nest支持的命令

常用命令:

  • 创建控制器:nest g co user module
  • 创建服务:nest g s user module
  • 创建模块:nest g mo user module
  • 默认以src为根路径生成

nest g controller posts 

表示创建posts的控制器,这个时候会在src目录下面生成一个posts的文件夹,这个里面就是posts的控制器,代码如下

import { Controller } from '@nestjs/common';  
@Controller('posts') 
export class PostsController { } 

创建好控制器后,nestjs会自动的在 app.module.ts 中引入PostsController,代码如下

// src/app.module.ts 
import { Module } from '@nestjs/common'; 
import { AppController } from './app.controller'; 
import { AppService } from './app.service'; 
import { PostsController } from './posts/posts.controller'      
@Module({     
    imports: [],     
    controllers: [AppController, PostsController],     
    providers: [AppService], 
}) 
export class AppModule {}

nest配置路由请求数据

Nestjs提供了其他HTTP请求方法的装饰器
@Get() @Post() @Put() 、 @Delete()、 @Patch()、 @Options()、 @Head()和 @All()

在Nestjs中获取Get传值或者Post提交的数据的话我们可以使用Nestjs中的装饰器来获取。

@Request()  req 
@Response() 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]

示例

@Controller('posts')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  @Post('create')
  create(@Body() createPostDto: CreatePostDto) {
    return this.postsService.create(createPostDto);
  }

  @Get('list')
  findAll(@Query() query) {
    return this.postsService.findAll(query);
  }

  @Get(':id')
  findById(@Param('id') id: string) {
    return this.postsService.findById(id);
  }

  @Put(':id')
  update(
    @Param('id') id: string,
    @Body() updatePostDto: UpdatePostDto,
  ) {
    return this.postsService.update(id, updatePostDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.postsService.remove(id);
  }
}

注意

  • 关于nest的return: 当请求处理程序返回 JavaScript 对象或数组时,它将自动序列化为 JSON。但是,当它返回一个字符串时,Nest 将只发送一个字符串而不是序列化它

Nest服务

Nestjs中的服务可以是service 也可以是provider。他们都可以通过 constructor 注入依赖关系。服务本质上就是通过@Injectable() 装饰器注解的类。在Nestjs中服务相当于MVCModel

创建服务

nest g service posts

创建好服务后就可以在服务中定义对应的方法

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Not, Between, Equal, Like, In } from 'typeorm';
import * as dayjs from 'dayjs';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { PostsEntity } from './entities/post.entity';
import { PostsRo } from './interfaces/posts.interface';

@Injectable()
export class PostsService {
  constructor(
    @InjectRepository(PostsEntity)
    private readonly postsRepository: Repository<PostsEntity>,
  ) {}

  async create(post: CreatePostDto) {
    const { title } = post;
    const doc = await this.postsRepository.findOne({ where: { title } });
    console.log('doc', doc);
    if (doc) {
      throw new HttpException('文章标题已存在', HttpStatus.BAD_REQUEST);
    }
    return {
      data: await this.postsRepository.save(post),
      message: '创建成功',
    };
  }

  // 分页查询列表
  async findAll(query = {} as any) {
    let { pageSize, pageNum, orderBy, sort, ...params } = query;
    orderBy = query.orderBy || 'create_time';
    sort = query.sort || 'DESC';
    pageSize = Number(query.pageSize || 10);
    pageNum = Number(query.pageNum || 1);
    console.log('query', query);
    
    const queryParams = {} as any;
    Object.keys(params).forEach((key) => {
      if (params[key]) {
        queryParams[key] = Like(`%${params[key]}%`); // 所有字段支持模糊查询、%%之间不能有空格
      }
    });
    const qb = await this.postsRepository.createQueryBuilder('post');

    // qb.where({ status: In([2, 3]) });
    qb.where(queryParams);
    // qb.select(['post.title', 'post.content']); // 查询部分字段返回
    qb.orderBy(`post.${orderBy}`, sort);
    qb.skip(pageSize * (pageNum - 1));
    qb.take(pageSize);

    return {
      list: await qb.getMany(),
      totalNum: await qb.getCount(), // 按条件查询的数量
      total: await this.postsRepository.count(), // 总的数量
      pageSize,
      pageNum,
    };
  }

  // 根据ID查询详情
  async findById(id: string): Promise<PostsEntity> {
    return await this.postsRepository.findOne({ where: { id } });
  }

  // 更新
  async update(id: string, updatePostDto: UpdatePostDto) {
    const existRecord = await this.postsRepository.findOne({ where: { id } });
    if (!existRecord) {
      throw new HttpException(`id为${id}的文章不存在`, HttpStatus.BAD_REQUEST);
    }
    // updatePostDto覆盖existRecord 合并,可以更新单个字段
    const updatePost = this.postsRepository.merge(existRecord, {
      ...updatePostDto,
      update_time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
    });
    return {
      data: await this.postsRepository.save(updatePost),
      message: '更新成功',
    };
  }

  // 删除
  async remove(id: string) {
    const existPost = await this.postsRepository.findOne({ where: { id } });
    if (!existPost) {
      throw new HttpException(`文章ID ${id} 不存在`, HttpStatus.BAD_REQUEST);
    }
    await this.postsRepository.remove(existPost);
    return {
      data: { id },
      message: '删除成功',
    };
  }
}

Nest模块

模块是具有 @Module() 装饰器的类。 @Module() 装饰器提供了元数据,Nest 用它来组织应用程序结构

每个 Nest 应用程序至少有一个模块,即根模块。根模块是 Nest 开始安排应用程序树的地方。事实上,根模块可能是应用程序中唯一的模块,特别是当应用程序很小时,但是对于大型程序来说这是没有意义的。在大多数情况下,您将拥有多个模块,每个模块都有一组紧密相关的功能。 @module() 装饰器接受一个描述模块属性的对象:

  • providers 由 Nest 注入器实例化的提供者,并且可以至少在整个模块中共享
  • controllers 必须创建的一组控制器
  • imports 导入模块的列表,这些模块导出了此模块中所需提供者
  • exports 由本模块提供并应在其他模块中可用的提供者的子集
// 创建模块 
posts nest g module posts

Nestjs中的共享模块

每个模块都是一个共享模块。一旦创建就能被任意模块重复使用。假设我们将在几个模块之间共享 PostsService 实例。 我们需要把 PostsService 放到 exports 数组中:

// posts.modules.ts 
import { Module } from '@nestjs/common'; 
import { PostsController } from './posts.controller'; 
import { PostsService } from './posts.service'; 
@Module({   
    controllers: [PostsController],   
    providers: [PostsService],   
    exports: [PostsService] 
    // 共享模块导出
}) 
export class PostsModule {}

可以使用 nest g res posts 一键创建以上需要的各个模块

配置静态资源

NestJS中配置静态资源目录完整代码

npm i @nestjs/platform-express -S
import { NestExpressApplication } from '@nestjs/platform-express'; 
// main.ts 
async function bootstrap() {   
    // 创建实例   
    const app = await NestFactory.create<NestExpressApplication>(AppModule);       
    //使用方式一   
    app.useStaticAssets('public')  
    //配置静态资源目录      
    // 使用方式二:配置前缀目录 设置静态资源目录   
    app.useStaticAssets(join(__dirname, '../public'), {     
        // 配置虚拟目录,比如我们想通过 http://localhost:3000/static/1.jpg 来访问public目录里面的文件     
        prefix: '/static/', // 设置虚拟路径   
    });   
    // 启动端口   
    const PORT = process.env.PORT || 9000;   
    await app.listen(PORT, () => Logger.log(`服务已经启动 http://localhost:${PORT}`)); 
} 
bootstrap();

配置模板引擎

npm i ejs --save

配置模板引擎

// main.ts 
import { NestFactory } from '@nestjs/core'; 
import { AppModule } from './app.module'; 
import {join} from 'path';  
async function bootstrap() {   
    const app = await NestFactory.create(AppModule);    
    app.setBaseViewsDir(join(__dirname, '..', 'views')) // 放视图的文件   
    app.setViewEngine('ejs'); //模板渲染引擎    
    await app.listen(9000); 
} 
bootstrap();

项目根目录新建views目录然后新建根目录 -> views -> default -> index.ejs

<!DOCTYPE html> 
<html lang="en"> 
<head>     
    <meta charset="UTF-8">     
    <meta name="viewport" content="width=device-width, initial-scale=1.0">     
    <meta http-equiv="X-UA-Compatible" content="ie=edge">     
    <title>Document</title> 
</head> 
<body>    
    <h3>模板引擎</h3>     
    <%=message%> 
</body> 
</html>

渲染页面

Nestjs中 Render装饰器可以渲染模板,使用路由匹配渲染引擎

import { Controller, Get, Render } from '@nestjs/common'; 
import { AppService } from './app.service';  
@Controller() 
export class AppController {   
    @Get()   
    @Render('default/index')  
    //使用render渲染模板引擎,参数就是文件路径:default文件夹下的index.ejs   
    getUser(): any {     
        return { message: "hello word" } //只有返回参数在模板才能获取,如果不传递参数,必须返回一个空对象   
    } 
} 

Cookie的使用

cookie和session的使用依赖于当前使用的平台,如:express和fastify
两种的使用方式不同,这里主要记录基于express平台的用法

cookie可以用来存储用户信息,存储购物车等信息,在实际项目中用的非常多

npm instlal cookie-parser --save 
npm i -D @types/cookie-parser --save

引入注册

// main.ts  
import { AppModule } from './app.module'; 
import { NestExpressApplication } from '@nestjs/platform-express'; 
import * as cookieParser from 'cookie-parser'  
async function bootstrap() {   
    const app = await NestFactory.create<NestExpressApplication>(AppModule);      //注册cookie   
    app.use(cookieParser('dafgafa'));  //加密密码      
    await app.listen(3000); 
} 
bootstrap();

接口中设置cookie 使用response

请求该接口,响应一个cookie

@Get()
index(@Response() res){
    //设置cookie, signed:true加密
    //参数:1:key, 2:value, 3:配置
    res.cookie('username', 'poetry', {maxAge: 1000 * 60 * 10, httpOnly: true, signed:true})
    
    //注意:
    //使用res后,返回数据必须使用res
    //如果是用了render模板渲染,还是使用return
    res.send({xxx})
}

cookie相关配置参数

  • domain String 指定域名下有效
  • expires Date 过期时间(秒),设置在某个时间点后会在该cookoe后失效
  • httpOnly Boolean 默认为false 如果为true表示不允许客户端(通过js来获取cookie)
  • maxAge String 最大失效时间(毫秒),设置在多少时间后失效
  • path String 表示cookie影响到的路径,如:path=/如果路径不能匹配的时候,浏览器则不发送这个cookie
  • secure Boolean 当 secure 值为 true 时,cookie 在 HTTP 中是无效,在 HTTPS 中才有效
  • signed Boolean 表示是否签名cookie,如果设置为true的时候表示对这个cookie签名了,这样就需要用
  • res.signedCookies()获取值cookie不是使用res.cookies()获取cookie
@Get() 
index(@Request() req){       
    console.log(req.cookies.username) //加密的cookie获取方式       
    console.log(req.signedCookies.username)         
    return req.cookies.username 
}

Cookie加密

// 配置中间件的时候需要传参
app.use(cookieParser('123456'));

// 设置cookie的时候配置signed属性
res.cookie('userinfo','hahaha',{domain:'.ccc.com',maxAge:900000,httpOnly:true,signed:true});

// signedCookies调用设置的cookie
console.log(req.signedCookies);

Session的使用

  • session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而session保存在服务器上
  • 当浏览器访问服务器并发送第一次请求时,服务器端会创建一个session对象,生成一个类似于key,value的键值对, 然后将key(cookie)返回到浏览器(客户)端,浏览器下次再访问时,携带key(cookie),找到对应的session(value)。 客户的信息都保存在session中

安装 express-session

npm i express-session --save
npm i -D @types/express-session --save
// main.ts

import { AppModule } from './app.module';
import { NestExpressApplication } from '@nestjs/platform-express';
import * as session from 'express-seesion'

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  
  //配置session
  app.use(session({
      secret: 'dmyxs',
      cookie: { maxAge: 10000, httpOnly: true },  //以cookie存储到客户端
      rolling: true //每次重新请求时,重新设置cookie
  }))
  
  await app.listen(3000);
}
bootstrap();

session相关配置参数

  • secret String 生成session签名的密钥
  • name String 客户端的cookie的名称,默认为connect.sid, 可自己设置
  • resave Boolean 强制保存 session 即使它并没有变化, 默认为true, 建议设置成false
  • saveUninitalized Boolean 强制将未初始化的 session 存储。当新建了一个 session 且未设定属性或值时,它就处于 未初始化状态。在设定一个 cookie 前,这对于登陆验证,减轻服务端存储压力,权限控制是有帮助的。默认:true, 建议手动添加
  • cookie Object 设置返回到前端cookie属性,默认值为{ path: ‘/’, httpOnly: true, secure: false, maxAge: null }
  • rolling Boolean 在每次请求时强行设置 cookie,这将重置 cookie 过期时间, 默认为false

接口中设置session

@Get()
index(@Request() req){
    //设置session
    req.session.username = 'poetry'
}

获取session

@Get('/session')
session(@Request() req, @Session() session ){
    //获取session:两种方式
    console.log(req.session.username)
    console.log(session.username)
    
    return 'hello session'
}

跨域,前缀路径、网站安全、请求限速

跨域,路径前缀,网络安全

yarn add helmet csurf
// main.ts
import { NestFactory } from '@nestjs/core';
import { Logger, ValidationPipe } from '@nestjs/common';

import * as helmet from 'helmet';
import * as csurf from 'csurf';

import { AppModule } from './app.module';

const PORT = process.env.PORT || 8000;

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 路径前缀:如:http://www.test.com/api/v1/user
  app.setGlobalPrefix('api/v1');

  //cors:跨域资源共享,方式一:允许跨站访问
  app.enableCors();
  // 方式二:const app = await NestFactory.create(AppModule, { cors: true });

  //防止跨站脚本攻击
  app.use(helmet());

  //CSRF保护:跨站点请求伪造
  app.use(csurf());
  
  await app.listen(PORT, () => {
    Logger.log(
      `服务已经启动,接口请访问:localhost:${PORT}${PREFIX}`,
    )
  });
}
bootstrap();

限速:限制客户端在一定时间内的请求次数

yarn add @nestjs/throttler

在需要使用的模块引入使用,这里是全局使用,在app.module.ts中引入。这里设置的是:1分钟内只能请求10次,超过则报status为429的错误

app.module.ts

import { APP_GUARD } from '@nestjs/core';
import { Module } from '@nestjs/common';
import { UserModule } from './modules/user/user.module';

//引入
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';

@Module({
  imports: [
  	UserModule,
    ThrottlerModule.forRoot({
      ttl: 60,  //1分钟
      limit: 10, //请求10次
    }),
  ],
  providers: [ //全局使用
    {
      provide: APP_GUARD,
      useClass: ThrottlerGuard,
    },
  ],
})
export class AppModule { }

管道、守卫、拦截器、过滤器、中间件

  • 管道:数据处理与转换,数据验证
  • 守卫:验证用户登陆,保护路由
  • 拦截器:对请求响应进行拦截,统一响应内容
  • 过滤器:异常捕获
  • 中间件:日志打印

执行顺序(时机)

从客户端发送一个post请求,路径为:/user/login,请求参数为:{userinfo: ‘xx’,password: ‘xx’},到服务器接收请求内容,触发绑定的函数并且执行相关逻辑完毕,然后返回内容给客户端的整个过程大体上要经过如下几个步骤:

全局使用: 管道 - 守卫 - 拦截器 - 过滤器 - 中间件。统一在main.ts文件中使用,全局生效

import { NestFactory } from '@nestjs/core';
import { ParseIntPipe } from '@nestjs/common';
import { AppModule } from './app.module';

import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { AuthGuard } from './common/guard/auth.guard';
import { AuthInterceptor } from './common/interceptors/auth.interceptor';


async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  //全局使用管道:这里使用的是内置,也可以使用自定义管道,在下文
  app.useGlobalPipes(new ParseIntPipe());

  //全局使用中间件
  app.use(LoggerMiddleware)
  
  //全局使用过滤器
  //这里使用的是自定义过滤器,先别管,先学会怎么在全局使用
  app.useGlobalFilters(new HttpExceptionFilter());  

  //全局使用守卫
  app.useGlobalGuards(new AuthGuard());
  
  //全局使用拦截器
  app.useGlobalInterceptors(new AuthInterceptor());
  
  await app.listen(3000);
}
bootstrap();

管道

常用内置管道,从@nestjs/common导出

  • ParseIntPipe:将字符串数字转数字
  • ValidationPipe:验证管道

局部使用管道

  • 匹配整个路径,使用UsePipes
  • 只匹配某个接口,使用UsePipes
  • 在获取参数时匹配,一般使用内置管道
import {
  Controller,
  Get,
  Put,
  Body,
  Param,
  UsePipes,
  ParseIntPipe
} from '@nestjs/common';
import { myPipe } from '../../common/pipes/user.pipe';

@Controller('user')
@UsePipes(new myPipe())  //局部方式1:匹配整个/user, get请求和put请求都会命中
export class UserController {
  @Get(':id')
  getUserById(@Param('id', new ParseIntPipe()) id) { //局部方式3:只匹配/user的get请求,使用的是内置管道
    console.log('user', typeof id);
    return id;
  }

  @Put(':id')
  @UsePipes(new myPipe())  //局部方式2:只匹配/user的put请求
  updateUser(@Body() user, @Param('id') id) {
    return {
      user,
      id,
    };
  }
}

自定义管道

使用快捷命令生成:nest g pi myPipe common/pipes

import {
  ArgumentMetadata,
  Injectable,
  PipeTransform,
  BadRequestException,
} from '@nestjs/common';

//自定义管道必须实现自PipeTransform,固定写法,该接口有一个transform方法
//transform参数:
//value:使用myPipe时所传递的值,可以是param传递的的查询路径参数,可以是body的请求体
//metadata:元数据,可以用它判断是来自param或body或query
@Injectable()
export class myPipe implements PipeTransform<string> {
  transform(value: string, metadata: ArgumentMetadata) {
    if (metadata.type === 'body') {
      console.log('来自请求体', value);
    }
    if (metadata.type === 'param') {
      console.log('来自查询路径', value);

      const val = parseInt(value, 10);
      //如果不是传递一个数字,抛出错误
      if (isNaN(val)) {
        throw new BadRequestException('Validation failed');
      }
      return val;
    }
    return value;
  }
}

守卫

自定义守卫

使用快捷命令生成:nest g gu myGuard common/guards

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core'; //反射器,作用与自定义装饰器桥接,获取数据

//自定义守卫必须CanActivate,固定写法,该接口只有一个canActivate方法
//canActivate参数:
//context:请求的(Response/Request)的引用
//通过守卫返回true,否则返回false,返回403状态码
@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) { }

  // 白名单数组
  private whiteUrlList: string[] = ['/user'];

  // 验证该次请求是否为白名单内的路由
  private isWhiteUrl(urlList: string[], url: string): boolean {
    if (urlList.includes(url)) {
      return true;
    }
    return false;
  }

  canActivate(context: ExecutionContext): boolean {
    // 获取请求对象
    const request = context.switchToHttp().getRequest();
    //console.log('request', request.headers);
    //console.log('request', request.params);
    //console.log('request', request.query);
    //console.log('request', request.url);

    // 用法一:验证是否是白名单内的路由
    if (this.isWhiteUrl(this.whiteUrlList, request.url)) {
      return true;
    } else {
      return false;
    }

    // 用法二:使用反射器,配合装饰器使用,获取装饰器传递过来的数据
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    //console.log(roles); // [ 'admin' ]
    //http://localhost:3000/user/9?user=admin,如果与装饰器传递过来的值匹配则通过,否则不通过
    //真实开发中可能从cookie或token中获取值
    const { user } = request.query;
    if (roles.includes(user)) {
      return true;
    } else {
      return false;
    }

    // 其他用法
    // 获取请求头中的token字段
    const token = context.switchToRpc().getData().headers.token;
    // console.log('token', token);

    // 获取session
    const userinfo = context.switchToHttp().getRequest().session;
    // console.log('session', userinfo);

    return true;
  }
}

局部使用守卫

import {
  Controller,
  Get,
  Delete,
  Param,
  UsePipes,
  UseGuards,
  ParseIntPipe,
} from '@nestjs/common';
import { AuthGuard } from '../../common/guard/auth.guard';
import { Role } from '../../common/decorator/role.decorator'; //自定义装饰器

@UseGuards(AuthGuard) //局部使用守卫,守卫整个user路径
@Controller('user')
export class UserController {
  @Get(':id')
  getUserById(@Param('id', new ParseIntPipe()) id) {
    console.log('user', typeof id);
    return id;
  }

  @Delete(':id')
  @Role('admin')  //使用自定义装饰器,传入角色,必须是admin才能删除
  removeUser(@Param('id') id) {
    return id;
  }
}

装饰器

自定义守卫中使用到了自定义装饰器

nest g d role common/decorator
//这是快捷生成的代码

import { SetMetadata } from '@nestjs/common';

//SetMetadata作用:将获取到的值,设置到元数据中,然后守卫通过反射器才能获取到值
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

拦截器

使用快捷命令生成:nest g in auth common/intercepters

import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';

//自定义拦截器必须实现自NestInterceptor,固定写法,该接口只有一个intercept方法
//intercept参数:
//context:请求上下文,可以拿到的Response和Request
@Injectable()
export class AuthInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    console.log('拦截器', request.url);
    return next.handle().pipe(
      map((data) => {
        console.log('全局响应拦截器方法返回内容后...');
        return {
          status: 200,
          timestamp: new Date().toISOString(),
          path: request.url,
          message: '请求成功',
          data: data,
        };
      }),
    );
  }
}

过滤器

局部使用过滤器

import {
  Controller,
  Get,
  UseFilters,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { HttpExceptionFilter } from '../../common/filters/http-exception.filter';

//局部使用过滤器
@UseFilters(new HttpExceptionFilter())
@Controller('/user')
export class ExceptionController {
  @Get()
  getUserById(@Query() { id }): string {
    if (!id) {
      throw new HttpException(
        {
          status: HttpStatus.BAD_REQUEST,
          message: '请求参数id 必传',
          error: 'id is required',
        },
        HttpStatus.BAD_REQUEST,
      );
    }
    return 'hello error';
  }
}

自定义过滤器

使用快捷命令生成:nest g f myFilter common/filters

import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
} from '@nestjs/common';

//必须实现至ExceptionFilter,固定写法,该接口只有一个catch方法
//catch方法参数:
//exception:当前正在处理的异常对象
//host:传递给原始处理程序的参数的一个包装(Response/Request)的引用
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    const status = exception.getStatus(); //获取状态码
    const exceptionRes: any = exception.getResponse(); //获取响应对象
    const { error, message } = exceptionRes;

    //自定义的异常响应内容
    const msgLog = {
      status,
      timestamp: new Date().toISOString(),
      path: request.url,
      error,
      message,
    };

    response.status(status).json(msgLog);
  }
}

中间件

局部使用中间件

import { Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middlerware';
import { UserModule } from './modules/user/user.module';

@Module({
	imports:[ UserModule ]
})
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware) //应用中间件
      .exclude({ path: 'user', method: RequestMethod.POST })  //排除user的post方法
      .forRoutes('user'); //监听路径  参数:路径名或*,*是匹配所以的路由
      // .forRoutes({ path: 'user', method: RequestMethod.POST }, { path: 'album', method: RequestMethod.ALL }); //多个
     // .apply(UserMiddleware) //支持多个中间件
     // .forRoutes('user')
  }
}

自定义中间件

nest g mi logger common/middleware
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  //req:请求参数
  //res:响应参数
  //next:执行下一个中件间
  use(req: Request, res: Response, next: () => void) {
    const { method, path } = req;
    console.log(`${method} ${path}`);
    next();
  }
}

函数式中间件

// 函数式中间件-应用于全局
export function logger(req, res, next) {
  next();
}

// main.ts
async function bootstrap() {
  // 创建实例
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  
  // 设置全局日志函数中间件
  app.use(logger);
}
bootstrap();

一例看懂中间件、守卫、管道、异常过滤器、拦截器

从客户端发送一个post请求,路径为:/user/login,请求参数为:{userinfo: ‘xx’,password: ‘xx’},到服务器接收请求内容,触发绑定的函数并且执行相关逻辑完毕,然后返回内容给客户端的整个过程大体上要经过如下几个步骤:`

项目需要包支持:

npm install --save rxjs xml2js class-validator class-transformer 
  • rxjs 针对JavaScript的反应式扩展,支持更多的转换运算
  • xml2js 转换xml内容变成json格式
  • class-validatorclass-transformer 管道验证包和转换器

建立user模块:模块内容结构:

nest g res user

user.controller.ts文件

import {
  Controller,
  Post,
  Body
} from '@nestjs/common';
import { UserService } from './user.service';
import { UserLoginDTO } from './dto/user.login.dto';

@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Post('test')
  loginIn(@Body() userlogindto: UserLoginDTO) {
    return userlogindto;
  }

}

user.module.ts文件

import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';

@Module({
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

user.service.ts文件

import { Injectable } from '@nestjs/common';

@Injectable()
export class UserService {}

user.login.dto.ts文件

// user / dto / user.login.dto.ts

import { IsNotIn, MinLength } from 'class-validator';
export class UserLoginDTO{
  /* 
  * 账号
  */
  @IsNotIn(['',undefined,null],{message: '账号不能为空'})
  username: string;

  /* 
  * 密码
  */
  @MinLength(6,{
    message: '密码长度不能小于6位数'
  })
  password: string;
}

app.module.ts文件

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

// 子模块加载
import { UserModule } from './user/user.module'

@Module({
  imports: [
    UserModule
  ]
})
export class AppModule {}

新建common文件夹里面分别建立对应的文件夹以及文件:
中间件(middleware) — xml.middleware.ts
守卫(guard) — auth.guard.ts
管道(pipe) — validation.pipe.ts
异常过滤器(filters) — http-exception.filter.ts
拦截器(interceptor) — response.interceptor.ts

// main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

import { ValidationPipe } from './common/pipe/validation.pipe';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import { XMLMiddleware } from './common/middleware/xml.middleware';
import { AuthGuard } from './common/guard/auth.guard';
import { ResponseInterceptor } from './common/interceptor/response.interceptor';


async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 全局注册通用验证管道ValidationPipe
  app.useGlobalPipes(new ValidationPipe());

  // 全局注册通用异常过滤器HttpExceptionFilter
  app.useGlobalFilters(new HttpExceptionFilter());

  // 全局注册xml支持中间件(这里必须调用.use才能够注册)
  app.use(new XMLMiddleware().use);

  // 全局注册权限验证守卫
  app.useGlobalGuards(new AuthGuard());

  // 全局注册响应拦截器
  app.useGlobalInterceptors(new ResponseInterceptor());

  await app.listen(3001);
}
bootstrap();

中间件是请求的第一道关卡

  1. 执行任何代码。
  2. 对请求和响应对象进行更改。
  3. 结束请求-响应周期。
  4. 调用堆栈中的下一个中间件函数。
  5. 如果当前的中间件函数没有结束请求-响应周期, 它必须调用 next() 将控制传递给下一个中间件函数。否则, 请求将被挂起

本例中:使用中间件让express支持xml请求并且将xml内容转换为json数组

// common/middleware/xml.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
const xml2js = require('xml2js');
const parser = new xml2js.Parser();

@Injectable()
export class XMLMiddleware implements NestMiddleware {
  // 参数是固定的Request/Response/next,
  // Request/Response/next对应请求体和响应体和下一步函数
  use(req: Request, res: Response, next: Function) {
    console.log('进入全局xml中间件...');
    // 获取express原生请求对象req,找到其请求头内容,如果包含application/xml,则执行转换
    if(req.headers['content-type'] && req.headers['content-type'].includes('application/xml')){
      // 监听data方法获取到对应的参数数据(这里的方法是express的原生方法)
      req.on('data', mreq => {
        // 使用xml2js对xml数据进行转换
        parser.parseString(mreq,function(err,result){
          // 将转换后的数据放入到请求对象的req中
          console.log('parseString转换后的数据',result);
          // 这里之后可以根据需要对result做一些补充完善
          req['body']= result;
        })
      })
    }
    // 调用next方法进入到下一个中间件或者路由
    next();
  }
}

注册方式

  • 全局注册:在main.ts中导入需要的中间件模块如:XMLMiddleware然后使用 app.use(new XMLMiddleware().use)即可
  • 模块注册:在对应的模块中注册如:user.module.ts

同一路由注册多个中间件的执行顺序为,先是全局中间件执行,然后是模块中间件执行,模块中的中间件顺序按照.apply中注册的顺序执行

守卫是第二道关卡

守卫控制一些权限内容,如:一些接口需要带上token标记,才能够调用,守卫则是对这个标记进行验证操作的。
本例中代码如下:

// common/guard/auth.guard.ts

import {Injectable,CanActivate,HttpException,HttpStatus,ExecutionContext,} from '@nestjs/common';
@Injectable()
export class AuthGuard implements CanActivate {
  // context 请求的(Response/Request)的引用
  async canActivate(context: ExecutionContext): Promise<boolean> {
    console.log('进入全局权限守卫...');
    // 获取请求对象
    const request = context.switchToHttp().getRequest();
    // 获取请求头中的token字段
    const token = context.switchToRpc().getData().headers.token;
    // 如果白名单内的路由就不拦截直接通过
    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;
  }
};

注册方式

  • 全局注册:在main.ts中导入需要的守卫模块如:AuthGuard。然后使用 app.useGlobalGuards(new AuthGuard()) 即可
  • 模块注册:在需要注册的controller控制器中导入AuthGuard。然后从@nestjs/common中导UseGuards装饰器。最后直接放置在对应的@Controller()或者@Post/@Get…等装饰器之下即可

同一路由注册多个守卫的执行顺序为,先是全局守卫执行,然后是模块中守卫执行

拦截器是第三道关卡

想到自定义返回内容如

{
    "statusCode": 400,
    "timestamp": "2022-05-14T08:06:45.265Z",
    "path": "/user/login",
    "message": "请求失败",
    "data": {
        "isNotIn": "账号不能为空"
    }
}

这个时候就可以使用拦截器来做一下处理了。
拦截器作用:

  1. 在函数执行之前/之后绑定额外的逻辑
  2. 转换从函数返回的结果
  3. 转换从函数抛出的异常
  4. 扩展基本函数行为
  5. 根据所选条件完全重写函数 (例如, 缓存目的)

拦截器的执行顺序分为两个部分:

  • 第一个部分在管道和自定义逻辑(next.handle()方法)之前。
  • 第二个部分在管道和自定义逻辑(next.handle()方法)之后。
// common/interceptor/response.interceptor.ts

/* 
 * 全局响应拦截器,统一返回体内容
 *
*/

import {
  Injectable,
  NestInterceptor,
  CallHandler,
  ExecutionContext,
} from '@nestjs/common';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
// 返回体结构
interface Response<T> {
  data: T;
}
@Injectable()
export class ResponseInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(
    context: ExecutionContext,
    next: CallHandler<T>,
  ): Observable<Response<T>> {
    // 解析ExecutionContext的数据内容获取到请求体
    const ctx = context.switchToHttp();
    const request = ctx.getRequest();
    // 实现数据的遍历与转变
    console.log('进入全局响应拦截器...');
    return next.handle().pipe(
      map(data => {
        console.log('全局响应拦截器方法返回内容后...');
        return {
          statusCode: 0,
          timestamp: new Date().toISOString(),
          path: request.url,
          message: '请求成功',
          data:data
        };
      }),
    );
  }
}

中间多了个全局管道以及自定义逻辑,即只有路由绑定的函数有正确的返回值之后才会有next.handle()之后的内容

注册方式

  • 全局注册:在main.ts中导入需要的模块如:ResponseInterceptor。然后使用 app.useGlobalInterceptors(new ResponseInterceptor())即可
  • 模块注册:在需要注册的controller控制器中导入ResponseInterceptor。然后从@nestjs/common中导入UseInterceptors装饰器。最后直接放置在对应的@Controller()或者@Post/@Get…等装饰器之下即可

同一路由注册多个拦截器时候,优先执行模块中绑定的拦截器,然后其拦截器转换的内容将作为全局拦截器的内容,即包裹两次返回内容如:

{ // 全局拦截器效果
    "statusCode": 0,
    "timestamp": "2022-05-14T08:20:06.159Z",
    "path": "/user/login",
    "message": "请求成功",
    "data": {
    	"pagenum": 1, // 模块中拦截器包裹效果
    	“pageSize": 10
        "list": []
    }
}

认识官方的三个内置管道

  1. ValidationPipe:基于class-validatorclass-transformer这两个npm包编写的一个常规的验证管道,可以从class-validator导入配置规则,然后直接使用验证(当前不需要了解ValidationPipe的原理,只需要知道从class-validator引规则,设定到对应字段,然后使用ValidationPipe即可)
  2. ParseIntPipe:转换传入的参数为数字

如:传递过来的是/test?id=‘123’”这里会将字符串‘123’转换成数字123

  1. ParseUUIDPipe:验证字符串是否是 UUID(通用唯一识别码)

如:传递过来的是/test?id=‘xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx’”这里会验证格式是否正确,不正确则抛出错误,否则调用findOne方法

本例中管道使用如下:

// common/pipe/validation.pipe.ts

/* 
 * 全局dto验证管道
 *
*/

import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';

@Injectable()
export class ValidationPipe implements PipeTransform<any>{
  // value 是当前处理的参数,而 metatype 是属性的元类型
  async transform(value: any, { metatype }: ArgumentMetadata) {
    console.log('进入全局管道...');
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    // plainToClass方法将普通的javascript对象转换为特定类的实例
    const object = plainToClass(metatype, value);
    // 验证该对象返回出错的数组
    const errors = await validate(object);
    if (errors.length > 0) {
      // 将错误信息数组中的第一个内容返回给异常过滤器
      let errormsg = errors.shift().constraints;
      throw new BadRequestException(errormsg);
    }
    return value;
  }
  // 验证属性值的元类型是否是String, Boolean, Number, Array, Object中的一种
  private toValidate(metatype: any): boolean {
    const types: Function[] = [String, Boolean, Number, Array, Object];
    return !types.includes(metatype);
  }

}

注册方式

  • 全局注册:在main.ts中导入需要的模块如:ValidationPipe;然后使用 app.useGlobalPipes(new ValidationPipe()) 即可
  • 模块注册:在需要注册的controller控制器中导入ValidationPipe;然后从@nestjs/common中导入UsePipes装饰器;最后直接放置在对应的@Controller()或者@Post/@Get…等装饰器之下即可,管道还允许注册在相关的参数上如:@Body/@Query…

注意: 同一路由注册多个管道的时候,优先执行全局管道,然后再执行模块管道:

  • 异常过滤器是所有抛出的异常的统一处理方案
  • 简单来讲就是捕获系统抛出的所有异常,然后自定义修改异常内容,抛出友好的提示。

内置异常类

系统提供了不少内置的系统异常类,需要的时候直接使用throw new XXX(描述,状态)这样的方式即可抛出对应的异常,一旦抛出异常,当前请求将会终止。

注意每个异常抛出的状态码有所不同。如:

BadRequestException400
UnauthorizedException401
ForbiddenException403
NotFoundException404
NotAcceptableException406
RequestTimeoutException408
ConflictException409
GoneException410
PayloadTooLargeException413
UnsupportedMediaTypeException415
UnprocessableEntityException422
InternalServerErrorException500
NotImplementedException501
BadGatewayException502
ServiceUnavailableException503
GatewayTimeoutException504

本例中使用的是自定义的异常类,代码如下:

// common/filters/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException,Logger,HttpStatus } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
  // exception 当前正在处理的异常对象
  // host 是传递给原始处理程序的参数的一个包装(Response/Request)的引用
  catch(exception: HttpException, host: ArgumentsHost) {
    console.log('进入全局异常过滤器...');
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    // HttpException 属于基础异常类,可自定义内容
    // 如果是自定义的异常类则抛出自定义的status 
    // 否则就是内置HTTP异常类,然后抛出其对应的内置Status内容
    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;
    // 抛出错误信息
    const message =
      exception.message ||
      exception.message.message ||
      exception.message.error ||
      null;
    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);
  }
}

注册方式

  • 全局注册:在main.ts中导入需要的模块如:HttpExceptionFilter 然后使用 app.useGlobalFilters(new HttpExceptionFilter()) 即可
  • 模块注册:在需要注册的controller控制器中导入HttpExceptionFilter然后从@nestjs/common中导入UseFilters装饰器;最后直接放置在对应的@Controller()或者@Post/@Get…等装饰器之下即可

注意:  同一路由注册多个管道的时候,只会执行一个异常过滤器,优先执行模块中绑定的异常过滤器,如果模块中无绑定异常过滤则执行全局异常过滤器

数据验证

如何 限制 和 验证 前端传递过来的数据?

常用:dto(data transfer object数据传输对象) + class-validator,自定义提示内容,还能集成swagger

class-validator的验证项装饰器

github.com/typestack/c…

@IsOptional() //可选的
@IsNotEmpty({ message: ‘不能为空’ })
@MinLength(6, {message: ‘密码长度不能小于6位’})
@MaxLength(20, {message: ‘密码长度不能超过20位’})
@IsEmail({}, { message: ‘邮箱格式错误’ }) //邮箱
@IsMobilePhone(‘zh-CN’, {}, { message: ‘手机号码格式错误’ }) //手机号码
@IsEnum([0, 1], {message: ‘只能传入数字01’}) //枚举
@ValidateIf(o => o.username === ‘admin’) //条件判断,条件满足才验证,如:这里是传入的username是admin才验证
yarn add class-validator class-transformer

全局使用内置管道ValidationPipe ,不然会报错,无法起作用

import { NestFactory } from '@nestjs/core';
import { Logger, ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe()); //全局内置管道
  await app.listen(3000);
}
bootstrap();

编写dto,使用class-validator的校验项验证

创建DTO:只需要用户名,密码即可,两种都不能为空

可以使用nest g res user一键创建带有dto的接口模块

import { IsNotEmpty, MinLength, MaxLength } from 'class-validator';

export class CreateUserDto {
  @IsNotEmpty({ message: '用户名不能为空' })
  username: string;

  @IsNotEmpty({ message: '密码不能为空' })
  @MinLength(6, {
    message: '密码长度不能小于6位',
  })
  @MaxLength(20, {
    message: '密码长度不能超过20位',
  })
  password: string;
}

修改DTO:用户名,密码,手机号码,邮箱,性别,状态,都是可选的

import {
  IsEnum,
  MinLength,
  MaxLength,
  IsOptional,
  IsEmail,
  IsMobilePhone,
} from 'class-validator';
import { Type } from 'class-transformer';

export class UpdateUserDto {
  @IsOptional()
  username: string;

  @IsOptional()
  @MinLength(6, {
    message: '密码长度不能小于6位',
  })
  @MaxLength(20, {
    message: '密码长度不能超过20位',
  })
  password: string;

  @IsOptional()
  @IsEmail({}, { message: '邮箱格式错误' })
  email: string;

  @IsOptional()
  @IsMobilePhone('zh-CN', {}, { message: '手机号码格式错误' })
  mobile: string;

  @IsOptional()
  @IsEnum(['male', 'female'], {
    message: 'gender只能传入字符串male或female',
  })
  gender: string;

  @IsOptional()
  @IsEnum({ 禁用: 0, 可用: 1 },{
    message: 'status只能传入数字0或1',
  })
  @Type(() => Number) //如果传递的是string类型,不报错,自动转成number类型
  status: number;
}

controllerservice一起使用

// user.controller.ts

import {
  Controller,
  Post,
  Body,
  HttpCode,
  HttpStatus,
} from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) { }
  
  @Post()
  @HttpCode(HttpStatus.OK)
  async create(@Body() user: CreateUserDto) { //使用创建dto
    return await this.userService.create(user);
  }
  
  @Patch(':id')
    async update(@Param('id') id: string, @Body() user: UpdateUserDto) {  //使用更新dto
      return await this.userService.update(id, user);
    }
  }
// user.service.ts

import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { UsersEntity } from './entities/user.entity';
import { ToolsService } from '../../utils/tools.service';
import { CreateUserDto } from './dto/create-user.dto';


@Injectable()
export class UserService {
  constructor(
    @InjectRepository(UsersEntity)
    private readonly usersRepository: Repository<UsersEntity>,
  ) { }

  async create(user: CreateUserDto) { //使用dto
    do some thing....
  }
}


完结

基础知识才是根基,掌握好才能构建出更科学合理更庞大性能更好的应用。加油!!!