Nestjs学习总结
原文地址: https://blog.poetries.top/2022/05/25/nest-summary
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中服务相当于MVC
的Model
创建服务
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">
<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>
<h3>模板引擎</h3>
<%=message%>
</body>
</html>
渲染页面
Nestjs中
Render
装饰器可以渲染模板,
使用路由匹配渲染引擎
mport { 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,
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,
自定义管道
使用快捷命令生成:
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,
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,
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-validator
、class-transformer
管道验证包和转换器
建立user模块:模块内容结构:
nest g res user
user.controller.ts文件
import {
Controller,
Post,
} 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();
中间件是请求的第一道关卡
- 执行任何代码。
- 对请求和响应对象进行更改。
- 结束请求-响应周期。
- 调用堆栈中的下一个中间件函数。
- 如果当前的中间件函数没有结束请求-响应周期, 它必须调用 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": "账号不能为空"
这个时候就可以使用拦截器来做一下处理了。
拦截器作用:
- 在函数执行之前/之后绑定额外的逻辑
- 转换从函数返回的结果
- 转换从函数抛出的异常
- 扩展基本函数行为
- 根据所选条件完全重写函数 (例如, 缓存目的)
拦截器的执行顺序分为两个部分:
- 第一个部分在管道和自定义逻辑(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": []
管道是第四道关卡
- 管道是请求过程中的第四个内容,主要用于对请求参数的验证和转换操作。
-
项目中使用
class-validator
class-transformer
进行配合验证相关的输入操作内容
认识官方的三个内置管道
-
ValidationPipe
:基于class-validator
和class-transformer
这两个npm包编写的一个常规的验证管道,可以从class-validator
导入配置规则,然后直接使用验证(当前不需要了解ValidationPipe
的原理,只需要知道从class-validator
引规则,设定到对应字段,然后使用ValidationPipe
即可) -
ParseIntPipe
:转换传入的参数为数字
如:传递过来的是/test?id=‘123’”这里会将字符串‘123’转换成数字123
- 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(描述,状态)这样的方式即可抛出对应的异常,一旦抛出异常,当前请求将会终止。
注意每个异常抛出的状态码有所不同 。如:
BadRequestException — 400
UnauthorizedException — 401
ForbiddenException — 403
NotFoundException — 404
NotAcceptableException — 406
RequestTimeoutException — 408
ConflictException — 409
GoneException — 410
PayloadTooLargeException — 413
UnsupportedMediaTypeException — 415
UnprocessableEntityException — 422
InternalServerErrorException — 500
NotImplementedException — 501
BadGatewayException — 502
ServiceUnavailableException — 503
GatewayTimeoutException — 504
本例中使用的是自定义的异常类,代码如下:
// 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的验证项装饰器
https:// github.com/typestack/cl ass-validator#usage
@IsOptional() //可选的
@IsNotEmpty({ message: ‘不能为空’ })
@MinLength(6, {message: ‘密码长度不能小于6位’})
@MaxLength(20, {message: ‘密码长度不能超过20位’})
@IsEmail({}, { message: ‘邮箱格式错误’ }) //邮箱
@IsMobilePhone(‘zh-CN’, {}, { message: ‘手机号码格式错误’ }) //手机号码
@IsEnum([0, 1], {message: ‘只能传入数字0或1’}) //枚举
@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;
controller
和
service
一起使用
// 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....
进阶
配置抽离
yarn add nestjs-config
app.module.ts
import * as path from 'path';
import { Module } from '@nestjs/common';
//数据库
import { TypeOrmModule } from '@nestjs/typeorm';
//全局配置
import { ConfigModule, ConfigService } from 'nestjs-config';
@Module({
imports: [
//1.配置config目录
ConfigModule.load(path.resolve(__dirname, 'config', '**/!(*.d).{ts,js}')),
//2.读取配置,这里读取的是数据库配置
TypeOrmModule.forRootAsync({
useFactory: (config: ConfigService) => config.get('database'),
inject: [ConfigService], // 获取服务注入
controllers: [AppController],
providers: [AppService],
export class AppModule {}
配置数据库
src -> config -> database
import { join } from 'path';
export default {
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'your password',
database: 'test',
entities: [join(__dirname, '../', '**/**.entity{.ts,.js}')],
synchronize: true,
环境配置
yarn add cross-env
cross-env的作用是兼容window系统和mac系统来设置环境变量
在package.json中配置
"scripts": {
"start:dev": "cross-env NODE_ENV=development nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "cross-env NODE_ENV=production node dist/main",
dotenv的使用
yarn add dotenv
根目录创建 env.parse.ts
import * as fs from 'fs';
import * as path from 'path';
import * as dotenv from 'dotenv';
const isProd = process.env.NODE_ENV === 'production';
const localEnv = path.resolve('.env.local');
const prodEnv = path.resolve('.env.prod');
const filePath = isProd && fs.existsSync(prodEnv) ? prodEnv : localEnv;
// 配置 通过process.env.xx读取变量
dotenv.config({ path: filePath });
导入环境
// main.ts
import '../env.parse'; // 导入环境变量
.env.local
PORT=9000
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=123
MYSQL_DATABASE=test
.env.prod
PORT=9000
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=1234
MYSQL_DATABASE=test
读取环境变量
process.env.MYSQL_HOST
形式
文件上传与下载
yarn add @nestjs/platform-express compressing
compressing 文件下载依赖,提供流的方式
配置文件的目录地址,以及文件的名字格式
// src/config/file.ts 上传文件配置
import { join } from 'path';
import { diskStorage } from 'multer';
* 上传文件配置
export default {
root: join(__dirname, '../../assets/uploads'),
storage: diskStorage({
destination: join(
__dirname,
`../../assets/uploads/${new Date().toLocaleDateString()}`,
filename: (req, file, cb) => {
const filename = `${new Date().getTime()}.${file.mimetype.split('/')[1]}`;
return cb(null, filename);
// app.module.ts
import { ConfigModule, ConfigService } from 'nestjs-config';
@Module({
imports: [
// 加载配置文件目录 src/config
ConfigModule.load(resolve(__dirname, 'config', '**/!(*.d).{ts,js}')),
controllers: [],
providers: [],
export class AppModule implements NestModule {}
// upload.controller.ts
import {
Controller,
Post,
UseInterceptors,
UploadedFile,
UploadedFiles,
Body,
} from '@nestjs/common';
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
import { FileUploadDto } from './dto/upload-file.dto';
import { UploadService } from './upload.service';
import { Response } from 'express';
@Controller('common')
export class UploadController {
constructor(private readonly uploadService: UploadService) {}
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file) {
this.uploadService.uploadSingleFile(file);
return true;
// 多文件上传
@Post('uploads')
@UseInterceptors(FilesInterceptor('file'))
uploadMuliFile(@UploadedFiles() files, @Body() body) {
this.uploadService.UploadMuliFile(files, body);
return true;
@Get('export')
async downloadAll(@Res() res: Response) {
const { filename, tarStream } = await this.uploadService.downloadAll();
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Disposition', `attachment; filename=${filename}`);
tarStream.pipe(res);
// upload.service.ts
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { join } from 'path';
import { createWriteStream } from 'fs';
import { tar } from 'compressing';
import { ConfigService } from 'nestjs-config';
@Injectable()
export class UploadService {
constructor(private readonly configService: ConfigService) {}
uploadSingleFile(file: any) {
console.log('file', file);
UploadMuliFile(files: any, body: any) {
console.log('files', files);
async downloadAll() {
const uploadDir = this.configService.get('file').root;
const tarStream = new tar.Stream();
await tarStream.addEntry(uploadDir);
return { filename: 'download.tar', tarStream };
// upload.module.ts
import { Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';
import { ConfigService } from 'nestjs-config';
import { UploadService } from './upload.service';
import { UploadController } from './upload.controller';
@Module({
imports: [
MulterModule.registerAsync({
useFactory: (config: ConfigService) => config.get('file'),
inject: [ConfigService],
controllers: [UploadController],
providers: [UploadService],
export class UploadModule {}
实现图片随机验证码
nest如何实现图片随机验证码?
这里使用的是 svg-captcha 这个库,你也可以使用其他的库
yarn add svg-captcha
封装,以便多次调用
src -> utils -> tools.service.ts
import { Injectable } from '@nestjs/common';
import * as svgCaptcha from 'svg-captcha';
@Injectable()
export class ToolsService {
async captche(size = 4) {
const captcha = svgCaptcha.create({ //可配置返回的图片信息
size, //生成几个验证码
fontSize: 50, //文字大小
width: 100, //宽度
height: 34, //高度
background: '#cc9966', //背景颜色
return captcha;
在使用的module中引入
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { ToolsService } from '../../utils/tools.service';
@Module({
controllers: [UserController],
providers: [UserService, ToolsService],
export class UserModule { }
使用
import { Controller, Get, Post,Body } from '@nestjs/common';
import { EmailService } from './email.service';
@Controller('user')
export class UserController{
constructor(private readonly toolsService: ToolsService,) {} //注入服务
@Get('authcode') //当请求该接口时,返回一张随机图片验证码
async getCode(@Req() req, @Res() res) {
const svgCaptcha = await this.toolsService.captche(); //创建验证码
req.session.code = svgCaptcha.text; //使用session保存验证,用于登陆时验证
console.log(req.session.code);
res.type('image/svg+xml'); //指定返回的类型
res.send(svgCaptcha.data); //给页面返回一张图片
@Post('/login')
login(@Body() body, @Session() session) {
//验证验证码,由前端传递过来
const { code } = body;
if(code?.toUpperCase() === session.code?.toUpperCase()){
console.log(‘验证码通过’)
return 'hello authcode';
前端简单代码
<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
form {
display: flex;
.input {
width: 80px;
height: 32px;
.verify_img {
margin: 0px 5px;
</style>
</head>
<h2>随机验证码</h2>
<form action="/user/login" method="post" enctype="application/x-www-form-urlencoded">
<input type="text" name='code' class="input" />
<img class="verify_img" src="/user/code" title="看不清?点击刷新"
onclick="javascript:this.src='/user/code?t='+Math.random()"> //点击再次生成新的验证码
<button type="submit">提交</button>
</form>
</body>
</html>
邮件服务
邮件服务使用文档 https:// nest-modules.github.io/ mailer/docs/mailer
// 邮件服务配置
// app.module.ts
import { MailerModule } from '@nestjs-modules/mailer';
import { resolve, join } from 'path';
import { ConfigModule, ConfigService } from 'nestjs-config';
@Module({
imports: [
// 加载配置文件目录 src/config
ConfigModule.load(resolve(__dirname, 'config', '**/!(*.d).{ts,js}')),
// 邮件服务配置
MailerModule.forRootAsync({
useFactory: (config: ConfigService) => config.get('email'),
inject: [ConfigService],
controllers: [],
providers: [],
export class AppModule implements NestModule {}
// src/config/email.ts 邮件服务配置
import { join } from 'path';
// npm i ejs -S
import { EjsAdapter } from '@nestjs-modules/mailer/dist/adapters/ejs.adapter';
export default {
transport: {
host: 'smtp.qq.com',
secureConnection: true, // use SSL
secure: true,
port: 465,
ignoreTLS: false,
auth: {
user: '123@test.com',
pass: 'dfafew1',
defaults: {
from: '"nestjs" <123@test.com>',
// preview: true, // 发送邮件前预览
template: {
dir: join(__dirname, '../templates/email'), // 邮件模板
adapter: new EjsAdapter(),
options: {
strict: true,
邮件服务使用
// email.services.ts
import { Injectable } from '@nestjs/common';
import { MailerService } from '@nestjs-modules/mailer';
@Injectable()
export class EmailService {
// 邮件服务注入
constructor(private mailerService: MailerService) {}
async sendEmail() {
console.log('发送邮件');
await this.mailerService.sendMail({
to: 'test@qq.com', // 收件人
from: '123@test.com', // 发件人
// subject: '副标题',
text: 'welcome', // plaintext body
html: '<h1>hello</h1>', // HTML body content
// template: 'email', // 邮件模板
// context: { // 传入邮件模板的data
// email: 'test@qq.com',
// },
return '发送成功';
nest基于possport + jwt做登陆验证
方式与逻辑
- 基于possport的本地策略和jwt策略
- 本地策略 主要是验证账号和密码是否存在,如果存在就登陆,返回 token
- jwt策略 则是验证用户 登陆时附带的token 是否匹配和有效,如果不匹配和无效则返回 401状态码
yarn add @nestjs/jwt @nestjs/passport passport-jwt passport-local passport
yarn add -D @types/passport @types/passport-jwt @types/passport-local
jwt策略 jwt.strategy.ts
// src/modules/auth/jwt.strategy.ts
import { Strategy, ExtractJwt, StrategyOptions } from 'passport-jwt';
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { jwtConstants } from './constants';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromHeader('token'),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret, // 使用密钥解析
} as StrategyOptions);
//token验证, payload是super中已经解析好的token信息
async validate(payload: any) {
return { userId: payload.userId, username: payload.username };
本地策略 local.strategy.ts
// src/modules/auth/local.strategy.ts
import { Strategy, IStrategyOptions } from 'passport-local';
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { AuthService } from './auth.service';
//本地策略
//PassportStrategy接受两个参数:
//第一个:Strategy,你要用的策略,这里是passport-local,本地策略
//第二个:别名,可选,默认是passport-local的local,用于接口时传递的字符串
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({
usernameField: 'username',
passwordField: 'password',
} as IStrategyOptions);
// validate是LocalStrategy的内置方法
async validate(username: string, password: string): Promise<any> {
//查询数据库,验证账号密码,并最终返回用户
return await this.authService.validateUser({ username, password });
constants.ts
// src/modules/auth/constants.ts
export const jwtConstants = {
secret: 'secretKey',
使用守卫 auth.controller.ts
// src/modules/auth/auth.controller.ts
import { Controller, Get, Post, Request, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
// 登录测试 无需token
@UseGuards(AuthGuard('local')) //本地策略,传递local,执行local里面的validate方法
@Post('login')
async login(@Request() req) { //通过req可以获取到validate方法返回的user,传递给login,登陆
return this.authService.login(req.user);
// 在需要的地方使用守卫,需要带token才可访问
@UseGuards(AuthGuard('jwt'))//jwt策略,身份鉴权
@Get('userInfo')
getUserInfo(@Request() req) {//通过req获取到被验证后的user,也可以使用装饰器
return req.user;
在module引入jwt配置和数据库查询的实体 auth.module.ts
// src/modules/auth/auth.module.ts
import { LocalStrategy } from './local.strategy';
import { jwtConstants } from './constants';
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './jwt.strategy';
import { UsersEntity } from '../user/entities/user.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forFeature([UsersEntity]),
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '10d' },
controllers: [AuthController],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
export class AuthModule {}
auth.service.ts
// src/modules/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { compareSync } from 'bcryptjs';
@Injectable()
export class AuthService {
constructor(
@InjectRepository(UsersEntity),
private readonly usersRepository: Repository<UsersEntity>,
private jwtService: JwtService
validateUser(username: string, password: string) {
const user = await this.usersRepository.findOne({
where: { username },
select: ['username', 'password'],
if (!user) ToolsService.fail('用户名或密码不正确');
//使用bcryptjs验证密码
if (!compareSync(password, user.password)) {
ToolsService.fail('用户名或密码不正确');
return user;
login(user: any) {
const payload = { username: user.username }; // 把信息存在token