异步本地存储
根据 Node.js 官方文档:“AsyncLocalStorage 用于在回调和 Promise 链中创建异步状态。它允许在 Web 请求或任何其他异步持续期间存储数据,这类似于其他语言中的线程本地存储。”
简而言之,AsyncLocalStorage 允许你在使用 async 函数执行时存储一个状态,并使该状态对函数内部的所有代码路径都可用。
基础示例
让我们看看它的实际效果。首先,我们将创建一个全新的 Node.js 项目(不含任何依赖),并使用 AsyncLocalStorage 在模块之间共享状态,而无需通过引用传递它。
你可以在 als-basic-example GitHub 仓库中找到此示例的最终代码。
第 1 步. 创建一个新项目
npm init --yes
打开 package.json 文件,将模块系统设置为 ESM。
{
"type": "module"
}
第 2 步. 创建 AsyncLocalStorage 实例
创建一个名为 storage.js 的文件,在其中创建并导出一个 AsyncLocalStorage 实例。
import { AsyncLocalStorage } from 'async_hooks'
export const storage = new AsyncLocalStorage()
第 3 步. 在 storage.run 内部执行代码
创建一个名为 main.js 的入口文件。在此文件中,导入在 ./storage.js 文件中创建的 AsyncLocalStorage 实例。
storage.run 方法接受我们要共享的状态作为第一个参数,接受一个回调函数作为第二个参数。该回调函数内部的所有代码路径(包括被导入的模块)都可以访问同一个状态。
import { storage } from './storage.js'
import UserService from './user_service.js'
import { setTimeout } from 'node:timers/promises'
async function run(user) {
const state = { user }
return storage.run(state, async () => {
await setTimeout(100)
const userService = new UserService()
await userService.get()
})
}
为了演示,我们将不等待其完成而连续调用 run 方法三次。将以下代码粘贴到 main.js 文件末尾。
run({ id: 1 })
run({ id: 2 })
run({ id: 3 })
第 4 步. 从 user_service 模块访问状态
最后,让我们在 user_service 模块中导入存储实例并访问当前状态。
import { storage } from './storage.js'
export class UserService {
async get() {
const state = storage.getStore()
console.log(`The user id is ${state.user.id}`)
}
}
第 5 步. 执行 main.js 文件
让我们运行 main.js 文件,看看 UserService 能否访问到状态。
node main.js
为什么需要异步本地存储?
与 PHP 等其他语言不同,Node.js 不是一门基于线程的语言。在 PHP 中,每个 HTTP 请求都会创建一个新线程,每个线程拥有自己的内存。这让你可以将状态存储在全局内存中,并在代码库的任何位置访问它。
在 Node.js 中,你无法拥有一个在 HTTP 请求之间相互隔离的全局状态,因为 Node.js 运行在单线程上,并拥有共享内存。因此,所有 Node.js 应用都通过将其作为参数传递来共享数据。
通过引用传递数据在技术上没有任何缺点。但是,它确实会让代码变得冗长,尤其是当你配置 APM 工具并需要手动向其提供请求数据时。
用法
AdonisJS 在 HTTP 请求期间使用 AsyncLocalStorage,并将 HTTP 上下文 作为状态共享。因此,你可以在应用中全局访问 HTTP 上下文。
首先,你必须在 config/app.ts 文件中启用 useAsyncLocalStorage 标志。
export const http = defineConfig({
useAsyncLocalStorage: true,
})
启用后,你可以使用 HttpContext.get 或 HttpContext.getOrFail 方法获取当前进行中请求的 HTTP 上下文实例。
在以下示例中,我们在一个 Lucid 模型中获取上下文。
import { HttpContext } from '@adonisjs/core/http'
import { BaseModel } from '@adonisjs/lucid'
export default class Post extends BaseModel {
get isLiked() {
const ctx = HttpContext.getOrFail()
const authUserId = ctx.auth.user.id
return !!this.likes.find((like) => {
return like.userId === authUserId
})
}
}
注意事项
如果 ALS 能让你的代码更直观,并且你倾向于全局访问而非通过引用传递 HTTP 上下文,那么可以使用它。
但是,请注意以下情况,它们很容易导致内存泄漏或程序行为不稳定。
顶层访问
不要在任意模块的顶层访问 ALS,因为 Node.js 中的模块会被缓存。
错误用法
将 HttpContext.getOrFail() 方法的结果赋值给顶层变量,会持有首次导入该模块的请求的引用。
import { HttpContext } from '@adonisjs/core/http'
const ctx = HttpContext.getOrFail()
export default class UsersController {
async index() {
ctx.request
}
}
正确用法
相反,你应该将 getOrFail 方法的调用移到 index 方法内部。
import { HttpContext } from '@adonisjs/core/http'
export default class UsersController {
async index() {
const ctx = HttpContext.getOrFail()
}
}
静态属性内部
任何类的静态属性(非方法)在模块被导入时就会立即求值;因此,你不应在静态属性中访问 HTTP 上下文。
错误用法
import { HttpContext } from '@adonisjs/core/http'
import { BaseModel } from '@adonisjs/lucid'
export default class User extends BaseModel {
static connection = HttpContext.getOrFail().tenant.name
}
正确用法
相反,你应该将 HttpContext.get 调用移到方法内部,或者将该属性转换为 getter。
import { HttpContext } from '@adonisjs/core/http'
import { BaseModel } from '@adonisjs/lucid'
export default class User extends BaseModel {
static query() {
const ctx = HttpContext.getOrFail()
return super.query({ connection: tenant.connection })
}
}
事件处理器内部
事件处理器在 HTTP 请求结束后才执行。因此,你应避免尝试在它们内部访问 HTTP 上下文。
import emitter from '@adonisjs/core/services/emitter'
export default class UsersController {
async index() {
const user = await User.create({})
emitter.emit('new:user', user)
}
}
避免在事件监听器内部使用
import { HttpContext } from '@adonisjs/core/http'
import emitter from '@adonisjs/core/services/emitter'
emitter.on('new:user', () => {
const ctx = HttpContext.getOrFail()
})