@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 化的createAtom与useSelector订阅)、1.170.18那次大规模调度器重构的动机与迁移要点,以及如何在升级到 1.170.x 时规避破坏性变更。
版本事实均以当前仓库为准:
packages/vue-router/package.json中记录当前版本为1.170.32,engines.node要求>=20.19,peerDependencies.vue为^3.3.0。
一、版本脉络总览:这一阶段的演进主线
从 CHANGELOG.md 可以看到,1.167.0 至 1.170.32 期间@tanstack/vue-router的变更可分为四条主线:
- 响应式状态模型重构:从
createStore迁移到createAtom(#7150),随后升级 TanStack Store 至 0.11 并全面改用useSelector订阅(1.170.32),同时移除了pendingMatches/cachedMatches并转向 signal 化响应式(1.168.0)。 - 匹配与加载调度重构:
1.170.18将匹配加载重写为基于 lane 的调度器,统一跟踪导航、预加载与后台刷新,是这一阶段最核心的破坏性变更。 - 错误与 not-found 边界治理:类型收窄到
unknown、保留 falsy 抛错值、从生命周期回调中排除错误/not-found 边界的结构后代、保留已成功的 not-found 匹配作为终端共享边界。 - 链接、预加载与体积优化:
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 的响应式系统。
二、响应式状态模型演进:createStore→createAtom→useSelector
2.1 两次关键迁移
- 1.168.13:迁移
createStore至createAtom,API 更简单(#7150)。 - 1.168.0:移除
pendingMatches与cachedMatches,全面转向 signal 化响应式;solid uses its own native signals(Solid 使用自身原生 signal),Vue 侧则依赖@tanstack/vue-store的响应式原语。 - 1.168.4 / 1.168.1:跟进
tanstack/store 0.9.3、0.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-core的RouterCore,构造时传入getStoreFactory,并通过declare module '@tanstack/router-core'扩展了 Vue 特有的路由选项:
defaultComponent(默认Outlet)、defaultErrorComponent(默认ErrorComponent)、defaultPendingComponent、defaultNotFoundComponent(默认NotFound);Wrap/InnerWrap:分别包裹整个路由器与路由器内部内容(可用于注入全局 Context);defaultOnCatch:路由器 ErrorBoundary 捕获错误的默认处理器。
这些选项在 1.170.x 中均可直接用于createRouter()的配置对象。
2.4 源码佐证:useMatch的订阅实现
useMatch.tsx 是 Vue 侧所有“读取路由状态”钩子的地基(useSearch、useParams、useLoaderData均基于它)。其客户端分支使用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)期间调用,与既有文档行为一致; - 文档化的默认
gcTime与preloadGcTime与运行时默认一致,即 5 分钟(300_000)。
3.2 移除/变更的导出内部 API(升级重点)
该版本移除或调整了大量内部导出,升级到 1.170.18+ 的开发者需要对照迁移:
| 原 API | 替代方案 |
|---|---|
RouterState.loadedAt | match.updatedAt |
RouterState.isTransitioning | 订阅router.state.status/router.state.isLoading |
RouterState.statusCode、RouterState.redirect | 服务端 loader 内部处理,不再暴露在router.state |
RouteMatch.fetchCount | 已移除,无替代(纯信息性) |
RouteMatch.status === 'redirected' | 被重定向的 match 直接从 match 列表中移除,不再渲染 |
RouteMatch.globalNotFound | 私有化_notFound,改用match.status === 'notFound' |
React/Solid/VueMatch组件的matchIdprop | routeIdprop |
RouterStores的matchesId/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.looseRoutesById | routesById |
RouterCore.isPrerendering()、isViewTransitionTypesSupported、viewTransitionPromise | 移除,无替代 |
RouterCore.getParsedLocationHref()、clearExpiredCache() | 移除;过期缓存条目在 match 提交时自动对账 |
RouterCore.latestLoadPromise、beforeLoad() | 移除,无替代 |
commitLocationPromise、pendingBuiltLocation | 内部字段_commitPromise、_pendingLocation |
GetMatchFn、UpdateMatchFn | 移除 |
@tanstack/router-core独立导出的getMatchedRoutes() | 改用实例方法router.getMatchedRoutes() |
RouterCore.loadRouteChunk()第二参数数组 | 第二参数改为'errorComponent'、'notFoundComponent'或false(单参数用法不变) |
Redirect.redirectHandled | 移除(内部 redirect 记账) |
MatchRoutesOpts.preload、MatchRoutesOpts.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 抛出的值(如
null、undefined、0、false);React/Vue 的边界 error 组件与onCatch回调类型收窄为unknown。升级后应先做窄化(如error instanceof Error)再读取message/stack;ErrorComponentProps<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)触发预加载,并通过timeoutMap(WeakMap)管理预加载超时。Link 事件处理器同时兼容 Vue 原生小写事件(onMouseenter等)与 camelCase 版本,并支持asChild与disabled。
5.2 属性与体积清理
- 1.170.24(#8043):移除未文档化的 Link
isTransitioning状态与data-transitioning属性。 - 1.168.10(#7138):修复
Link将内部路由属性(preloadIntentProximity、from、unsafeRelative等)泄漏到渲染出的 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-invariant与tiny-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/(renderRouterToString、renderRouterToStream、defaultRenderHandler、defaultStreamHandler等),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)。
九、升级建议与注意事项
- 版本与依赖:当前
@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.25与isbot;要求 Node>=20.19、Vue^3.3.0。 - 1.170.18 是分水岭:若你曾使用
RouterState.loadedAt、isTransitioning、RouteMatch.fetchCount、router.beforeLoad()、hasNotFoundMatch()、looseRoutesById或独立导出的getMatchedRoutes(),请按上文表格迁移;Match组件改用routeIdprop;从router.state.matches读取 match 列表。 - 错误边界类型:升级 React/Vue 适配层后,读取边界 error 的
message/stack前先做instanceof Error收窄;注意 falsy 抛错值(null/undefined/0/false)现在会被保留。 - 预加载与链接:
preloadDelay现在同样作用于视口预加载,离开视口的链接预加载会被取消;data-transitioning属性已移除,勿在样式/测试中依赖。 - 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),仅供参考