ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

TanStack Solid Router 导航拦截(Navigation Blocking)实战:useBlocker 原理与示例源码解析

TanStack Solid Router 导航拦截(Navigation Blocking)实战:useBlocker 原理与示例源码解析 TanStack Solid Router 导航拦截Navigation Blocking实战useBlocker 原理与示例源码解析【免费下载链接】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本篇文章以仓库内 examples/solid/navigation-blocking 示例为线索系统讲解 TanStack Solid Router 的导航拦截Navigation Blocking机制。你将掌握useBlocker钩子与Block组件的全部配置项、withResolver自定义确认 UI 的两种实现方式以及底层 history 拦截器的执行原理并能直接复刻表单脏数据离开确认这类真实场景。示例总览先跑起来仓库中的导航拦截示例位于 examples/solid/navigation-blocking/README.md它本身是一个可独立运行的 Vite Solid 应用。按 README 说明运行方式只有两步npm install # 或 yarn npm start # 或 yarn start该示例构建了一条由 4 个路由组成的路由树见 src/main.tsx/首页/foo/$id带路径参数与 search 参数/editor-1嵌套路由父级含输入框/editor-1/editor-2嵌套子路由含输入框示例同时演示了两类拦截根组件级从/editor-1导航到/foo/123?helloworld时必定拦截子路由级/editor-1输入框有内容表单脏数据时拦截一切离开行为。什么是导航拦截导航拦截Navigation Blocking是一种在导航真正发生前暂停它的能力官方指南 docs/router/guide/navigation-blocking.md 给出的典型触发场景包括用户有未保存的更改unsaved changes用户正处在表单填写中途用户正处在支付流程中途此时应向用户展示确认提示或自定义 UI用户确认导航继续用户取消挂起的导航被全部阻止。底层工作原理一层history 拦截器导航拦截并不是路由层独有的魔法它的核心是给整个底层 history API 加了一层或多层 blocker。只要存在任意 blocker导航就会以两种方式被暂停见 docs/router/guide/navigation-blocking.md自定义 UI 路径路由可控的导航由路由层触发的导航Link点击、navigate()调用等会被逐层、异步、顺序地交给每个 blocker 的blockerFn判断。任何一个 blocker 返回true就放行并继续询问下一个直到全部放行任何一个返回false就取消导航且后续 blocker 不再执行。onbeforeunload事件路径页面级卸载对于关闭标签页、刷新、卸载页面资源这类路由无法直接控制的动作依赖浏览器原生的onbeforeunload事件弹出通用确认框。用户确认则绕过所有 blocker 并卸载页面取消则保持现状。上述逻辑在 packages/history/src/index.ts 中有清晰实现导航发生时遍历当前全部 blockers逐个执行blocker.blockerFn({ currentLocation, nextLocation, action })若任一返回true被拦截则通过win.history.go(-delta)回退并把ignoreNextPop置位防止浏览器历史栈被破坏。onbeforeunload分支packages/history/src/index.ts的判断规则是只要任意一个blocker 的enableBeforeUnload为true或其函数返回true就调用e.preventDefault()并设置e.returnValue 触发浏览器原生确认框。useBlocker 钩子官方 API 参考useBlocker的完整 API 定义见 docs/router/api/router/useBlockerHook.md。注意其中标注当前新版useBlockerAPI 仍处于 experimental 阶段。选项options选项必填默认值类型说明shouldBlockFn是—ShouldBlockFn返回boolean或Promisebooleantrue表示拦截该次导航false表示放行enableBeforeUnload否trueboolean \| (() boolean)是否或按条件拦截浏览器beforeunload事件disabled否falseboolean是否整体禁用该 blockerwithResolver否falseboolean是否使用钩子返回的 resolver 来人工解决拦截而非由shouldBlockFn自行解决blockerFn否—BlockerFn⚠️ 已废弃请改用shouldBlockFncondition否trueboolean⚠️ 已废弃为true时拦截导航shouldBlockFn收到的参数类型如下源码类型定义见 packages/solid-router/src/useBlocker.tsxtype ShouldBlockFnLocation... { routeId: TRouteId // 匹配到的路由 id fullPath: TFullPath // 路由完整路径模板如 /foo/$id pathname: string // 实际路径名 params: TAllParams // 路径参数类型安全 search: TFullSearchSchema // search 参数类型安全 } type ShouldBlockFnArgs { current: ShouldBlockFnLocation next: ShouldBlockFnLocation action: HistoryAction // 触发的 history 动作 }返回值当withResolver: true时返回一个响应式访问器Solid 中为Solid.AccessorBlockerResolver解构后包含字段说明statusblocked \| idlecurrent被拦截时当前出发位置信息next被拦截时目标位置信息action触发本次导航的HistoryActionproceed允许导航继续reset取消导航status重置为idle当withResolver: false时返回值类型为void拦截与否完全由shouldBlockFn的返回值决定。示例源码逐段解析下面完整解读 examples/solid/navigation-blocking/src/main.tsx。1. 根组件级精确匹配目标地址的拦截根组件RootComponent注册了一个规则型拦截器main.tsxconst blocker useBlocker({ shouldBlockFn: ({ current, next }) { if ( current.routeId /editor-1 next.fullPath /foo/$id next.params.id 123 next.search.hello world ) { return true } return false }, enableBeforeUnload: false, withResolver: true, })它演示了两个关键点条件可以做到路径模板级的精确next.fullPath是/foo/$id这样的模板字符串params和search都是经过类型推断的结构化对象因此可以组合出仅当从 editor-1 去往/foo/123?helloworld时拦截这样的细粒度规则enableBeforeUnload: false关闭了页面卸载拦截这里只关心应用内导航不希望浏览器在刷新/关页时弹出通用对话框。注意即使shouldBlockFn返回false若不显式设置该项beforeunload仍可能被触发默认true。被拦截后通过blocker().status blocked渲染自定义确认 UImain.tsx{blocker().status blocked ( div classmt-2 div Are you sure you want to leave editor 1 for /foo/123?helloworld ? /div button onClick{blocker().proceed}YES/button button onClick{blocker().reset}NO/button /div )}点击 YES 调用proceed()放行点击 NO 调用reset()取消——这正是withResolver: true的价值拦截的最终裁决权交还给 UI而不是在shouldBlockFn内部用window.confirm同步解决。2. 子路由级表单脏数据拦截Editor1Component注册了第二个拦截器且把两个条件分开配置main.tsxconst [value, setValue] createSignal() const blocker useBlocker({ shouldBlockFn: () value() ! , enableBeforeUnload: () value() ! , withResolver: true, })这里的关键差异shouldBlockFn: () value() ! —— 输入框有内容就阻止应用内导航包括跳到/editor-1/editor-2这类嵌套导航enableBeforeUnload: () value() ! —— 用函数形式动态控制浏览器beforeunload有内容时刷新/关页也会被浏览器原生确认框拦截无内容时完全放行。这正好对应 API 文档中该选项的boolean | (() boolean)类型。拦截时 UI 还能拿到current/next的pathname向用户展示从哪里到哪里main.tsx{blocker().status blocked ( div divAre you sure you want to leave editor 1?/div div You are going from {blocker().current?.pathname} to{ } {blocker().next?.pathname} /div button onClick{blocker().proceed}YES/button button onClick{blocker().reset}NO/button /div )}3. 导航入口与路由实例示例其余部分展示了导航入口如何携带params与search触发上述拦截main.tsxLink to/foo/$id params{{ id: 123 }} search{{ hello: world }} activeOptions{{ exact: true, includeSearch: true }} foo 123 /Linkfoo路由通过validateSearch校验 search 参数main.tsx保证next.search.hello在shouldBlockFn中是类型安全的字符串路由实例还开启了defaultPreload: intent与scrollRestoration: truemain.tsx。自定义确认 UI 的三种写法官方指南 docs/router/guide/navigation-blocking.md 明确指出大多数场景下在shouldBlockFn里用window.confirm配合withResolver: false就够了。但若想更贴合应用设计有三种方式方式一hook withResolver推荐示例采用const { proceed, reset, status } useBlocker({ shouldBlockFn: () formIsDirty(), withResolver: true, }) // JSX 中 {status blocked ( div pAre you sure you want to leave?/p button onClick{proceed}Yes/button button onClick{reset}No/button /div )}注意withResolver: true时shouldBlockFn的返回值不再决定拦截结果真正的裁决发生在你调用proceed()/reset()的那一刻。方式二hook 返回 Promise 的 shouldBlockFn无 resolver不用withResolver而是让shouldBlockFn自己返回一个Promiseboolean将确认弹窗封装进 PromiseuseBlocker({ shouldBlockFn: () { if (!formIsDirty()) return false const shouldBlock new Promiseboolean((resolve) { modals.open({ title: Are you sure you want to leave?, children: ( SaveBlocker confirm{() { modals.closeAll(); resolve(false) }} reject{() { modals.closeAll(); resolve(true) }} / ), onClose: () resolve(true), }) }) return shouldBlock }, })resolve(false)表示用户确认离开放行resolve(true)表示用户取消拦截。这个模式让你可以接入任意第三方 modal 管理器同时保持shouldBlockFn签名不变。方式三Block 组件 render props除了 hook还可以用Block组件从tanstack/solid-router导入源码见 packages/solid-router/src/useBlocker.tsximport { Block } from tanstack/solid-router Block shouldBlockFn{() formIsDirty()} withResolver {({ status, proceed, reset }) ( {status blocked ( div pAre you sure you want to leave?/p button onClick{proceed}Yes/button button onClick{reset}No/button /div )} / )} /BlockBlock内部就是调用useBlocker并把 resolver 通过 render propsSolid 中为 children 函数暴露给子级不传 children 函数时则纯粹作为无渲染拦截器使用。源码级原理useBlocker 内部发生了什么结合 packages/solid-router/src/useBlocker.tsx 的实现可以梳理出完整调用链选项归一化_resolveBlockerOptsuseBlocker.tsx负责把旧版blockerFn/condition参数转换为新的shouldBlockFn形态——旧版condition被包进Solid.createMemo保持响应式旧 API 因此被标记为deprecated注册 history blockeruseBlocker在Solid.createEffect中调用router.history.block({ blockerFn, enableBeforeUnload })注册自身useBlocker.tsx并在onCleanup中注销保证组件卸载后拦截器不泄漏位置解析与类型安全每次导航时组合函数先通过router.parseLocation与router.getMatchedRoutes把currentLocation/nextLocation解析为包含routeId、fullPath、params、search的类型化对象useBlocker.tsx这就是shouldBlockFn中能拿到精确类型的原因若目标路由不存在routeId记为__notFound__resolver 机制当withResolver: true且shouldBlockFn返回true时内部构造一个Promise并把status置为blockedproceed/reset分别resolve(false)/resolve(true)useBlocker.tsx导航被挂起直到 Promise 被解决随后status回到idle。从 history 层看block()将 blocker 推入一个共享数组packages/history/src/index.ts因此多个组件可以同时注册多个拦截器导航时会按注册顺序逐个询问而onbeforeunload则是任意一个启用即拦截。这就是layers of blockers设计的直观体现。注意事项与最佳实践shouldBlockFn支持异步签名是(args) boolean | Promiseboolean因此可以放在里面做异步校验如请求服务端确认但期间导航会一直挂起注意用户体验withResolver与返回值的关系withResolver: true时shouldBlockFn的返回值不影响结果只有proceed()/reset()能解决拦截不设withResolver时返回值直接决定是否拦截enableBeforeUnload默认开启如果不希望刷新/关页弹原生确认框务必显式设为false如示例根组件那样响应式写法Solid 中条件要写成信号调用形式value()并在组件内使用createSignal/createMemo驱动才能随状态实时生效拦截器生命周期useBlocker在组件卸载时自动注销若把拦截器放在根组件如示例的RootComponent它会对整个应用生效嵌套拦截叠加示例中根组件与Editor1同时注册了拦截器导航时会依次询问各自决定是否放行。想深入更多细节可继续阅读官方指南 docs/router/guide/navigation-blocking.md 与 API 参考 docs/router/api/router/useBlockerHook.md并对照 history 层实现 packages/history/src/index.ts 理解底层行为。【免费下载链接】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),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进