前言
上一篇介绍了如何使用 JWT 进行单点登录,接下来,要完善一下后端项目的一些基础功能。
首先,一个良好的服务端,应该有较完善的日志收集功能,这样才能在生产环境发生异常时,能够从日志中复盘,找出 Bug 所在。
其次,要针对项目中抛出的异常进行归类,并将信息反映在接口或日志中。
最后,请求接口的参数也应该被记录,以便统计分析(主要用于大数据和恶意攻击分析)。
GitHub 项目地址,欢迎各位大佬 Star。
一、日志系统
这里使用的是 log4js
,前身是 log4j
,如果有写过 Java 的大佬应该不会陌生。
已经有大佬总结了 log4js 的用法,就不在赘述了:
1. 配置
先安装依赖包
1 | 复制代码$ yarn add log4js stacktrace-js -S |
在 config 目录下新建一个文件 log4js.ts
,用于编写配置文件:
1 | 复制代码// config/log4js.ts |
上面贴出了我的配置,并标注了一些简单的注释,请配合 《Node.js 之 log4js 完全讲解》 一起食用。
2. 实例化
有了配置,就可以着手写 log4js 的实例以及一些工具函数了。
在 src/utils
下新建 log4js.ts
:
1 | 复制代码// src/utils/log4js.ts |
上面贴出了我实例化 log4js 的过程,主要是处理日志的组成部分(包含了时间、类型,调用文件以及调用的坐标),还可以根据日志的不同级别,在控制台中用不同的颜色显示。
这个文件,不但可以单独调用,也可以做成中间件使用。
3. 制作中间件
我们希望每次用户请求接口的时候,自动记录请求的路由、IP、参数等信息,如果每个路由都写,那就太傻了,所以需要借助中间件来实现。
Nest 中间件实际上等价于 express 中间件。
中间件函数可以执行以下任务:
- 执行任何代码;
- 对请求和响应对象进行更改;
- 结束请求-响应周期;
- 调用堆栈中的下一个中间件函数;
- 如果当前的中间件函数没有【结束请求】或【响应周期】, 它必须调用
next()
将控制传递给下一个中间件函数。否则,请求将被挂起;
执行下列命令,创建中间件文件:
1 | 复制代码$ nest g middleware logger middleware |
然后,src
目录下,就多出了一个 middleware
的文件夹,里面的 logger.middleware.ts
就是接下来的主角,Nest 预设的中间件模板长这样:
1 | 复制代码// src/middleware/logger.middleware.ts |
这里只是实现了 NestMiddleware
接口,它接收 3 个参数:
- req:即 Request,请求信息;
- res:即 Response ,响应信息;
- next:将控制传递到下一个中间件,写过 Vue、Koa 的应该不会陌生;
接下来,我们将日志功能写入中间件:
1 | 复制代码// src/middleware/logger.middleware.ts |
同时,Nest 也支持【函数式中间件】,我们将上面的功能用函数式实现一下:
1 | 复制代码// src/middleware/logger.middleware.ts |
上面的日志格式进行了一些改动,主要是为了方便查看。
至于使用 Nest 提供的还是函数式中间件,可以视需求决定。当然,Nest 原生的中间件高级玩法会更多一些。
4. 应用中间件
做好中间件后,我们只需要将中间件引入 main.ts 中就好了:
1 | 复制代码// src/main.ts |
保存代码后,就会发现,项目目录下就多了几个文件:
这就是之前 config/log4js.ts
中配置的成果
接下来,我们试着请求一下登录接口:
发现虽然是打印了,但是没有请求参数信息。
于是,我们还要做一部操作,将请求参数处理一下:
1 | 复制代码// src/main.ts |
再请求一次,发现参数已经出来了:
上面的打印信息,IP 为
::1
是因为我所有的东西都跑在本地,正常情况下,会打印对方的 IP 的。
再去看看 logs/
文件夹下:
上图可以看到日志已经写入文件了。
5. 初探拦截器
前面已经示范了怎么打印入参,但是光有入参信息,没有出参信息肯定不行的,不然怎么定位 Bug 呢。
Nest 提供了一种叫做 Interceptors
(拦截器) 的东东,你可以理解为关卡,除非遇到关羽这样的可以过五关斩六将,否则所有的参数都会经过这里进行处理,正所谓雁过拔毛。
详细的使用方法会在后面的教程进行讲解,这里只是先大致介绍一下怎么使用:
执行下列指令,创建 transform
文件
1 | 复制代码$ nest g interceptor transform interceptor |
然后编写出参打印逻辑,intercept 接受两个参数,当前的上下文和传递函数,这里还使用了 pipe
(管道),用于传递响应数据:
1 | 复制代码// src/interceptor/transform.interceptor.ts |
保存文件,然后在 main.ts
中引入,使用 useGlobalInterceptors
调用全局拦截器:
1 | 复制代码import { NestFactory } from '@nestjs/core'; |
我们再试一次登录接口:
可以看到,出参的日志已经出来了,User 为 undefiend
是因为登录接口没有使用 JWT 守卫,若路由加了 @UseGuards(AuthGuard('jwt'))
,则会把用户信息绑定在 req 上,具体操作可回顾上一篇教程。
二、异常处理
在开发的过程中,难免会写出各式各样的“八阿哥”,不然程序员就要失业了。一个富有爱心的程序员应该在输出代码的同时创造出3个岗位(手动狗头)。
回归正题,光有入参出参日志还不够,异常的捕获和抛出也需要记录。
接下来,我们先故意写错语法,看看控制台打印什么:
如图,只会记录入参以及控制台默认的报错信息,而默认的报错信息,是不会写入日志文件的。
再看看请求的返回数据:
如图,这里只会看到 “Internal server error”,其他什么信息都没有。
这样就会有隐患了,用户在使用过程中报错了,但是日志没有记录报错的原因,就无法统计影响范围,如果是简单的报错还好,如果涉及数据库各种事务或者并发问题,就很难追踪定位了,总不能一直看着控制台吧。
因此,我们需要捕获代码中未捕获的异常,并记录日志到 logs/errors
里,方便登录线上服务器,对错误日志进行筛选、排查。
1. 初探过滤器
Nest 不光提供了拦截器,也提供了过滤器,就代码结构而言,和拦截器很相似。
内置的异常层负责处理整个应用程序中的所有抛出的异常。当捕获到未处理的异常时,最终用户将收到友好的响应。
我们先新建一个 http-exception.filter
试试:
1 | 复制代码$ nest g filter http-exception filter |
打开文件,默认代码长这样:
1 | 复制代码// src/filter/http-exception.filter.ts |
可以看到,和拦截器的结构大同小异,也是接收 2 个参数,只不过用了 @Catch()
来修饰。
2. HTTP 错误的捕获
Nest提供了一个内置的 HttpException 类,它从 @nestjs/common 包中导入。对于典型的基于 HTTP REST/GraphQL API 的应用程序,最佳实践是在发生某些错误情况时发送标准 HTTP 响应对象。
HttpException 构造函数有两个必要的参数来决定响应:
- response 参数定义 JSON 响应体。它可以是 string 或 object,如下所述。
- status参数定义HTTP状态代码。
默认情况下,JSON 响应主体包含两个属性:
- statusCode:默认为 status 参数中提供的 HTTP 状态代码
- message:基于状态的 HTTP 错误的简短描述
我们先来编写捕获打印的逻辑:
1 | 复制代码// src/filter/http-exception.filter.ts |
上面代码表示如何捕获 HTTP 异常,并组装成更友好的信息返回给用户。
我们测试一下,先把注册接口的 Token 去掉,请求:
上图是还没有加过滤器的请求结果。
我们在 main.ts 中引入 http-exception
:
1 | 复制代码// src/main.ts |
使用全局过滤器 useGlobalFilters
调用 http-exception
,再请求:
再看控制台打印:
如此一来,就可以看到未带 Token 请求的结果了,具体信息的组装,可以根据个人喜好进行修改。
3. 内置HTTP异常
为了减少样板代码,Nest 提供了一系列继承自核心异常 HttpException 的可用异常。所有这些都可以在 @nestjs/common包中找到:
- BadRequestException
- UnauthorizedException
- NotFoundException
- ForbiddenException
- NotAcceptableException
- RequestTimeoutException
- ConflictException
- GoneException
- PayloadTooLargeException
- UnsupportedMediaTypeException
- UnprocessableException
- InternalServerErrorException
- NotImplementedException
- BadGatewayException
- ServiceUnavailableException
- GatewayTimeoutException
结合这些,可以自定义抛出的异常类型,比如后面的教程说到权限管理的时候,就可以抛出 ForbiddenException
异常了。
4. 其他错误的捕获
除了 HTTP 相关的异常,还可以捕获项目中出现的所有异常,我们新建 any-exception.filter
:
1 | 复制代码$ nest g filter any-exception filter |
一样的套路:
1 | 复制代码// src/filter/any-exception.filter.ts |
和 http-exception
的唯一区别就是 exception
的类型是 unknown
我们将 any-exception 引入 main.ts:
1 | 复制代码// src/main.ts |
注意:AllExceptionsFilter 要在 HttpExceptionFilter 的上面,否则 HttpExceptionFilter 就不生效了,全被 AllExceptionsFilter 捕获了。
然后,我们带上 Token (为了跳过 401 报错)再请求一次:
再看看控制台:
已经有了明显的区别,再看看 errors.log,也写进了日志中:
如此一来,代码中未捕获的错误也能从日志中查到了。
总结
本篇介绍了如何使用 log4js 来管理日志,制作中间件和拦截器对入参出参进行记录,以及使用过滤器对异常进行处理。
文中日志的打印格式可以按照自己喜好进行排版,不一定局限于此。
良好的日志管理能帮我们快速排查 Bug,减少加班,不做资本家的奴隶,把有限的精力投入到无限的可能上。
下一篇将介绍如何使用 DTO 对参数进行验证,解脱各种 if - else。
本篇收录于NestJS 实战教程,更多文章敬请关注。
参考资料:
`
本文转载自: 掘金