AsyncLocalStorage
根据 Node.js 官方文档:“AsyncLocalStorage 用于在回调和 promise 链中创建异步状态。*它允许在 Web 请求的整个生命周期或任何其他异步持续时间中存储数据。它类似于其他语言中的线程局部存储 *。
为了进一步简化解释,AsyncLocalStorage 允许你在执行异步函数时存储状态,然后使其可用于该函数中的所有代码路径。例如:
注:以下是一个虚构的例子。但是,你仍然可以通过创建一个空的 Node.js 项目来跟进。
首先创建一个AsyncLocalStorage
的实例并将其导出。这将允许多个模块访问同一个存储实例。
// 文件名: storage.ts
import { AsyncLocalStorage } from 'async_hooks'
export const storage = new AsyncLocalStorage()
创建 main 文件。它将使用 storage.run
方法以初始状态执行异步函数。
// 文件名: main.ts
import { storage } from './storage'
import ModuleA from './ModuleA'
async function run(id) {
const state = { id }
return storage.run(state, async () => {
await (new ModuleA()).run()
})
}
run(1)
run(2)
run(3)
最后, ModuleA
可以使用 storage.getStore()
方法获取状态。
// title: ModuleA.ts
import { storage } from './storage'
import ModuleB from './ModuleB'
export default class ModuleA {
public async run() {
console.log(storage.getStore())
await (new ModuleB()).run()
}
}
与 ModuleA
一样,ModuleB
也可以使用 storage.getStore
方法访问相同的状态。
换句话说,在 storage.run
方法调用期间,整个操作链可以访问初始化在 main.js
文件中的相同的状态。
异步本地存储需要什么?
与 PHP 等其他语言不同,Node.js 不是线程型语言。
在 PHP 中,每个 HTTP 请求都会创建一个新线程,每个线程都有自己的内存。这允许你将状态存储到全局内存中并在代码库中的任何位置访问它。
在 Node.js 中,你不能将数据保存到全局对象之后还能在 HTTP 请求之间保持隔离。因为 Node.js 是单线程运行并在所有 HTTP 请求中共享内存,所以这不可能实现。
因为 Node.js 不必为每个 HTTP 请求启动单独的应用程序,所以它具有极大的性能优势。
但是,这也意味着你必须将状态作为函数参数或类参数传递,因为你无法将其写入全局对象。比如:
http.createServer((req, res) => {
const state = { req, res }
await (new ModuleA()).run(state)
})
// Module A
class ModuleA {
public async run(state) {
await (new ModuleB()).run(state)
}
}
异步本地存储解决了这个问题,因为它允许多个异步操作之间进行状态隔离。
AdonisJS 如何使用 ALS?
ALS 代表 AsyncLocalStorage。 AdonisJS 在 HTTP 请求期间使用异步本地存储,并将 HTTP 上下文 设置为状态。代码类似于以下内容。
storage.run(ctx, () => {
await runMiddleware()
await runRouteHandler()
ctx.finish()
})
中间件和路由处理程序通常也运行其他操作。例如,使用模型来获取用户信息。
export default class UsersController {
public index() {
await User.all()
}
}
User
模型实例现在可以访问上下文,因为它们是在storage.run
方法的代码路径中创建的。
import HttpContext from '@ioc:Adonis/Core/HttpContext'
export default class User extends BaseModel {
public get isFollowing() {
const ctx = HttpContext.get()!
return this.id === ctx.auth.user.id
}
}
模型静态属性(不是方法)无法访问 HTTP 上下文,因为它们是在导入模型时进行评估的。所以你必须理解代码执行路径和谨慎使用ALS。
用法
要在你的应用程序中使用 ALS,你必须先在 config/app.ts
文件中启用它。如果该属性不存在,请手动创建该属性。
// 文件名: config/app.ts
export const http: ServerConfig = {
useAsyncLocalStorage: true,
}
启用后,你可以使用 HttpContext
模块在代码库中的任何位置访问当前 HTTP 上下文。
注:确保在 HTTP 请求过程中调用了代码路径以使
ctx
可用。否则,它将是null
。
import HttpContext from '@ioc:Adonis/Core/HttpContext'
class SomeService {
public async someOperation() {
const ctx = HttpContext.get()
}
}
应该如何使用?
此时,你可以将 Async Local Storage 视为具有特定请求的全局状态。 全局状态或变量通常被认为是不好的,因为它们使测试和调试变得更加困难。
如果你不小心访问了 HTTP 请求中的本地存储,Node.js 中的异步本地存储可能会变得更加棘手。
即使你可以访问异步本地存储,我们仍然建议你像之前那样编写代码(通过引用传递ctx
)。通过引用来传递数据可以明确执行路径,还能更轻松地单独测试你的代码。
那为什么要引入异步本地存储?
异步本地存储(ALS)与 APM 工具相得益彰,这些工具从你的应用程序中收集性能指标,以帮助你调试和查明问题。
在 ALS 之前,APM 工具不能轻易地将不同的资源与给定的 HTTP 请求相关联。例如,它可以显示执行给定 SQL 查询花费了多少时间,但无法告诉你执行该查询的 HTTP 请求。
现在无需你接触任何一行代码,借助 ALS,这一切都成为了可能。 AdonisJS 将使用 ALS 通过其应用程序级分析器收集指标。
使用 ALS 时的注意事项
如果你认为 ALS 使你的代码更简单,并且你更喜欢全局访问而不是通过引用传递所有内容,那么你可以随意使用 ALS。
但是,请注意以下容易导致内存泄漏或程序行为不稳定的情况。
顶级访问
永远不要在任何模块的顶层访问异步本地存储。例如:
❌ 失效
在 Node.js 中,模块会被缓存。因此 HttpContext.get()
方法将在第一个 HTTP 请求期间只执行一次,并在你的进程的生命周期中永远保持其 ctx
。
import HttpContext from '@ioc:Adonis/Core/HttpContext'
const ctx = HttpContext.get()
export default class UsersController {
public async index() {
ctx.request
}
}
✅ 生效
相反,你应该将 .get
调用移到 index
方法中。
export default class UsersController {
public async index() {
const ctx = HttpContext.get()
}
}
内部静态属性
任何类的静态属性(不是方法)都会在导入该模块后立即进行评估,因此你不应该在静态属性中访问 ctx
。
❌ 失效
在以下示例中,当你在控制器中导入 User
模型时,HttpContext.get()
代码将被执行并永久缓存。因此,要么你将收到 null
,要么你最终缓存的 tenant 连接来自于第一个请求。
import HttpContext from '@ioc:Adonis/Core/HttpContext'
export default class User extends BaseModel {
public static connection = HttpContext.get()!.tenant.connection
}
✅ 生效
相反,你应该将 HttpContext.get
调用移到 query
方法内。
import HttpContext from '@ioc:Adonis/Core/HttpContext'
export default class User extends BaseModel {
public static query() {
const ctx = HttpContext.get()!
return super.query({ connection: tenant.connection })
}
}
事件处理程序
在 HTTP 请求期间发出的事件的处理程序可以使用 HttpContext.get()
方法访问请求上下文。例如:
export default class UsersController {
public async index() {
const user = await User.create({})
Event.emit('new:user', user)
}
}
// 标题: Event handler
import HttpContext from '@ioc:Adonis/Core/HttpContext'
Event.on('new:user', () => {
const ctx = HttpContext.get()
})
但是,在通过事件处理程序访问上下文时,你应该注意几件事。
- 事件绝不能尝试使用
ctx.response.send()
发送响应,因为这不是事件的应该做的。 - 在事件处理程序中访问
ctx
使其依赖于 HTTP 请求。换句话说,该事件不再是通用的,并且应该始终在 HTTP 请求期间发出以使其生效。
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。