📕
余烬的小册
数据结构与算法GitHub
  • 总述
  • 经验记录
    • 经验总结
      • web component
      • 前端性能优化总结与分析
      • 我的长列表优化方案
      • 双向通讯解决方案
      • 🔧基于istanbul实现代码测试覆盖率工具
      • 表单系统(低代码表单)
      • 跨端小程序
      • 设计一个即时聊天功能
      • 跨页面通讯 3658699fe4cb4d0bbe22b0881390bacd
    • 踩坑记录
      • HTML踩坑记录
      • Flutter踩坑记录
      • CSS踩坑记录
  • 源码解析
    • Vue源码解析
      • Vue2源码解析系列-响应式原理
      • Vue2源码解析系列-模板编译
      • Vue2源码解析系列-渲染系统(待更新)
        • Patch
      • Vue2源码解析系列-调度系统(todo)
      • Vue2组件更新流程(todo)
      • 如何学习Vue源码
      • Vue3源码解析系列-响应系统
      • Vue3源码解析系列-渲染系统
      • Vue3源码解析系列-组件化和渲染优化(todo)
      • Vue router源码解析(todo)
    • React源码解析(todo)
    • 微前端
      • qiankun源码解析(todo)
    • Vite源码解析
      • Vite Client源码
      • Vite Server源码(todo)
  • 前端技术
    • javaScript
      • ES6
        • 变量声明
        • 模块化
        • 箭头函数
        • 你不知道的for...of
        • 新的数据结构Set和Map
        • JavaScript异步编程终极解决方案
        • ES6 Class 3a0c0a225a534984aabe9a943c5df975
      • JavaScript Error
      • JavaScript浅拷贝和深拷贝
      • JavaScript闭包
      • JavaScript最佳实践
      • JavaScript设计模式
      • async函数的polyfill
    • 深入理解JavaScript系列
      • JavaScript中的继承
      • JavaScript原始类型和引用类型
      • JavaScript浅拷贝和深拷贝
      • JavaScript手写系列
      • JavaScript之this
      • 词法环境和环境记录
      • JavaScript内存泄漏
      • 执行上下文
      • 从ECMAScript规范中学习this
    • TypeScript
      • TypeScript基础教程
      • Typescript高级操作
      • TypeScript工具类型
      • Typescript手写实现工具类型
      • Typescript总结(思维导图)
    • 浏览器原理
      • 页面渲染原理
      • 浏览器存储
      • JavaScript事件循环
      • 事件循环
      • 跨域
      • DOM事件流
      • 从输入url到页面渲染
      • 判断节点之间的关系及根据节点关系查找节点
      • history API
    • 跨端技术
      • Flutter
        • Flutter布局组件
    • 前端工程化
      • Babel插件开发指南
      • 循环依赖
      • pm2
    • React
      • React 状态管理
      • React组件通讯
      • Redux入门
      • Flux
      • React Hook(todo)
      • Effect
  • 服务器端
    • 计算机网络
      • 应用层
      • 运输层
      • 物理层
      • 数据链路层
      • HTTP缓存
      • HTTPS
      • 网络层
    • NodeJs
      • Node.js
      • nodejs最佳实践
      • 《深入浅出Nodejs》小结
      • mongoose填充(populate)
      • node事件循环
      • Node子进程
      • nestjs从零开始
      • nodejs流
      • Nodejs调试
      • Koa源码解析
    • 服务器
      • 操作系统
      • Linux
      • nginx常用指令
      • nginx常用配置
    • 数据库
      • Mysql常见语法
      • MongoDB Indexes索引
  • 前端安全与性能优化
    • 前端安全
      • 跨站脚本攻击(XSS)
      • 跨站点请求伪造(CSRF)
      • 点击劫持
      • 中间人攻击
      • 越权攻击与JWT
    • 前端性能优化
      • 前端监控系统
      • 前端性能优化总结与分析 7348bba0918645b1899006dc842a64c1
      • 衡量性能的核心指标 0dc15ef127cf4f4a9f1137c377420292
      • 图片懒加载
  • 杂项
    • 其他
      • Git
      • web component框架
      • 实现滚动框的懒加载
      • Stencil指南
    • CSS
      • 定位和层叠上下文
      • BFC
      • 盒模型
      • css选择器
      • css变量
由 GitBook 提供支持
在本页
  • 基础概念
  • 控制器Controller
  • 路由
  • 状态码
  • 参数
  • 控制器中的装饰器
  • Headers
  • 提供者Provider
  • 模块Module
  • 根模块
  • 实践总结
  • Typescript
  • Joi or class-validator
  • 参数验证
  • 文件上传与下载
  • 数据库
  • 日志
  • 压缩
  • 鉴权
  • 安全
  • 自定义装饰器
  • 拦截器
  • Swagger文档
  • 参考
在GitHub上编辑
  1. 服务器端
  2. NodeJs

nestjs从零开始

上一页Node子进程下一页nodejs流

tags: Nodejs Created time: June 17, 2022 5:01 PM emoji: https://nestjs.com/favicon.264d6486.ico

Nest 是一个用于构建高效,可扩展的 服务器端应用程序的框架。它使用渐进式 JavaScript,内置并完全支持 (但仍然允许开发人员使用纯 JavaScript 编写代码)并结合了 OOP(面向对象编程),FP(函数式编程)和 FRP(函数式响应编程)的元素。

在底层,Nest使用强大的 HTTP Server 框架,如 Express(默认)和 Fastify。Nest 在这些框架之上提供了一定程度的抽象,同时也将其 API 直接暴露给开发人员。这样可以轻松使用每个平台的无数第三方模块。

基础概念

nestjs的三个基础概念:控制器、提供者和模块。

控制器负责处理传入的请求和传出的响应。

提供者 是 Nest的一个基本概念。许多基本的 Nest类可能被视为 provider,例如 service, repository, factory, helper等等。

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

控制器Controller

在nestjs中要创建一个控制器需要定义一个由@Controller 装饰器装饰的类,装饰器会将类与所需的元数据相关联,使nest能够创建路由映射。

路由

例如我们实现一个最简单的 Get /cat/ 的路由,并响应一条响应数据。

import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats'
  }
}

这里除了@Controller 装饰器,还用到了@Get装饰器,表示匹配**Get HTTP请求,**除了@Get装饰器外还有@Delete、@Post、@Patch 等装饰器,对应着HTTP方法。

这些装饰器可以传递一个字符串,例如@Get(’index’) 就对应着路径/cats/index 。

  • 注意:

    路由与装饰方法的方法名称没有直接联系,例如上面代码中你可以把findAll方法名改成其他任意名称。

状态码

在Restfull API中状态码由很重要的作用,默认情况下nest总是返回200状态码(Post方法是201),可以通过@HttpCode 装饰器来改变状态码。

@Post()
@HttpCode(204)
create() {
  return 'This action adds a new cat';
}

当发送异常时,nest会返回500状态码,但是你可以通过主动抛出异常来改变状态码。

@Post()
  @UseInterceptors(FileInterceptor('file'))
  @ApiConsumes('multipart/form-data')
  @ApiBody({
    description: '图片文件',
    type: FileUploadDto,
  })
  update(@UploadedFile() file: Express.Multer.File) {
    const reg = /^image/;
    if (!reg.test(file.mimetype)) {
      throw new HttpException('只接受图片文件', HttpStatus.FORBIDDEN);
    }
    if (file.size > IMAGE_SIZE) {
      throw new HttpException('图片不能大于10MB', HttpStatus.FORBIDDEN);
    }
    try {
      this.imageService.saveImage(file);
      return new ResponseJSON(true);
    } catch {
      throw new HttpException('上传失败', 500);
    }
  }

HttpException 是nest内置的一个异常类,可以通过主动抛出HttpException 异常来自定义响应。

HttpException 构造函数有两个必要的参数来决定响应:

  • response 参数定义 JSON 响应体。它可以是 string 或 object,如下所述。

而HttpStatus其实是nest内置的一个枚举,拥有所有HTTP状态码,你可以直接写状态码,也可以通过HttpStatus 枚举成员来传递状态码,例如HttpStatus.FORBIDDEN 就是403 禁止。

参数

一般来说HTTP中可以通过三种方式传递参数。

  • Param 路由参数

  • Query 查询字符串

  • Body 请求体

Param路由参数你可以想象加载图片的场景,例如你想加载一个a.jpg图片,通常是通过特定url+图片名的形式访问的,例如/images/a.jpg,当你想访问b.jpg图片时就是/images/b.jpg ,而a.jpg和b.jpg其实就是路由参数,这和vue-router的Param类似。

Get(':img')
findOne(@Param('img') img): string {
  return `返回${img}图片`;
}

通过: + 参数名的形式定义Param,然后可以通过@Param(name) 访问该路由参数。

Query查询字符串就是我们常见的url参数形式,例如/login?username=123&password=123 ,查询字符串的格式是key=value ,并且通过&字符连接(第一个参数用?)。

@Get()
getDocs(@Query() query) {
    return query;
}

前面两种方式都是将参数放到请求的url中,而最后这种方式是将参数放到请求体中,同理通过@Post装饰器来获取数据。

@Post()
async create(@Body() createCatDto) {
  return 'This action adds a new cat';
}

控制器中的装饰器

nest(默认情况下)其实是对express的封装,你可以通过这些装饰器来很方便地获取相应的数据。

装饰器
对应的对象

@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]

@Ip()

req.ip

@HostParam()

req.hosts

Headers

要自定义响应头可以通过@Header或者直接使用express的相关方法。

@Post()
@Header('Cache-Control', 'none')
create() {
  return 'This action adds a new cat';
}
// 或者
const imageCacheTime = 100000
@Get(':imagePath')
  fetchImage(@Param() param: FetchImageDto, @Res() res) {
    res
      .set('cache-control', `public, max-age=${imageCacheTime}`)
      .sendFile(join(__dirname, '../../../public/images/' + param.imagePath));
}

自定义headers是非常有用的,你可以通过自定义header来实现重定向、http缓存等功能,当然nest提供相应的装饰器来方便地实现这些功能,但是他们的底层仍然是通过HTTP headers来实现的,学会自定义header可以更加精确地控制行为。

提供者Provider

提供者是一个用 @Injectable()装饰器注释的类,可以通过contructor注入依赖关系。

import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}

提供者的意义在于,它负责处理复杂的任务,以前面的控制器为例,你可以让控制器只负责路由、HTTP等相关逻辑,而更具体、更复杂的的数据处理任务交给提供者。

// cat.controller.ts
// 控制器关注路由、HTTP相关的逻辑
@Controller(APIBase + 'aircle')
export class CatController {
	constructor(private readonly catsService : CatsService ) {} // 依赖注入
	@Get()
	  findAll() {
	    return this.catsService.findAll();
	}
}

// cat.service.ts
// 而提供者通常负责更复杂的任务
@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}

在 Nest 中,借助 TypeScript 功能,管理依赖项非常容易,因为它们仅按类型进行解析。在下面的示例中,Nest 将 catsService 通过创建并返回一个实例来解析 CatsService(或者,在单例的正常情况下,如果现有实例已在其他地方请求,则返回现有实例)。解析此依赖关系并将其传递给控制器的构造函数(或分配给指定的属性):

constructor(private readonly catsService: CatsService) {}

模块Module

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

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

@module() 装饰器接受一个描述模块属性的对象:

providers
由 Nest 注入器实例化的提供者,并且可以至少在整个模块中共享

controllers

必须创建的一组控制器

imports

导入模块的列表,这些模块导出了此模块中所需提供者

exports

由本模块提供并应在其他模块中可用的提供者的子集。

CatsController和 CatsService属于同一个应用程序域,因此将它们都移入CatsModule中。

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

根模块

nest必须要有一个根模块,根模块是 Nest 开始安排应用程序树的地方。

其他模块通过import直接或间接的导入到根模块中。

@Module({
  imports: [
    MongooseModule.forRoot('mongodb://localhost/myblog'),
    DocsModule,
    ImageModule,
    LoginModule,
    AircleModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})

实践总结

使用nest构建一套restfull风格的API。

Typescript

nest提供良好的typescript支持,这也是我选择nest而不是egg的原因之一。typescript可以使用type、interface和class定义DTO,但是推荐使用class,因为接口和type在编译后会消失,而类会被保留,这在管道等功能中会起作用。

Joi or class-validator

typescript提供编译时的类型支持,这在编写代码的时候非常有用,但是typescript无法提供运行时的验证功能,比如想验证客户端传入的参数是否有误。在nodejs中有两个流行的数据验证库:Joi和class-validator 两者都能提供数据验证功能,但是他们的风格不相同,class-validator是使用类+装饰器的方式实现数据验证,而Joi则链式调用相应方法的方法实现数据验证,很明显class-validator 更符合nest的风格,你甚至可以将Typescript DTO和class-validator 写到同一个类中。

// 这是一个typescript类,可以在编译时提供类型支持
// 同时也能在运行时进行数据验证
export class LoginDto {
  @IsEmail()
  email: string;
  @IsString()
  password: string;
}

参数验证

在实现后端API接口时,有一个可能出现的场景是前端传入错误的参数,比如参数类型错误,参数个数错误等,而这个时候我们想给前端一个友好的提示,而不是直接报500。

要实现这个功能就需要在执行路由处理函数前先对参数进行验证,如果验证通过才执行处理函数返回响应,如果验证失败则抛出一个友好的异常。要达到这个目的需要用到nest的管道功能。

管道

管道是一个很有趣的概念,在nodejs中也有管道的概念。数据流流入管道中,经过一系列的处理,最终从管道的另一头流出,而在这个过程中,管道可以实现两个目的

  • 转换:将输入数据转换成所需的数据输出

  • 验证:对输入数据进行验证,只有验证成功的数据才能输出

Nest 自带八个开箱即用的管道,即

  • ValidationPipe

  • ParseIntPipe

  • ParseBoolPipe

  • ParseArrayPipe

  • ParseUUIDPipe

  • DefaultValuePipe

  • ParseEnumPipe

  • ParseFloatPipe

所有管道都必须实现一个transform方法,该方法接收两个参数:value和metadata。

value :当前处理的参数,

metadata :元数据。元数据对象包含一些属性:

type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<unknown>;
data?: string;
参数
描述

type

告诉我们该属性是一个 body @Body(),query @Query(),param @Param() 还是自定义参数 在这里阅读更多。

metatype

属性的元类型,例如 String。 如果在函数签名中省略类型声明,或者使用原生 JavaScript,则为 undefined。

data

传递给装饰器的字符串,例如 @Body('string')。 如果您将括号留空,则为 undefined。

实现一个参数验证管道,如果传递的参数

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

@Injectable()
export class validRequest implements PipeTransform {
  async transform(value: any, metadata: ArgumentMetadata) {
    const metatype = metadata.metatype;
		// 如果没有DTO,或者DTO是String等,则直接通过
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
		/*
		* metatype是参数对象的类
		* plainToClass函数将value转换成metatype的实例
		* validate进行验证
		*/
    const object = plainToClass(metatype, value);
    const errors = await validate(object);

		// 存在异常,返回参数错误响应
    if (errors.length > 0) {
      throw new HttpException('参数错误', HttpStatus.BAD_REQUEST);
    }
    return value;
  }

  private toValidate(metatype: any): boolean {
    const types: any[] = [String, Boolean, Number, Array, Object];
    return !types.includes(metatype);
  }
}

metatype 就是路由处理函数中的DTO,例如下面代码中metatype就是LoginDto 。

// login.dto.ts
// 这是一个typescript类,可以在编译时提供类型支持
// 同时也能在运行时进行数据验证
export class LoginDto {
  @IsEmail()
  email: string;
  @IsString()
  password: string;
}
// login.controller.ts
@Controller('user')
export class LoginController {
  constructor(private readonly loginService: LoginService) {}

  @Post('login')
  login(@Body() body: LoginDto) {
    return this.loginService.login(body);
  }
}

可以全局注册该管道,也可以局部模块使用,在这种场景下我们可以全局注册使用。

app.useGlobalPipes(new validRequest());

文件上传与下载

数据库

日志

压缩

鉴权

安全

中间层实现xss防护

自定义装饰器

拦截器

Swagger文档

参考

status参数定义HTTP。

Node.js
TypeScript
状态代码
Nest.js 中文文档
Documentation | NestJS - A progressive Node.js framework