nestjs-cls 是 NestJS 生态中一个非常流行且强大的库,全称是 NestJS Continuation-Local Storage。
用一句话概括:它让你的 NestJS 应用拥有了"请求级别的全局变量"能力。
它的核心作用是解决 "参数透传" (Prop Drilling) 的痛苦,尤其是在处理异步操作(Async)的时候。
为了让你彻底明白,我们对比一下有它 和没它的区别:
1. 痛点:没有 CLS 时 (参数透传的地狱)
假设你有一个需求:在数据库保存数据时,自动记录是哪个 User ID 创建的。
你的调用链路是:Controller -> Service -> Repository。
- Controller : 从 Request 里拿到
userId,传给 Service。 - Service : 拿到
userId,做业务逻辑,再传给 Repository。 - Repository : 拿到
userId,写库。
代码会变成这样:
// 每一层都要被迫接收 userId 这个参数,哪怕中间层根本不需要用它,只是为了传给下一层
// Controller
createOrder(user) {
this.service.create(user.id, data);
}
// Service
create(userId, data) {
// Service 层其实不需要 userId,但必须接住它传给 Repo
this.repo.save(userId, data);
}
// Repository
save(userId, data) {
db.insert({ ...data, createdBy: userId });
}
问题: 如果调用链有 10 层,你就得改 10 个函数签名,把 userId 一层层传下去。这叫"参数透传",非常难维护。
2. 解决方案:使用 nestjs-cls (隐形背包)
nestjs-cls 基于 Node.js 的 AsyncLocalStorage 技术。它允许你在请求刚进来的时候(比如在 Middleware 或 Guard 里),把 userId 放进一个**"隐形的上下文背包"**里。
在这个请求处理的任何地方、任何层级 (Service, Repository, 甚至工具函数),你都可以直接把手伸进背包里拿到这个 userId,而不需要通过函数参数传递。
代码变得很干净:
// 1. 在拦截器/中间件里 (请求刚进来)
cls.set('userId', req.user.id); // 放入背包
// ... 中间层函数完全不需要传 userId 参数 ...
// 2. 在深层的 Repository 里 (想用就直接拿)
save(data) {
// 直接从空气中(上下文)拿到 userId
const userId = this.cls.get('userId');
db.insert({ ...data, createdBy: userId });
}
3. 它为什么安全?(并发安全)
你可能会问:"这不就是全局变量吗?Node.js 是异步的,多个请求同时进来,会不会张三拿到了李四的 ID?"
不会。 这正是 nestjs-cls (以及底层的 AsyncLocalStorage) 的核心黑科技。
- 它能保证数据隔离。
- 请求 A 的上下文,只有请求 A 的后续逻辑能访问。
- 请求 B 进来,会创建一个全新的上下文,互不干扰。
4. 常见的使用场景
除了上面的 UserID 例子,这些场景必用 nestjs-cls:
- Trace ID / Request ID (全链路追踪):
-
- 请求进来生成一个 UUID,存入 CLS。
- 在代码任何地方打 Log 时,自动从 CLS 取出这个 ID 附带在日志里。这样查日志时,能把属于同一个请求的所有日志串起来。
- 多租户系统 (Multi-tenancy):
-
- 请求进来判断是哪个公司(Tenant),存入 CLS。
- 在数据库查询层,自动从 CLS 读取 TenantID,强制加上
WHERE tenant_id = ...,防止数据越权。
- 数据库事务 (Transactions):
-
- 把数据库的
Transaction Manager存入 CLS。 - 这样 Service 层不需要把事务对象传给 Repository,Repository 自动从 CLS 获取当前的事务上下文进行操作。
- 把数据库的
总结
看到 import { ClsModule } from 'nestjs-cls';,你就知道这个项目使用了上下文管理技术。
它在干的事就是:"帮你在整个请求的处理周期内,偷偷带着一些数据(如用户ID、追踪ID),让你在任何地方都能随时取用,而不用把这些数据写在函数参数里传来传去。"