Nest.js 最佳实践:异步上下文(Context)实现自动填充
在开发企业级 Web
应用的时候,经常会在设计数据表的时候设计一些 createBy
或者 updateBy
这种类似的字段,有使用 Java
开发过 Web
的朋友都应该知道能够使用 ThreadLocal
这种类似的方案能够在一个完整的请求当中存储上下文信息,确保同一 HTTP 请求可以随时"拿到"当前登录用户。
本篇文章是基于 Node.js 14 以后提供的 AsyncLocalStorage 以及 nestjs-cls 开源库来实现一个请求过程当中自动填充数据的教程。
一、初始化 Nest.js 项目
创建 Nest.js 项目
bash
npm i -g @nestjs/cli
nest new nest-context-demo
cd nest-context-demo
这里我直接使用自己搭建的一个脚手架项目,Github 地址是: github.com/DimplesY/ne... 。
当然也希望能够给个 star 支持一下 🎉。
使用如下命令即可
bash
npx degit DimplesY/nest-api-starter nest-context-demo

创建数据库容器
进入到项目目录,使用下面的 docker compose
命令启动一个 postgres
bash
cd nest-context-demo
docker compose up postgres -d

并且在项目的根目录下创建一个 .env 文件,贴入下面的内容
shell
DATABASE_URL=postgres://postgres:123456@localhost:5432/api
REDIS_PASSWORD=123456
随后使用 pnpm install
安装项目的依赖。
使用 pnpm run db:push
命令将我们项目内的数据表同步到数据库当中,如下图所示。

然后执行pnpm run db:studio
,用浏览器打开 local.drizzle.studio/ ,看到如下界面即表示我们的数据表都成功的创建到了数据库当中。

因为本教程主要教的内容是实现一个请求内共享的上下文,就不再对 drizzle-orm
进行过多的介绍,如果你想学习,可以阅读官方文档: orm.drizzle.team/docs/overvi...
创建一个 test
用户,并且密码为 123456
,如下所示。

密码生成工具在
@/common/utils/password.util.ts
,你可以调用来生成其他密码。
在还没介绍 nest-cls
之前,我们先对 app.module.ts
进行一下修改,代码如下,直接将 nest-cls
相关的代码注释掉,代码如下。
ts
// src/app.module.ts
import { Module } from '@nestjs/common';
import { LoggerModule } from '@/logger/logger.module';
import { ConfigModule } from '@nestjs/config';
import { DrizzleModule } from './drizzle/drizzle.module';
// import { ClsModule } from 'nestjs-cls';
import { AuthModule } from './modules/auth/auth.module';
@Module({
imports: [
// ClsModule.forRoot({
// middleware: {
// mount: true,
// },
// global: true,
// }),
DrizzleModule,
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
LoggerModule,
AuthModule,
],
})
export class AppModule {}
ts
// /src/common/guards/auth.guard.ts
import { AuthService } from '@/modules/auth/auth.service';
import {
CanActivate,
ExecutionContext,
HttpStatus,
Injectable,
} from '@nestjs/common';
// import { ClsService } from 'nestjs-cls';
import { BizException } from '@/common/exceptions/biz.exception';
import { DrizzleService } from '@/drizzle/database.provider';
import {
ExpressBizRequest,
getNestExecutionContextRequest,
} from '../transformers/get-req.transformer';
import { UserSelectType } from '@/drizzle/schema/users';
function isJWT(token: string): boolean {
const parts = token.split('.');
return (
parts.length === 3 &&
/^[a-zA-Z0-9_-]+$/.test(parts[0]) &&
/^[a-zA-Z0-9_-]+$/.test(parts[1]) &&
/^[a-zA-Z0-9_-]+$/.test(parts[2])
);
}
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly authService: AuthService,
private readonly drizzle: DrizzleService,
// private readonly cls: ClsService,
) {}
async canActivate(context: ExecutionContext): Promise<any> {
const request = this.getRequest(context);
const query = request.query as Record<string, any>;
const headers = request.headers;
const Authorization: string =
headers.authorization ||
(headers.Authorization as string) ||
(query.token as string);
if (!Authorization) {
throw new BizException(HttpStatus.UNAUTHORIZED, '请登录');
}
const jwt = Authorization.replace(/[Bb]earer /, '');
if (!isJWT(jwt)) {
throw new BizException(HttpStatus.UNAUTHORIZED, '请登录');
}
const payload = await this.authService.verifyToken(jwt);
if (!payload.id) {
throw new BizException(HttpStatus.UNAUTHORIZED, '请登录');
}
const user = await this.drizzle.db.query.users.findFirst({
where: (user, { eq }) => eq(user.id, payload.id),
});
if (!user) {
throw new BizException(HttpStatus.UNAUTHORIZED, '请登录');
}
this.attachUserAndToken(request, user, Authorization);
// this.cls.set('user', payload);
return true;
}
private getRequest(context: ExecutionContext) {
return getNestExecutionContextRequest(context);
}
private attachUserAndToken(
request: ExpressBizRequest,
user: UserSelectType,
token?: string,
) {
request.user = user;
request.token = token;
}
}
接下来我们需要自己动手实现一个存储上下文信息的工具,创建 context/user-context.ts
文件,代码如下:
ts
// context/user-context.ts
import { AsyncLocalStorage } from 'async_hooks';
export interface UserContext {
id?: number;
}
export const userAsyncStorage = new AsyncLocalStorage<UserContext>();
创建一个拦截器 common/interceptors/user-context.interceptor.ts
ts
import { userAsyncStorage } from '@/context/user-context';
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { getNestExecutionContextRequest } from '../transformers/get-req.transformer';
@Injectable()
export class UserContextInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
const request = getNestExecutionContextRequest(context);
const user = request.user;
return userAsyncStorage.run(user ? { id: user.id } : {}, () =>
next.handle(),
);
}
}
在全局注册上面定义的 user-context.interceptor.ts
,main.ts
代码如下:
ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from '@/app.module';
import { APP_PORT } from '@/app.config';
import { WinstonModule } from 'nest-winston';
import { logger } from '@/logger/logger.global';
import { UserContextInterceptor } from './common/interceptors/user-context.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: WinstonModule.createLogger({ instance: logger }),
});
app.useGlobalInterceptors(new UserContextInterceptor());
await app.listen(APP_PORT);
}
void bootstrap();
接下来我们再来创建一些工具函数,方便我们在上下文当中获取信息。
ts
// context/current-user.ts
import { userAsyncStorage } from './user-context';
export function getCurrentUser() {
return userAsyncStorage.getStore();
}
OK,到这里我们已经实现了一个自己的存储异步上下文的工具,能够在一个请求当中随时获取当前用户的信息。
效果展示
创建一个接口来测试一下,修改 auth.controller.ts
,添加下面的代码。
ts
@Auth()
@Get('/profile')
handleProfile() {
const user = getCurrentUser();
return user;
}
先登录一下,获得一个用于用户信息认证的 token,如下图所示(我是用的 apifox)。

在测试一下我们刚刚新增个接口,如下图所示,可以看到我们已经成功的拿到了用户的 id 信息。

至此,我们已经成功的实现了一个类似 Java
里面 ThreadLocal
的功能,能够在一个请求的处理函数的整个过程当中直接获取用户的 id,当然你也可以存储其他信息。
在正式项目当中还是直接使用项目已经集成的 nest-cls
,原理在本篇教程当中实现的都大差不差。
nest-cls
官方文档地址:papooch.github.io/nestjs-cls/