@tanstack/vue-router 1.167 → 1.170 演进解读:匹配调度重构、错误边界与响应式订阅模型
2026/9/16 18:14:20 网站建设 项目流程

@tanstack/vue-router 1.167 → 1.170 演进解读:匹配调度重构、错误边界与响应式订阅模型

【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router

导读

本文基于packages/vue-router/CHANGELOG.md中记录的@tanstack/vue-router从 1.167.0 到 1.170.32 的完整发布历史,系统梳理这一阶段该 Vue 路由库在匹配与数据加载调度、错误/未找到(not-found)边界、Link 预加载、路径参数解析、SSR 与构建产物等方面的关键演进,并结合本仓库packages/vue-router/src下的源码实现逐条佐证。读完本文,你将理解@tanstack/vue-router当前的内部状态模型(signal 化的createAtomuseSelector订阅)、1.170.18那次大规模调度器重构的动机与迁移要点,以及如何在升级到 1.170.x 时规避破坏性变更。

版本事实均以当前仓库为准:packages/vue-router/package.json中记录当前版本为1.170.32engines.node要求>=20.19peerDependencies.vue^3.3.0


一、版本脉络总览:这一阶段的演进主线

从 CHANGELOG.md 可以看到,1.167.0 至 1.170.32 期间@tanstack/vue-router的变更可分为四条主线:

  1. 响应式状态模型重构:从createStore迁移到createAtom(#7150),随后升级 TanStack Store 至 0.11 并全面改用useSelector订阅(1.170.32),同时移除了pendingMatches/cachedMatches并转向 signal 化响应式(1.168.0)。
  2. 匹配与加载调度重构1.170.18将匹配加载重写为基于 lane 的调度器,统一跟踪导航、预加载与后台刷新,是这一阶段最核心的破坏性变更。
  3. 错误与 not-found 边界治理:类型收窄到unknown、保留 falsy 抛错值、从生命周期回调中排除错误/not-found 边界的结构后代、保留已成功的 not-found 匹配作为终端共享边界。
  4. 链接、预加载与体积优化preloadDelay应用于视口预加载、取消离开视口的预加载、移除非文档化的isTransitioning状态、内联isCtrlKey判断做字节压缩、清理 IntersectionObserver 选项,以及持续为 Vue/Solid 链接共享规范化 pathname 比较以缩减包体积。

此外还有大量Updated dependencies条目,说明@tanstack/vue-router绝大部分能力由底层@tanstack/router-core(1.167.x → 1.171.x)与@tanstack/history(1.161.x → 1.162.x)承载,Vue 适配层负责把核心逻辑接入 Vue 的响应式系统。


二、响应式状态模型演进:createStorecreateAtomuseSelector

2.1 两次关键迁移

  • 1.168.13:迁移createStorecreateAtom,API 更简单(#7150)。
  • 1.168.0:移除pendingMatchescachedMatches,全面转向 signal 化响应式;solid uses its own native signals(Solid 使用自身原生 signal),Vue 侧则依赖@tanstack/vue-store的响应式原语。
  • 1.168.4 / 1.168.1:跟进tanstack/store 0.9.30.9.2
  • 1.170.32:升级 TanStack Store 至 0.11,并将路由器订阅迁移到useSelector,在保留 selector 比较的同时补齐 Vue 订阅清理逻辑(#7824)。

2.2 源码佐证:Vue 适配层的 store 工厂

routerStores.ts 是整个 Vue 适配层的响应式桥接点,它把核心库的GetStoreConfig契约映射到@tanstack/vue-store

import { batch, createAtom } from '@tanstack/vue-store' import type { GetStoreConfig } from '@tanstack/router-core' import type { Readable } from '@tanstack/vue-store' export const getStoreFactory: GetStoreConfig = (_opts) => { return { createMutableStore: createAtom, createReadonlyStore: createAtom, batch, } }

可见:

  • 可变与只读 store 均由createAtom实现,即当前所有路由状态都走 atom(信号)原语;
  • batch用于批量提交状态,保证一次导航过程中的多次状态写入只触发一次渲染。

2.3 源码佐证:Router 类的 Vue 扩展

router.ts 中Router类继承自@tanstack/router-coreRouterCore,构造时传入getStoreFactory,并通过declare module '@tanstack/router-core'扩展了 Vue 特有的路由选项:

  • defaultComponent(默认Outlet)、defaultErrorComponent(默认ErrorComponent)、defaultPendingComponentdefaultNotFoundComponent(默认NotFound);
  • Wrap/InnerWrap:分别包裹整个路由器与路由器内部内容(可用于注入全局 Context);
  • defaultOnCatch:路由器 ErrorBoundary 捕获错误的默认处理器。

这些选项在 1.170.x 中均可直接用于createRouter()的配置对象。

2.4 源码佐证:useMatch的订阅实现

useMatch.tsx 是 Vue 侧所有“读取路由状态”钩子的地基(useSearchuseParamsuseLoaderData均基于它)。其客户端分支使用useSelector订阅按路由 key 划分的 presentation store:

if (opts.from) { // routeId case: subscribe to the stable per-route presentation atom. const matchStore = router.stores.getMatchStore(opts.from) match = useSelector(matchStore) } else { // Nearest-match case: use the routeId from context for stable lookup. const nearestRouteId = Vue.inject(routeIdContext) if (nearestRouteId) { match = useSelector(router.stores.getMatchStore(nearestRouteId)) } else { match = Vue.ref(undefined) } }

随后用Vue.computed包装opts.select,并保留lastOwnedMatch在 match 暂时消失时回退(配合 1.170.27 的 “preserve pending UI across retained routes” 与 1.170.25 的 “Keep active route components mounted by default when route params change”)。shouldThrow默认true,找不到 match 时抛出 invariant;设为false时返回undefined——这正是 1.170.30 中“route-scopeduseMatch/useSearch/useParams转发shouldThrow选项并保留可选返回类型”(#8169)的落地位置。

useSearch.tsx、useParams.tsx、useLoaderData.tsx 均以useMatch+select组合实现,例如useSearch内部:

return useMatch({ from: opts.from!, strict: opts.strict, shouldThrow: opts.shouldThrow, select: (match) => (opts.select ? opts.select(match.search) : match.search), })

三、核心重构:基于 lane 的匹配加载调度器(1.170.18)

3.1 动机与改动

1.170.18(#7805)是这一阶段最具分量的内部重构,将匹配加载重写为lane-based scheduler:每次导航、预加载、后台刷新都被视为一个有序工作单元。它解决的问题包括:

  • 重叠导航之间 pending/redirect/retry 状态互相泄漏;
  • SSR 对 redirect、error、not-found 响应的状态码不正确;
  • 客户端在服务端已完成工作后仍重复执行(hydration 间隙)。

配套行为变化:

  • 失效(invalidation)会退役匹配中的活跃预加载,使旧的推测性 loader 结果在失效后无法变成新鲜缓存数据;
  • 路由headers()只在服务端执行,不再在客户端资产投影(asset projection)期间调用,与既有文档行为一致;
  • 文档化的默认gcTimepreloadGcTime与运行时默认一致,即 5 分钟(300_000)。

3.2 移除/变更的导出内部 API(升级重点)

该版本移除或调整了大量内部导出,升级到 1.170.18+ 的开发者需要对照迁移:

原 API替代方案
RouterState.loadedAtmatch.updatedAt
RouterState.isTransitioning订阅router.state.status/router.state.isLoading
RouterState.statusCodeRouterState.redirect服务端 loader 内部处理,不再暴露在router.state
RouteMatch.fetchCount已移除,无替代(纯信息性)
RouteMatch.status === 'redirected'被重定向的 match 直接从 match 列表中移除,不再渲染
RouteMatch.globalNotFound私有化_notFound,改用match.status === 'notFound'
React/Solid/VueMatch组件的matchIdproprouteIdprop
RouterStoresmatchesId/matchStores/getRouteMatchStore()ids/byRoute/getMatchStore()(route-keyed presentation store)
RouterCore.getMatch()/updateMatch()/cancelMatch()/cancelMatches()router.state.matches读取(如router.state.matches.find((m) => m.id === id)),不再支持从外部变更/取消单个进行中的 match
RouterCore.hasNotFoundMatch()router.state.matches.some((m) => m.status === 'notFound')
RouterCore.looseRoutesByIdroutesById
RouterCore.isPrerendering()isViewTransitionTypesSupportedviewTransitionPromise移除,无替代
RouterCore.getParsedLocationHref()clearExpiredCache()移除;过期缓存条目在 match 提交时自动对账
RouterCore.latestLoadPromisebeforeLoad()移除,无替代
commitLocationPromisependingBuiltLocation内部字段_commitPromise_pendingLocation
GetMatchFnUpdateMatchFn移除
@tanstack/router-core独立导出的getMatchedRoutes()改用实例方法router.getMatchedRoutes()
RouterCore.loadRouteChunk()第二参数数组第二参数改为'errorComponent''notFoundComponent'false(单参数用法不变)
Redirect.redirectHandled移除(内部 redirect 记账)
MatchRoutesOpts.preloadMatchRoutesOpts.dest移除
StartTransitionFn(fn, expected) => Promise<boolean>(原为(fn) => void),仅影响自定义框架适配器

3.3 1.170.18 之后的调度细节修复

  • 1.170.29:按需构建客户端预加载 location,移除框架链接使用的预构建 location 参数(#8132);重载时保留 context(#8130)。
  • 1.170.30:从路由生命周期回调中排除错误/not-found 边界之下的结构后代,并在失效、hydration、后台刷新、被取代的导航发布过程中保留生命周期成员资格(#8165);保留客户端导航期间已成功的 not-found 匹配作为终端共享边界,在目标加载时保留路由 context(#8161)。
  • 1.168.11:修复被重定向的 pending 路由过渡,使懒加载目标路由可完成加载而不被过期的重定向 match 引发渲染错误(#7137)。

四、错误与 not-found 边界处理

这一阶段对错误边界做了多轮打磨:

  • 1.170.30(#8209):React 与 Vue 错误边界保留 falsy 抛出的值(如nullundefined0false);React/Vue 的边界 error 组件与onCatch回调类型收窄为unknown。升级后应先做窄化(如error instanceof Error)再读取message/stackErrorComponentProps<TError>仍可用于已收窄类型的值;路由onError类型不变。SSR 侧则把非Error的 loader 错误包装为Error以匹配 Solid 原生边界行为,原始值保留在cause中。
  • 1.168.9(#7077):保留组件抛出的notFound()错误穿过框架错误边界,使路由notFoundComponent无需显式routeId即可渲染。
  • 1.170.24(#8045):当Outlet被渲染在 pending、error 或 not-found 组件内部时给出警告——这是对错误/not-found 边界语义的显式约束。

五、Link 组件、预加载与体积优化

5.1 预加载行为细化

  • 1.170.24(#8044):将preloadDelay应用于视口链接预加载,并在链接离开视口时取消进行中的预加载。
  • 1.170.20(#7971):清理 Link 组件中的 IntersectionObserver 选项。

link.tsx 中可以看到 Vue 版 Link 的完整实现骨架:useLinkProps在 SSR 下只渲染一次、不建立 store 订阅与 observer;客户端则用useIntersectionObserver(见 utils.ts)触发预加载,并通过timeoutMapWeakMap)管理预加载超时。Link 事件处理器同时兼容 Vue 原生小写事件(onMouseenter等)与 camelCase 版本,并支持asChilddisabled

5.2 属性与体积清理

  • 1.170.24(#8043):移除未文档化的 LinkisTransitioning状态与data-transitioning属性。
  • 1.168.10(#7138):修复Link将内部路由属性(preloadIntentProximityfromunsafeRelative等)泄漏到渲染出的 DOM 元素的问题,React/Solid/Vue 三端同步修复。
  • 1.170.26(#8073):在 Link 组件内联isCtrlKey判断以缩减字节。
  • 1.170.31(#8308):共享规范化 pathname 比较,减少 Vue/Solid 链接包体积;同时让 Vue 链接在目标变为内部地址时刷新链接状态(refresh Vue link state when destinations become internal)。
  • 1.168.2(#7007):用自研实现替换tiny-invarianttiny-warning以缩减包体积。
  • 1.168.14(#7152):缩短内部不可压缩的 store 名称,进一步做字节压缩。

5.3 导航与 URL 校验

  • 1.170.31(#8308):校验导航与重定向目标,将模糊相对 URL 保持在当前 origin,约束 prerender 请求与输出路径;避免重定向 header 出现在序列化的 server function 响应体中;保留原生表单 HTTP 重定向、文档重定向的路由错误处理与 mask、共享 loader 重定向的按导航目标;显式重定向Locationheader 优先于路由选项检查;复用协议相对 URL 检查解析重定向 scheme。
  • 1.170.30(#8251):链接、导航、重定向与构建配置中的绝对 URL 检查改用URL.canParse,并为旧浏览器保留URL构造器回退。

六、路径参数解析与 search 中间件

6.1params.parse的两处增强

  • 1.169.0(#7263):允许params.parse实验性地返回false,以在路径匹配期间跳过某个候选路由;抛出的解析错误仍浮出到选中的 match 上(不会静默落入其他路由);类型化模板链接的出站 URL 生成仍走“精确路由查找 +params.stringify”。
  • 1.170.20(#7967):匹配路由时保留路径参数的原始字符串形式,使params.parse产生的结构化值能够生成稳定的 match ID,避免复用过期 loader 数据;同时RouterCore.getMatchedRoutes()的返回从对象改为元组[matchedRoutes, rawParams, foundRoute]

6.2 search 中间件组合修复

  • 1.170.13(#7555 的导出清单)。

七、SSR 与构建产物演进

  • 1.170.9(#7497):修复流式渲染(streaming)问题。
  • 1.168.20(#7253):为 TanStack Start 增加内联 CSS manifest 的 SSR 支持,路由样式可嵌入 HTML 响应并在 hydration 时不产生重复 stylesheet 链接。
  • 1.170.8(#7477):支持 Rsbuild 客户端输出格式——默认 module 输出,经典 script 环境用 IIFE;客户端入口脚本与预加载表示为根路由 manifest 资产,脚本预加载跟随 manifest 脚本格式,跨域配置使用script键;transformAssets脚本回调上下文仅暴露{ kind: 'script', url }
  • 1.168.3(#7023):新增transformAssets能力。
  • 1.168.5(#7042):修复滚动恢复——不再节流。
  • 1.170.6(#7447):修复 hash 导航被过期的滚动恢复条目覆盖的问题。
  • 1.168.18(#7167):修复路由文件转换对 route ID 引号、更多导出Route模式的支持,并避免边缘情况下的错误 import 重写(对路由调用检测、import 移除安全性、引号保留、构造器替换与不支持的 route 定义补充了测试覆盖)。

SSR 相关源码可继续参考 src/ssr/(renderRouterToStringrenderRouterToStreamdefaultRenderHandlerdefaultStreamHandler等),Vue 端 SSR 入口通过@tanstack/vue-router/ssr/server/ssr/client子路径导出(见 package.json)。


八、类型系统与 DX 改进

  • 1.168.12(#7139):修复MatchRoute子回调参数推断——从目标to路由解析 params,而非路由路径 key,React/Solid/Vue 三端同步修复。
  • 1.167.4(#6866 目录)。
  • 1.167.0(#6921):新增staleReloadMode
  • 1.170.0(#7395):干净的 minor 版本,重新出发(fresh start)。
  • 1.170.20(#7970):createFileRoute不再依赖FileRoute类。

构建链方面:1.167.1修复使用@tanstack/vite-config 0.4.3构建(#6923),1.167.2升级到 vite-config 5.x(rolldown)(#6926)。


九、升级建议与注意事项

  1. 版本与依赖:当前@tanstack/vue-router@1.170.32依赖@tanstack/router-core@1.171.x@tanstack/history@1.162.x@tanstack/vue-store@^0.11.0@vue/runtime-dom@^3.5.25isbot;要求 Node>=20.19、Vue^3.3.0
  2. 1.170.18 是分水岭:若你曾使用RouterState.loadedAtisTransitioningRouteMatch.fetchCountrouter.beforeLoad()hasNotFoundMatch()looseRoutesById或独立导出的getMatchedRoutes(),请按上文表格迁移;Match组件改用routeIdprop;从router.state.matches读取 match 列表。
  3. 错误边界类型:升级 React/Vue 适配层后,读取边界 error 的message/stack前先做instanceof Error收窄;注意 falsy 抛错值(null/undefined/0/false)现在会被保留。
  4. 预加载与链接preloadDelay现在同样作用于视口预加载,离开视口的链接预加载会被取消;data-transitioning属性已移除,勿在样式/测试中依赖。
  5. SSR 行为:路由headers()仅在服务端执行;服务器响应状态码与 redirect 处理已移入服务端 loader 内部,不再暴露在router.state

十、进一步阅读

  • 版本记录全文:packages/vue-router/CHANGELOG.md
  • 包配置与入口:packages/vue-router/package.json、src/index.tsx
  • 状态桥接与订阅:src/routerStores.ts、src/useMatch.tsx、src/useRouterState.tsx
  • 路由实例与选项:src/router.ts、src/route.ts
  • 链接与导航:src/link.tsx、src/useNavigate.tsx
  • SSR 实现:src/ssr/
  • 测试与工具链:tests/、vitest.config.ts
  • 底层核心(匹配/加载/错误边界逻辑本体):packages/router-core/src、packages/history/src

【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询