当您听到“异步本地存储”这个短语时,您会想到什么?您最初可能认为它指的是基于浏览器的本地存储的某种神奇实现。然而,这个假设是不正确的。异步本地存储既不与浏览器相关,也不是典型的存储机制。您可能使用过的一两个库在幕后使用它。在很多情况下,这个功能可以让你免于处理混乱的代码。
异步本地存储是 Node.js 中引入的功能,最初在 v13.10.0 和 v12.17.0 版本中添加,后来在 v16.4.0 中稳定下来。它是 async_hooks 模块的一部分,该模块提供了一种跟踪 Node.js 应用程序中的异步资源的方法。该功能允许创建多个异步函数可以访问的共享上下文,而无需显式传递它。上下文在传递给 AsyncLocalStorage 实例的 run() 方法的回调中执行的每个(也是唯一)操作中可用。
在深入示例之前,让我们先解释一下我们将使用的模式。
初始化
import { AsyncLocalStorage } from "async_hooks"; import { Context } from "./types"; export const asyncLocalStorage = new AsyncLocalStorage<Context>(); // export const authAsyncLocalStorage = new AuthAsyncLocalStorage<AuthContext>()
在上面的模块中,我们初始化 AsyncLocalStorage 的实例并将其导出为变量。
用法
asyncLocalStorage.run({ userId }, async () => { const usersData: UserData = await collectUsersData(); console.log("usersData", usersData); }); // (method) AsyncLocalStorage<unknown>.run<Promise<void>>(store: unknown, callback: () => Promise<void>): Promise<void> (+1 overload)
run() 方法有两个参数:存储(包含我们想要共享的数据)和回调(我们放置逻辑的位置)。因此,回调中的每个函数调用都可以访问存储,从而允许跨异步操作无缝共享数据。
async function collectUsersData() { const context = asyncLocalStorage.getStore(); }
要访问上下文,我们导入实例并调用 asyncLocalStorage.getStore() 方法。最棒的是,从 getStore() 检索的存储是类型化的,因为我们在初始化期间将 Context 类型传递给了 AsyncLocalStorage: new AsyncLocalStorage
没有身份验证系统的 Web 应用程序。我们必须验证身份验证令牌并提取用户信息。一旦我们获得了用户身份,我们希望使其在路由处理程序中可用,并避免在每个处理程序中重复代码。让我们看看如何利用 AsyncLocalStorage 来实现身份验证上下文,同时保持代码整洁。
我在这个例子中选择了 fastify。
根据文档 fastify 是:
快速且低开销的 Web 框架,适用于 Node.js
好的,让我们开始吧:
import { AsyncLocalStorage } from "async_hooks"; import { Context } from "./types"; export const asyncLocalStorage = new AsyncLocalStorage<Context>(); // export const authAsyncLocalStorage = new AuthAsyncLocalStorage<AuthContext>()
asyncLocalStorage.run({ userId }, async () => { const usersData: UserData = await collectUsersData(); console.log("usersData", usersData); }); // (method) AsyncLocalStorage<unknown>.run<Promise<void>>(store: unknown, callback: () => Promise<void>): Promise<void> (+1 overload)
async function collectUsersData() { const context = asyncLocalStorage.getStore(); }
npm install fastify
现在到了非常重要的部分。我们将添加一个 onRequest 钩子来使用 authAsyncLocalStorage.run() 方法包装处理程序。
type Context = Map<"userId", string>;
成功验证后,我们从 authAsyncLocalStorage 调用 run() 方法。作为存储参数,我们传递身份验证上下文以及从令牌中检索到的 userId。在回调中,我们调用done函数来继续Fastify生命周期。
如果我们有需要异步操作的身份验证检查,我们应该将它们添加到回调中。这是因为,根据文档:
使用 async/await 或返回 Promise 时,done 回调不可用。如果您在这种情况下调用完成回调,则可能会发生意外行为,例如处理程序的重复调用
这是一个示例:
import { AsyncLocalStorage } from "async_hooks"; import { Context } from "./types"; export const authAsyncLocalStorage = new AsyncLocalStorage<Context>();
我们的示例只有一条受保护的路线。在更复杂的场景中,您可能需要仅使用身份验证上下文包装特定路由。在这种情况下,您可以:
好吧,我们的上下文已经设置完毕,我们现在可以定义一条受保护的路由:
import Fastify from "fastify"; /* other code... */ const app = Fastify(); function sendUnauthorized(reply: FastifyReply, message: string) { reply.code(401).send({ error: `Unauthorized: ${message}` }); } /* other code... */
代码非常简单。我们导入 authAsyncLocalStorage,检索 userId,初始化 UserRepository 并获取数据。这种方法使路由处理程序保持干净和专注。
在此示例中,我们将重新实现 Next.js 中的 cookies 助手。但是等等,这是一篇关于 AsyncLocalStorage 的文章,对吗?那么我们为什么要谈论cookie呢?答案很简单:Next.js 使用 AsyncLocalStorage 来管理服务器上的 cookie。这就是为什么在服务器组件中读取 cookie 如此简单:
import Fastify from "fastify"; import { authAsyncLocalStorage } from "./context"; import { getUserIdFromToken, validateToken } from "./utils"; /* other code... */ app.addHook( "onRequest", (request: FastifyRequest, reply: FastifyReply, done: () => void) => { const accessToken = request.headers.authorization?.split(" ")[1]; const isTokenValid = validateToken(accessToken); if (!isTokenValid) { sendUnauthorized(reply, "Access token is invalid"); } const userId = accessToken ? getUserIdFromToken(accessToken) : null; if (!userId) { sendUnauthorized(reply, "Invalid or expired token"); } authAsyncLocalStorage.run(new Map([["userId", userId]]), async () => { await new Promise((resolve) => setTimeout(resolve, 2000)); sendUnauthorized(reply, "Invalid or expired token"); done(); }); }, ); /* other code... */
我们使用next/headers导出的cookies函数,它提供了多种管理cookies的方法。但这在技术上怎么可能呢?
首先,我想提一下,这个示例是基于我从 Lee Robinson 的精彩视频以及深入研究 Next.js 存储库中获得的知识。
在此示例中,我们将使用 Hono 作为我们的服务器框架。我选择它有两个原因:
首先安装Hono:
import { AsyncLocalStorage } from "async_hooks"; import { Context } from "./types"; export const asyncLocalStorage = new AsyncLocalStorage<Context>(); // export const authAsyncLocalStorage = new AuthAsyncLocalStorage<AuthContext>()
现在,初始化Hono并添加中间件:
asyncLocalStorage.run({ userId }, async () => { const usersData: UserData = await collectUsersData(); console.log("usersData", usersData); }); // (method) AsyncLocalStorage<unknown>.run<Promise<void>>(store: unknown, callback: () => Promise<void>): Promise<void> (+1 overload)
代码类似于 Fastify 示例中的中间件,不是吗?为了设置上下文,我们使用 setCookieContext,它是从 cookies 模块导入的 - 我们自定义的 cookies 函数的简单实现。让我们跟随 setCookieContext 函数并导航到导入它的模块:
async function collectUsersData() { const context = asyncLocalStorage.getStore(); }
setCookieContext 函数(其返回值我们传递给 Hono 中间件中的 cookieAsyncLocalStorage.run())从代表 hono 上下文的 c 参数中提取 cookie,并将它们与提供用于管理 cookie 的实用函数的闭包捆绑在一起。
我们的 cookie 功能复制了 next/headers 中 cookie 的功能。它利用 cookieAsyncLocalStorage.getStore() 方法来访问调用时传递给 cookieAsyncLocalStorage.run() 的相同上下文。
我们将 cookies 函数的返回包装在一个承诺中,以模仿 Next.js 实现的行为。在版本 15 之前,此函数是同步的。现在,在当前的 Next.js 代码中,cookie 返回的方法被附加到一个 Promise 对象,如以下简化示例所示:
npm install fastify
值得一提的另一点是,在我们的例子中,使用 cookies.setCookie 和 cookies.deleteCookie 总是会抛出错误,类似于在服务器组件中设置 cookie 时在 Next.js 中观察到的行为。我们硬编码了这个逻辑,因为在原来的实现中,我们是否可以使用setCookie或deleteCookie取决于存储在称为RequestStore的存储中的phase(WorkUnitPhase)属性(这是AsyncLocalStorage的实现,也存储cookie)。然而,这个主题更适合另一篇文章。为了使这个示例简单,我们省略 WorkUnitPhase 的模拟。
现在我们需要添加 React 代码。
type Context = Map<"userId", string>;
import { AsyncLocalStorage } from "async_hooks"; import { Context } from "./types"; export const authAsyncLocalStorage = new AsyncLocalStorage<Context>();
cookie 的用法与 Next.js React 服务器组件中的用法类似。
import Fastify from "fastify"; /* other code... */ const app = Fastify(); function sendUnauthorized(reply: FastifyReply, message: string) { reply.code(401).send({ error: `Unauthorized: ${message}` }); } /* other code... */
我们的模板是通过 hono 上下文中的 html 方法渲染的。这里的关键点是路由处理程序在 asyncLocalStorage.run() 方法中运行,该方法采用 cookieContext。这样一来,我们就可以在 DisplayCookies 组件中通过 cookies 函数来访问这个上下文了。
不可能在React服务器组件中设置cookie,所以我们需要手动设置:
让我们刷新一下页面:
现在,我们的 cookie 已成功检索并显示。
asyncLocalStorage 还有更多用例。此功能允许您在几乎任何服务器框架中构建自定义上下文。 asyncLocalStorage 上下文封装在 run() 方法的执行中,使其易于管理。它非常适合处理基于请求的场景。该 API 简单而灵活,通过为每个状态创建实例来实现可扩展性。可以无缝地维护身份验证、日志记录和功能标志等单独的上下文。
尽管它有很多好处,但仍有一些注意事项需要牢记。我听说 asyncLocalStorage 在代码中引入了太多“魔力”。我承认,当我第一次使用此功能时,我花了一些时间才完全掌握这个概念。另一件需要考虑的事情是将上下文导入模块会创建一个您需要管理的新依赖项。然而,最终,通过深度嵌套的函数调用传递值要糟糕得多。
感谢您的阅读,我们下一篇文章见!?
PS:您可以在这里找到示例(另加一个奖励)
博客帖子来源:https://www.aboutjs.dev/en/async-local-storage-is-here-to-help-you
以上是异步本地存储可以为您提供帮助的详细内容。更多信息请关注PHP中文网其他相关文章!