尧图精选

React Router 路由掩码(Route Masking)实战指南:隐藏真实 URL、实现模态框导航与平行路由

🕒 发布时间:2026/9/14 18:16:02 📁 来源:尧图网络
React Router 路由掩码Route Masking实战指南隐藏真实 URL、实现模态框导航与平行路由【免费下载链接】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路由掩码Route Masking是 TanStack Router 提供的一项 URL 伪装能力在浏览器历史记录和地址栏中展示一个「美化后」的 URL而路由内部实际导航到另一个真实的 URL并在 URL 被分享或按需在刷新后自动回落到所展示的 URL。本文基于 route-masking.md 官方指南结合router-core、react-router、solid-router、vue-router的源码与测试系统讲解掩码的工作原理、命令式/声明式两种用法以及unmaskOnReload的三种优先级配置。读完本文你将能实现「模态框地址隐藏」「搜索参数隐藏」等场景并为平行路由Parallel Routes等高级模式打下基础。什么是路由掩码Route Masking路由掩码的核心诉求是用户看到的 URL 与路由器实际导航到的 URL 不一致。这在很多真实交互中非常常见导航到模态框路由/photo/5/modal但地址栏显示/photos/5导航到评论模态框路由/post/5/comments但地址栏显示/posts/5导航时携带?showLogintrue之类的内部搜索参数但希望地址栏不包含该参数导航时携带?modalsettings搜索参数但希望地址栏显示/settings。这些场景都可以用路由掩码实现并且可以进一步扩展出更高级的模式例如官方指南中提到的 平行路由parallel routes。与 URL 重写URL Rewrites 不同路由掩码并不改变路由匹配本身——它改变的只是写入浏览器历史与地址栏的 URL。当用户复制分享该 URL 时掩码数据随历史栈脱离而丢失展示的 URL 成为真正的目标地址。路由掩码的工作原理使用路由掩码不需要理解其内部机制——这一节适合好奇底层实现的人。想直接上手可跳到「如何使用路由掩码」一节。__tempLocation把真实导航位置藏在 history state 中路由掩码利用了浏览器 History API 的location.state字段把真正想要导航到的运行时位置runtime location存进将要写入 URL 的那个 location 的 state 里属性名为__tempLocationconst location { pathname: /photos/5, search: , hash: , state: { key: wesdfs, __tempKey: sadfasd, __tempLocation: { pathname: /photo/5/modal, search: , hash: , state: {}, }, }, }上面的对象结构在源码中有完整对应。在 packages/router-core/src/history.ts 中历史记录的 state 类型明确声明了这两个私有字段__tempLocation?: HistoryLocation __tempKey?: string同时packages/router-core/src/location.ts 中ParsedLocation也增加了两个与掩码相关的可选属性用于在解析与提交阶段传递掩码状态maskedLocation?: ParsedLocationTSearchObj unmaskOnReload?: boolean解析阶段命中__tempLocation时狸猫换太子当路由器从 history 中解析一个 location 时会检查location.state.__tempLocation。核心逻辑位于 packages/router-core/src/router.tsconst { __tempLocation, __tempKey } location.state if (__tempLocation (!__tempKey || __tempKey this.tempLocationKey)) { // 同步 location key const parsedTempLocation parse(__tempLocation) as any parsedTempLocation.state.key location.state.key parsedTempLocation.state.__TSR_key location.state.__TSR_key delete parsedTempLocation.state.__tempLocation return { ...parsedTempLocation, maskedLocation: location, } } return location从源码可以清晰看到解析流程从 history state 中取出__tempLocation与__tempKey若__tempLocation存在且__tempKey为空即未开启刷新解除掩码或与当前路由实例的tempLocationKey相等则用__tempLocation作为实际解析结果与此同时把真正写入 URL 的那个 location保存到maskedLocation属性中以便需要知道真实 URL时随时取回最后删除内部字段避免污染。这里的关键是第 2 步的tempLocationKey判断。它由每个 Router 实例在初始化时随机生成见 packages/router-core/src/router.tstempLocationKey: string | undefined ${Math.round( Math.random() * 10000000, )}这一随机 key 正是「刷新后是否解除掩码」的控制机关下文「Unmasking on page reload」会详细展开。提交阶段把真实位置写回__tempLocation反向流程发生在commitLocation中packages/router-core/src/router.ts。当一次导航带有maskedLocation时路由器会这样构造最终写入 history 的 stateif (maskedLocation) { nextHistory { ...maskedLocation, state: { ...maskedLocation.state, __tempKey: undefined, __tempLocation: { ...nextHistory, search: nextHistory.searchStr, state: { ...nextHistory.state, __tempKey: undefined!, __tempLocation: undefined!, __TSR_key: undefined!, key: undefined!, }, }, }, } }也就是说写入历史/地址栏的是maskedLocation展示用 URL而真实导航目标被装进__tempLocation塞进 state。随后通过this.history.push / replace提交packages/router-core/src/router.ts浏览器地址栏与历史记录中呈现的就是被掩码后的 URL而路由匹配与渲染走的则是真实的__tempLocation。maskedLocation还有一个实际用途在 TanStack Router Devtools 中当检测到某个路由是被掩码的会优先展示真实 URL 而非被掩码的 URL避免开发者被地址栏的伪装误导。这一行为与 packages/router-core/src/load-client.ts 中const publicLocation location.maskedLocation ?? location的取值逻辑一脉相承。记住这一切都由路由器在底层自动处理你完全不需要手动操作__tempLocation。如何使用路由掩码路由掩码的 API 非常简洁提供两种使用方式命令式Imperative在Link和navigate()上使用mask选项声明式Declarative在 Router 上使用routeMasks选项。无论是哪种方式mask选项接受的导航对象与Link/navigate()完全一致因此你熟悉的to、replace、state、search、params、hash等选项都可以在mask内使用。唯一区别是mask里的导航对象不用于真实导航而是用于构造被掩码的展示 URL。mask选项是类型安全的如果使用 TypeScript向mask传入无效的导航对象会直接得到类型错误。在 packages/router-core/src/link.ts 中mask的类型被定义为ToMaskOptionsTRouter, TMaskFrom, TMaskTo它会根据to的目标路由自动推导出合法的params、search形状。命令式Link的mask选项以下示例中Link实际导航到/photos/$photoId/modal模态框路由但mask会把地址栏 URL 显示为/photos/$photoIdLink to/photos/$photoId/modal params{{ photoId: 5 }} mask{{ to: /photos/$photoId, params: { photoId: 5, }, }} Open Photo /Link测试用例也验证了mask与to、params、search、hash等属性可以同时传入Link见 packages/react-router/tests/link.test.tsx 中mask{{ to: /posts, hash: masked }}的组合用法。命令式navigate()的mask选项同样的能力也可以用在编程式导航上const navigate useNavigate() function onOpenPhoto() { navigate({ to: /photos/$photoId/modal, params: { photoId: 5 }, mask: { to: /photos/$photoId, params: { photoId: 5, }, }, }) }navigate()的mask用法在 packages/react-router/tests/useNavigate.test.tsx 中同样有测试覆盖。声明式Router 的routeMasks选项如果不想在每个Link/navigate()调用里都重复写mask可以在创建 Router 时通过routeMasks一次性声明掩码规则。路由掩码会匹配特定from路由并自动把命中该路由的导航掩码成to指定的 URL。Reactimport { createRouteMask } from tanstack/react-router const photoModalToPhotoMask createRouteMask({ routeTree, from: /photos/$photoId/modal, to: /photos/$photoId, params: (prev) ({ photoId: prev.photoId, }), }) const router createRouter({ routeTree, routeMasks: [photoModalToPhotoMask], })Solidimport { createRouteMask } from tanstack/solid-router const photoModalToPhotoMask createRouteMask({ routeTree, from: /photos/$photoId/modal, to: /photos/$photoId, params: (prev) ({ photoId: prev.photoId, }), }) const router createRouter({ routeTree, routeMasks: [photoModalToPhotoMask], })Vue 框架对应使用tanstack/vue-router的createRouteMaskAPI 完全一致三者的实现都导出在各自的route.ts(x)中。createRouteMask至少需要传入参数说明routeTree该路由掩码所应用到的路由树from该路由掩码所应用到的路由 IDRoute ID...navigateOptions标准的to、search、params、replace、hash等Link/navigate()选项createRouteMask同样是类型安全的传入无效的路由掩码给routeMasks选项会得到 TypeScript 类型错误。从源码看createRouteMask的实现非常轻量——它本质上是一个带强类型的参数校验/透传函数packages/react-router/src/route.tsxexport function createRouteMask TRouteTree extends AnyRoute, TFrom extends string, TTo extends string, ( opts: { routeTree: TRouteTree } ToMaskOptionsRouterCoreTRouteTree, never, boolean, TFrom, TTo, ): RouteMaskTRouteTree { return opts as any }真正的类型约束来自RouteMask类型packages/router-core/src/route.tsexport type RouteMaskTRouteTree extends AnyRoute { routeTree: TRouteTree from: RoutePathsTRouteTree to?: any params?: any search?: any hash?: any state?: any unmaskOnReload?: boolean }可以看到RouteMask还额外支持unmaskOnReload字段这正是声明式掩码控制刷新行为的入口。routeMasks的运行时匹配逻辑在buildLocation中当一次导航没有显式携带mask时路由器会尝试用routeMasks进行匹配packages/router-core/src/router.tsconst next build(opts) if (opts.mask) { next.maskedLocation build({ from: opts.from, ...opts.mask, }) } else if (this.options.routeMasks) { const match findFlatMatchRouteMaskTRouteTree( next.pathname, this.processedTree, ) if (match) { const params Object.assign(Object.create(null), match.rawParams) const { from: _from, params: maskParams, ...maskProps } match.route // 若 mask 的 params 是函数则以匹配到的 params 为上下文调用 const nextParams resolveNextParams(maskParams, params) next.maskedLocation build({ from: opts.from, ...maskProps, params: nextParams, }) } }从源码结构可以推断出显式mask优先调用方显式传入mask时直接使用它构造maskedLocationrouteMasks不再参与routeMasks按路径匹配使用findFlatMatch在路由树上查找命中的掩码路由并取出其配置params支持函数形态maskParams可以是一个接收匹配到的原始参数如prev.photoId并返回新参数的函数例如官方示例中的params: (prev) ({ photoId: prev.photoId })——这正是createRouteMask中函数式params的底层支撑。此外Router 的选项类型中routeMasks被声明为ArrayRouteMaskTRouteTreepackages/router-core/src/router.ts并在路由树处理阶段packages/router-core/src/router.ts被processRouteMasks合并进 processed tree。分享 URL 时自动解除掩码URL 一旦被分享就会自动解除掩码。原因在于URL 一旦脱离浏览器本地的历史栈掩码数据存在location.state里就随之丢失了——毕竟隐藏真实 URL正是掩码的初衷。你从历史记录中复制粘贴出去的 URL就是那个展示用的被掩码后的URL也就是你希望别人看到的那个地址。这一点由浏览器 History API 的天然行为保证state是会话级的不会随 URL 文本被复制或发送。本地刷新默认保留掩码默认情况下刷新页面时 URL 不会被解除掩码。掩码数据保存在历史记录的location.state中只要该历史记录仍在内存中的历史栈里掩码数据就依然可用刷新后 URL 会继续保持掩码状态。用前面的源码来解释刷新后路由器重新解析 history若__tempKey为undefined解析函数中的条件(!__tempKey || ...)恒为真__tempLocation会被继续采用于是 URL 继续保持掩码。只有当__tempKey被设置为某个具体值且与实例随机生成的tempLocationKey不相等时掩码才会被丢弃。刷新时解除掩码unmaskOnReload的三种配置如果你希望在本地刷新时也解除掩码有 3 个选项按优先级从低到高依次为后传入的会覆盖前面的Router 级默认值将 Router 的默认unmaskOnReload选项设为true掩码级在createRouteMask()创建路由掩码时从掩码函数中返回unmaskOnReload: true导航级最高优先级在Link组件或navigate()API 中传入unmaskOnReload: true。以 Router 级配置为例const router createRouter({ routeTree, unmaskOnReload: true, // 全局默认刷新时解除掩码 })在commitLocation中这三层优先级被合并为一个表达式packages/router-core/src/router.tsif ( nextHistory.unmaskOnReload ?? this.options.unmaskOnReload ?? false ) { nextHistory.state.__tempKey this.tempLocationKey }从源码可以看到确切的优先级链条nextHistory.unmaskOnReload导航级→this.options.unmaskOnReloadRouter 级→false默认值。??空值合并保证了只有显式传入true才会启用。其底层机制是当unmaskOnReload生效时提交 history 的 state 会带上__tempKey this.tempLocationKey。刷新后路由器解析 history发现__tempKey存在但不匹配当前重新随机生成的实例 key于是__tempLocation不再被采用路由回落到地址栏中的展示 URL——掩码被解除页面刷新后地址栏与路由内容一致。如果三处都没有设置则回落到默认行为不解除掩码刷新后继续保持掩码的 URL。实践建议与总结模态框优先用命令式mask模态框导航往往是局部的、一次性的直接在Link/navigate()上传mask最直观且能享受完整的类型推导全局规则用routeMasks如果某个路由永远应该被掩码例如某个内部专用路由用createRouteMaskrouteMasks声明一次即可避免在每个入口重复书写分享即解除、刷新按需掩码数据随历史栈存在分享出去的 URL 永远是展示 URL本地刷新默认保留掩码需要刷新解除时按「导航级 → 掩码级 → Router 级」的优先级配置unmaskOnReload与平行路由组合路由掩码可以与 平行路由parallel routes 结合实现如/posts/5展示而实际导航到/post/5/comments的复杂交互模式类型安全是核心优势mask、createRouteMask、routeMasks全程类型安全错误的params/search/to会在编译期被拦截。路由掩码是 TanStack Router 中所见非所得、所得即所想的优雅实现对外展示干净的 URL对内执行真实的导航全部封装在__tempLocation/maskedLocation两个内部字段与mask/routeMasks两个公开 API 中。无论是隐藏模态框路由、隐藏内部搜索参数还是支撑平行路由这类高级模式它都能在保持类型安全的前提下让地址栏与业务意图完美对齐。【免费下载链接】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),仅供参考
上一篇/下一篇内容由系统自动关联 返回资讯列表 →