ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

vinext 并发请求隔离架构解析:基于 AsyncLocalStorage 的两层作用域模型

vinext 并发请求隔离架构解析:基于 AsyncLocalStorage 的两层作用域模型 后端Web框架SSR【免费下载链接】vinextVite plugin that reimplements the Next.js API surface — deploy anywhere项目地址https://gitcode.com/gh_mirrors/vi/vinext点击查看免费下载导读本文深入讲解 vinext一个可部署到任意平台的 Vite 插件重实现了 Next.js API 表面如何基于 Node.js 的AsyncLocalStorageALS为每个请求维护相互隔离的状态。你将理解为什么没有 ALS 时headers()、cookies()、Head子元素、缓存标签等会在并发请求间相互“串台”掌握统一请求上下文per-request与每次缓存调用作用域per-call两层模型的划分逻辑、shim 注册模式以及如何在该架构上安全地新增一类请求级状态。一、背景并发请求下的状态泄漏问题vinext 的目标是让 Next.js 风格的 APIheaders、cookies、缓存、导航、SSR 等运行在支持并发请求的运行时上——尤其是 Cloudflare Workers 这类在同一隔离实例内并发处理多个请求的环境。与 Node.js 传统服务端的每请求单线程栈不同Workers 中多个请求的处理过程会交错执行。如果在模块顶层用单个全局变量保存当前请求的 headers或当前 SSR 收集到的Head子元素那么并发请求 A 与请求 B 会互相覆盖、互相读取对方的状态导致请求 A 读取到请求 B 的 Cookie请求 B 的页面head里出现请求 A 的标题标签缓存标签、动态渲染标记等跨请求串线缓存命中与失效逻辑错乱。AsyncLocalStorage正是为这类问题设计的标准方案通过als.run(store, fn)在某个异步执行上下文中挂载状态fn及其所有异步延续async continuations都能通过als.getStore()读取同一份状态而不同调用树之间的状态天然隔离。vinext 的完整设计记录在 ALS-ARCHITECTURE.md本文以它为骨架结合 packages/vinext/src/shims 下的真实实现进行纵深展开。二、两层作用域模型总览vinext 的 ALS 设计不是一个请求一个 ALS的朴素实现而是把作用域分成两层对应两种不同的生命周期粒度层级生命周期粒度实现存放内容统一请求上下文每个请求一个per-requestunified-request-context.ts中单个AsyncLocalStorageUnifiedRequestContext该请求的全部扁平化状态每调用缓存作用域每次缓存函数调用一个per-callcacheContextStoragecache-runtime.ts与_unstableCacheAlscache.ts单个use cache/unstable_cache()调用内的标签与生命周期收集历史上 vinext 曾为 App Router 请求嵌套 56 层独立的 ALS 作用域headers、navigation、cache-state、private-cache、fetch-cache、execution-context 各一个。PR #450 将这些作用域合并为一个扁平的统一请求上下文大幅降低了嵌套作用域的复杂度与出错概率。这一点在 unified-request-context.ts 的模块头注释中有明确记录。三、第一层统一请求上下文per-request3.1 扁平的UnifiedRequestContext结构一个AsyncLocalStorageUnifiedRequestContext以扁平对象的形式持有请求的全部状态。类型定义位于 unified-request-context.ts通过交叉类型把多个 shim 的状态切片组合在一起export type UnifiedRequestContext { executionContext: ExecutionContextLike | null; // request-context.ts requestCache: WeakMap(...args: any[]) any, unknown; // cache-for-request.ts afterContext: AfterRequestContext; // next/server after() } VinextHeadersShimState I18nState NavigationState CacheState PrivateCacheState FetchCacheState RouterState HeadState RootParamsState;各状态切片对应字段及来源模块与文档表格一致并补充了源码中的默认值字段含义来源模块默认值见createRequestContext()headersContext请求/响应 headers、cookiesheaders.tsnulldynamicUsageDetected、phase动态渲染状态navigation-state.tsfalse/renderpendingSetCookies、draftModeCookieHeaderCookie 变更headers.ts[]/nulli18nContext地区locale信息i18n-state.tsnullserverContext、serverInsertedHTMLCallbacksRSC 服务端上下文navigation-state.tsnull/[]requestScopedCacheLife请求级缓存生命周期覆盖cache-request-state.tsnull_privateCache请求级私有缓存 Mapcache-runtime.tsnullcurrentRequestTags重新验证标签fetch-cache.ts[]executionContextCloudflare Workers 执行上下文request-context.ts从独立 ALS 继承ssrContextPages Router 的 SSR 上下文供useRouter()router-state.tsnullssrHeadChildren、documentInitialHeadSSR 期间收集的Head子元素head-state.ts[]rootParams根参数root-params.tsnull此外还包含afterContextnext/server的after()延迟任务生命周期callbacks、responseClosed、pendingCallbacks、pendingPromises、completion、resolveCompletion以及大量 fetch 缓存相关字段cacheableFetchUrls、currentFetchSoftTags、currentFetchCacheMode、dynamicFetchUrls、nextFetchId等完整默认值可在 createRequestContext() 中查证。所有状态类型集中在 request-state-types.ts 统一导出。3.2 生命周期创建与运行统一上下文在每个请求开始时创建随后整个请求执行包括 React 渲染都运行在该作用域内import { createRequestContext, runWithRequestContext } from vinext/unified-request-context; // 每个请求开始 const ctx createRequestContext({ /* 可按需预填充部分字段 */ }); // 整个请求体含所有异步延续 await runWithRequestContext(ctx, async () { // React 渲染、headers()、cookies()、缓存读写……都读取同一个 ctx });runWithRequestContext(ctx, fn)的底层实现就是_als.run(ctx, fn)unified-request-context.ts它保证fn内部所有异步操作await、setTimeout、Promise.all等都延续同一份 store。在 vinext 的服务端代码中createRequestContext/runWithRequestContext被页面边界、路由分发等入口调用例如 app-page-dispatch.ts、app-page-boundary.ts、app-rsc-handler.ts 与 pages-page-handler.ts说明这套机制同时服务 App Router 与 Pages Router 两条渲染管线。3.3 跨环境共享globalThisSymbol.for一个关键工程细节是Vite 的多环境架构RSC / SSR / client以及 HMR 可能让同一个源码模块以多个不同 specifier 加载出多个模块实例。如果每个实例都new AsyncLocalStorage()请求状态会在实例间“分叉”——一个环境里headers()写入的状态另一个环境的connection()读不到。为此所有 ALS 实例都存放在globalThis上通过Symbol.for(...)全局符号注册表寻址unified-request-context.tsconst _REQUEST_CONTEXT_ALS_KEY Symbol.for(vinext.requestContext.als); const _als getOrCreateAlsUnifiedRequestContext(vinext.unifiedRequestContext.als);Symbol.for(key)在全局符号注册表中返回同一个符号globalThis[sym]是所有模块实例共享的同一个槽位配合??仅当槽位为空时赋值保证第一个调用者创建、后续所有调用者复用同一个 ALS。这条模式被封装为 als-registry.ts 中的getOrCreateAlsT(key)约定 key 使用vinext.xxx.als点号命名空间。该注册表还有两个重要衍生机制NoopAsyncLocalStorage 兜底在浏览器/client bundle 中node:async_hooks会解析为无构造函数的 stub如 Vite 的__vite-browser-external直接new AsyncLocalStorage()会在模块求值期抛错。getOrCreateAls检测到构造函数不可用时返回一个 no-op 实现——getStore()恒返回undefined让 shim 自然回退到非 ALS 代码路径run/exit等仍正常调用回调als-registry.ts。这镜像了 Next.js 的FakeAsyncLocalStorage设计。runOutsideRequestScopes(fn)注册表维护了所有已创建 ALS 的集合可以一次性把所有请求作用域全部exit掉供模块级一次性求值等场景使用——因为动态import()会把 ALS 传播进被导入模块的顶层求值若不退出第一个请求的状态会泄漏进模块作用域并残留到后续所有请求als-registry.ts。3.4 嵌套子作用域runWithUnifiedStateMutation统一上下文并不排斥嵌套。runWithUnifiedStateMutation(mutate, fn)从当前 store 浅拷贝出一个子上下文允许重置或覆盖某一片状态后运行fn同时保持子作用域内创建的异步延续的正确隔离unified-request-context.tsconst childCtx { ...parentCtx }; // 浅拷贝 mutate(childCtx); // 例如 childCtx.currentRequestTags [] return _als.run(childCtx, fn);使用上有严格约定引用类型字段数组、Set、Map、对象必须替换如ctx.currentRequestTags []而不是原地修改如ctx.currentRequestTags.push(...)否则父作用域也会观察到变更。requestCacheWeakMap 特意保持共享——同一请求内的嵌套作用域应当看到相同的缓存值。head-state.ts与router-state.ts的runWithHeadState/runWithRouterState就是它的典型消费者见下文第四节。四、第二层每次缓存调用的作用域per-call有两类 ALS 刻意保持独立于统一上下文因为它们的作用域不是整个请求而是请求内某一次函数调用4.1cacheContextStorageuse cache运行时定义于 cache-runtime.tsexport const cacheContextStorage getOrCreateAlsCacheContext(vinext.cacheRuntime.contextAls);CacheContextcache-runtime.ts收集本次缓存函数执行中的tags—— 执行期间通过cacheTag()声明的标签lifeConfigs—— 通过cacheLife()收集的生命周期配置variant—— 缓存变体default/remote/privatereadRootParamNames、hasExplicitRevalidate、hasExplicitExpire、dynamicNestedCacheError、invalidDynamicUsageError等。标记use cache的函数会被vinext:use-cacheVite 插件改写为调用registerCachedFunction()包装。包装后的调用链路registerCachedFunction→runCachedFunctionWithContext会用cacheContextStorage.run(ctx, async () fn(...args))包裹真实函数执行cache-runtime.ts并且通过_registerCacheContextAccessor注册访问器让cacheLife()/cacheTag()无需直接 import避免循环依赖即可访问当前上下文。一个请求可以同时运行多个缓存函数每个都需要独立的标签/生命周期收集。如果把这个作用域合并进统一上下文就需要为每次缓存调用管理嵌套作用域独立的 ALS 实现更简单也更正确。4.2_unstableCacheAlsunstable_cache()定义于 cache.tsconst _unstableCacheAls getOrCreateAlsboolean(vinext.unstableCache.als);每次unstable_cache()调用时运行_unstableCacheAls.run(true, () fn(...args))cache.ts只携带一个布尔true标记。它的作用是让headers()、cookies()、connection()能检测到自己身处缓存作用域内并抛出异常——动态 API 不允许在缓存作用域中使用。通过isInsideUnstableCacheScope()返回_unstableCacheAls.getStore() truecache.ts即可完成该检测。它只是一个布尔标志没有需要合并的状态。4.3 为什么这两层必须分离文档给出了精辟的总结统一上下文是 per-request缓存作用域是 per-call。缓存作用域嵌套在请求内部但绑定到具体的缓存函数调用——一个请求可能同时运行零个、一个或多个缓存函数每个函数需要隔离的标签/生命周期追踪。混在一起会让每次缓存的收集和整个请求的共享状态两个不同生命周期纠缠不清。值得补充的是use cache运行时还借助workUnitAsyncStoragework-unit-async-storage.ts标记工作单元类型cache/private-cache并在缓存 MISS 后将effectiveLifeminimum-wins 规则合并后的生命周期与标签向上冒泡到父缓存作用域与请求级 store驱动revalidateTag的按标签失效issue #1453 相关逻辑见 cache-runtime.ts 的propagateCacheTagsToRequest。五、shim 注册模式以head-state.ts与router-state.ts为例每个 shim 模块都遵循统一的基础 shim 状态模块 注册访问器三步模式。以Head状态为例基础 shim 提供回退状态与注册入口head.ts持有模块级回退状态并暴露_registerHeadStateAccessors()注册函数状态模块注册 ALS 背书的访问器head-state.ts 导入统一上下文注册getSSRHeadChildren()、resetSSRHead()、getDocumentInitialHead()、setDocumentInitialHead()等访问器状态模块判断作用域归属_getState()中先检查isInsideUnifiedScope()——在统一作用域内就从统一 store 读取否则回退到独立的vinext.head.als或模块级 fallbackhead-state.tsfunction _getState(): HeadState { if (isInsideUnifiedScope()) { return getRequestContext(); // 统一 store } return _als.getStore() ?? _fallbackState; // 独立 ALS / 回退单例 }这个统一优先、独立回退的模式非常关键它让同一个 shim 在 App Router 统一作用域、Pages Router 独立作用域以及测试环境无任何作用域下都能正确工作。router-state.ts的结构完全一致为 Pages Router 的useRouter()提供请求级隔离的ssrContextpathname、query、asPath、locale 等并通过registerRoutePatternForWarningAccessor把 SSR 路由模式发布给 Link shim 的重复斜杠告警使用router-state.ts。dev 与 prod 的加载差异devVite 为不同环境node 与 ssr维护独立的模块图。状态模块必须在每个使用它的环境中加载dev server 通过ModuleImporter接口调用runner.import(vinext/head-state)确保注册发生在 ssr 模块图中prod打包把所有内容折叠进单一模块图注册通过静态 import 自然完成。这意味着在 dev 下由 React 组件在 SSR 期间访问的状态模块必须显式经ModuleImporter加载仅 node 侧使用的状态则不需要。六、如何新增一类请求作用域状态六步清单文档给出的操作步骤与源码一一对应完整整理如下在UnifiedRequestContext中新增字段unified-request-context.ts 的类型定义中加上字段在 request-state-types.ts 中导出新类型保持类型来源集中在createRequestContext()中设置默认值unified-request-context.ts并同步更新runWithUnifiedStateMutation的引用类型字段需替换而非原地修改清单注释在 shim 中通过isInsideUnifiedScope()读写作用域内读统一 store作用域外回退到独立 ALS / fallback若该状态在 dev 的 SSR 期间被 React 组件访问在 dev-server.ts 中通过ModuleImporter接口调用runner.import()加载状态模块仅 node 侧使用的状态不需要若该状态是 per-call 而非 per-request如缓存作用域请把它放在独立的 ALS 中不要塞进统一上下文。遵循这份清单新状态就能自动获得并发请求隔离、跨 Vite 环境共享、dev/prod 一致行为、与既有 shim 相同的注册与回退语义。七、小结架构取舍一个扁平 store 优于多层嵌套PR #450 把 56 层嵌套 ALS 合并为一个UnifiedRequestContext让请求级状态的读写、默认值、类型与测试都集中化显著降低心智负担per-request 与 per-call 分层是正确粒度请求生命周期与缓存调用生命周期不同强行统一反而需要复杂的嵌套管理globalThisSymbol.for是 Vite 多环境下的必备手段没有它模块实例分叉会让并发状态静默错乱isInsideUnifiedScope()双路回退是兼容性基石同一 shim 在 App Router、Pages Router、测试与无 ALS 运行时都能正常工作。如需深入源码建议从 ALS-ARCHITECTURE.md 出发依次阅读 unified-request-context.ts、als-registry.ts、head-state.ts、router-state.ts、cache-runtime.ts 与 cache.ts并结合 app-page-dispatch.ts 等入口观察统一上下文在实际请求管线中的装配过程。赞分享后端Web框架SSR【免费下载链接】vinextVite plugin that reimplements the Next.js API surface — deploy anywhere项目地址https://gitcode.com/gh_mirrors/vi/vinext点击查看免费下载相关推荐数据版本控制终极指南Awesome open>数据版本控制终极指南Awesome open data centric AI中的DVC与lakeFS全方位对比分析 在数据驱动的AI开发中有效的数据版本控制后端humanlayer-wui 热键作用域系统实战指南基于 react-hotkeys-hook 的层级快捷键隔离架构humanlayer wui 热键作用域系统实战指南基于 react hotkeys hook 的层级快捷键隔离架构 本篇技术指南完整解析 humanlaye人工智能AI Agent后端MCP 服务CLI桌面应用Teleport Scopes基于路径层次的作用域隔离与委派管理设计全解析Teleport Scopes基于路径层次的作用域隔离与委派管理设计全解析 导读 本文基于仓库 rfd/0229 scopes.md https://link网络安全认证鉴权运维后端上一篇10个技巧快速掌握Android远程控制 - 终极指南 下一篇番茄小说下载器使用指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表