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 请求期间发出以使其生效。

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/adonisjs/5.x/fu...

译文地址:https://learnku.com/docs/adonisjs/5.x/fu...

上一篇 下一篇
贡献者:3
讨论数量: 0
发起讨论 查看所有版本


暂无话题~