ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TanStack Router 404 与资源缺失处理全指南:`notFound`、`notFoundComponent` 与 `notFoundMode` 深度解析

TanStack Router 404 与资源缺失处理全指南:`notFound`、`notFoundComponent` 与 `notFoundMode` 深度解析 TanStack Router 404 与资源缺失处理全指南notFound、notFoundComponent与notFoundMode深度解析【免费下载链接】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本文基于当前仓库TanStack Router 及配套的 React / Solid / Vue 框架实现中 docs/router/guide/not-found-errors.md 展开围绕路由匹配失败404与资源缺失两类场景系统讲解notFound函数、notFoundComponent路由选项、notFoundMode全局策略以及从旧版NotFoundRoute的迁移路径并辅以 packages/router-core/src/not-found.ts、packages/router-core/src/router.ts 等源码证据帮助读者在实战中正确配置、抛出并捕获 not-found 错误。概述not-found 错误的两大来源在 TanStack Router 中not-found 错误404有且仅有两种用途二者在底层共用同一套notFound函数与notFoundComponentAPI路径不匹配Non-matching route paths当路径无法匹配任何已知路由模式或者仅部分匹配某路由但带有额外路径段时路由Router会自动抛出not-found 错误。若路由的notFoundMode为fuzzy默认值则由最近的可匹配路由中配置了notFoundComponent的那一个负责处理若notFoundMode为root则统一由根路由负责处理。典型示例访问/users但路由树中根本不存在/users路由访问/posts/1/edit但路由树只声明了/posts/$postId多出的/edit段无法匹配。资源缺失Missing resources当某个资源找不到例如指定 ID 的文章不存在、异步数据不可用等场景。开发者需要自行在beforeLoad或loader中调用notFound工具函数抛出 not-found 错误。当在loader中调用时由最近的、配置了notFoundComponent的路由处理否则回退到根路由处理。典型示例访问/posts/1但 ID 为 1 的文章不存在访问/docs/path/to/document但该文档不存在。从路由自动抛错到开发者手动抛错最终都收敛为同一种错误对象与同一套渲染边界这正是新 API 相比旧NotFoundRoute更统一、更可控的原因。notFoundMode全局 404 处理策略当 TanStack Router 遇到无法匹配任何已知路由模式的 pathname或部分匹配但存在多余尾部 pathname 段时会自动抛出一个 not-found 错误。此时如何处理取决于路由器选项notFoundMode模式行为适用场景fuzzy默认智能寻找最近的可匹配路由渲染该路由的notFoundComponent尽量保留父级布局让用户基于原本想去的位置就近获得导航上下文root所有 not-found 错误一律由根路由的notFoundComponent处理希望全局统一 404 页面、不关心就近路由notFoundMode: fuzzy默认默认情况下notFoundMode为fuzzy若 pathname 未匹配任何已知路由路由器会尝试使用最近匹配的、且配置了notFoundComponent的路由。为什么这是默认值模糊匹配尽可能多地保留父级布局用户能借助保留的上下文导航到有用位置而不是直接跌落到一个与当前位置毫无关联的全新页面。最近合适路由的查找标准为该路由必须配置了notFoundComponent或者路由器配置了defaultNotFoundComponent。例如给定如下路由树__root__配置了notFoundComponentposts配置了notFoundComponent$postId配置了notFoundComponent若访问/posts/1/edit将渲染如下组件结构Root Posts Post Post.notFoundComponent因为$postId是最近的、配置了notFoundComponent的匹配路由所以渲染的是它的notFoundComponent。这一查找逻辑在源码中有精确对应findGlobalNotFoundRouteId见 packages/router-core/src/router.ts从已匹配路由数组的末尾最深层路由向前遍历一旦发现route.options.notFoundComponent就立即返回该路由 id若没有找到任何配置了notFoundComponent的路由则回退到具有子路由的最近分支最终仍无果时才回落到rootRouteIdfunction findGlobalNotFoundRouteId( notFoundMode: root | fuzzy | undefined, routes: ReadonlyArrayAnyRoute, ) { if (notFoundMode ! root) { let fallback for (let i routes.length - 1; i 0; i--) { const route routes[i]! if (route.options.notFoundComponent) { return route.id } fallback || route.children route.id } if (fallback) { return fallback } } return rootRouteId }notFoundMode: root当notFoundMode设为root时所有 not-found 错误都由根路由的notFoundComponent处理而不是从最近模糊匹配的路由向上冒泡。以上述同一路由树为例访问/posts/1/edit时将渲染Root Root.notFoundComponent因为模式为root所以渲染的是__root__路由的notFoundComponent。从源码可见root模式直接跳过最近的模糊匹配遍历无条件返回rootRouteId。notFoundMode的默认值在 packages/router-core/src/router.ts 处确认notFoundMode: options.notFoundMode ?? fuzzy。配置路由的notFoundComponent为同时处理上述两类 not-found 错误可以给任意路由挂载notFoundComponent。该组件会在 not-found 错误被抛出时渲染。例如为/settings路由配置notFoundComponent处理不存在的设置子页面export const Route createFileRoute(/settings)({ component: () { return ( div pSettings page/p Outlet / /div ) }, notFoundComponent: () { return pThis setting page doesnt exist!/p }, })或者为/posts/$postId路由配置notFoundComponent处理不存在的文章export const Route createFileRoute(/posts/$postId)({ loader: async ({ params: { postId } }) { const post await getPost(postId) if (!post) throw notFound() return { post } }, component: ({ post }) { return ( div h1{post.title}/h1 p{post.body}/p /div ) }, notFoundComponent: () { return pPost not found!/p }, })注意这里的notFoundComponent不是路由组件它在渲染时会被wrapInNonRouteComponentContext包裹见 packages/react-router/src/renderRouteNotFound.tsx因此不支持渲染Outlet /且与普通路由组件的数据访问方式存在差异详见下文notFoundComponent中的数据加载一节。全局默认 not-found 处理defaultNotFoundComponent你可能会希望为应用中所有带有子路由的路由提供默认的 not-found 组件。为什么只针对有子路由的路由叶子路由无子路由的路由永远不会渲染Outlet因此无法处理 not-found 错误。为此把defaultNotFoundComponent传给createRouter即可const router createRouter({ defaultNotFoundComponent: () { return ( div pNot found!/p Link to/Go home/Link /div ) }, })该选项的声明位于 packages/react-router/src/router.ts。渲染优先级在 packages/react-router/src/renderRouteNotFound.tsx 中清晰可见优先使用路由自身的notFoundComponent未配置时回退到路由器级defaultNotFoundComponent两者都没有时回退到 TanStack Router 内置的DefaultGlobalNotFound——即那个刻意保持极其简陋且故意不美观的默认组件仅渲染pNot Found/p实现见 packages/react-router/src/not-found.tsx。源码中还会在开发环境打印一条警告提示开发者为路由配置notFoundComponent或路由器级defaultNotFoundComponent以避免落到过度通用的默认组件。强烈建议至少为根路由挂载一个notFoundComponent或配置路由器级defaultNotFoundComponent。手动抛出notFound错误除了路由自动抛错你也可以在 loader 和组件中手动抛出 not-found 错误用于标记资源不存在。notFound函数与redirect函数的工作方式类似——抛出notFound()即可触发 not-found 错误export const Route createFileRoute(/posts/$postId)({ loader: async ({ params: { postId } }) { // 文章不存在时返回 null const post await getPost(postId) if (!post) { throw notFound() // 或者让 notFound 函数自行抛出 // notFound({ throw: true }) } // 走到这里post 一定有值因为不存在时已抛出错误 return { post } }, })从源码实现看packages/router-core/src/not-found.tsnotFound函数的核心逻辑是给选项对象打上内部标记isNotFound true然后根据throw选项决定直接抛出还是返回错误对象而isNotFound(obj)则用于运行时判别某个值是否是 TanStack Router 的 not-found 错误obj?.isNotFound true这也是CatchNotFound等边界组件判断错误类型的依据export function notFound(options: NotFoundError {}) { ;(options as any).isNotFound true if (options.throw) throw options return options } export function isNotFound(obj: any): obj is NotFoundError { return obj?.isNotFound true }上述手动抛出的 not-found 错误将由该路由自身或最近的、配置了notFoundComponent路由选项或defaultNotFoundComponent路由器选项的父级路由处理。若既没有找到合适的路由也没有合适的父级路由处理该错误则根路由会使用 TanStack Router 内置的极简默认 not-found 组件仅渲染pNot Found/p来处理。在beforeLoad中抛出时的行为当你或任何库代码在beforeLoad中抛出notFound()时TanStack Router 会像处理其他 not-found 错误一样解析它若传入了routeId由该路由或最近的合法祖先边界处理若未传入routeId由最近的、配置了notFoundComponent的路由/祖先处理取决于路由器的模式和匹配规则若找不到合适的边界则回退到根路由/默认 not-found 行为。对于beforeLoad中抛出的 not-found 错误TanStack Router 仍会运行必需的父级 loader以确保选中的 not-found 边界能带着它依赖的 loader 数据正常渲染。指定哪些路由处理 not-found 错误有时你希望在某个特定的父级路由上触发 not-found并绕过常规的 not-found 组件传播逻辑。此时可在notFound函数的route选项中传入目标路由 id// _pathlessLayout.tsx export const Route createFileRoute(/_pathlessLayout)({ // 这个会渲染 notFoundComponent: () { return pNot found (in _pathlessLayout)/p }, component: () { return ( div pThis is a pathless layout route!/p Outlet / /div ) }, }) // _pathlessLayout/route-a.tsx export const Route createFileRoute(/_pathless/route-a)({ loader: async () { // 让 LayoutRoute 处理这个 not-found 错误 throw notFound({ routeId: /_pathlessLayout }) // ^^^^^^^^^ 会从注册的 router 中自动补全 }, // 这个不会渲染 notFoundComponent: () { return pNot found (in _pathlessLayout/route-a)/p }, })routeId的类型为RouteIdsRegisteredRouter[routeTree]见 packages/router-core/src/not-found.ts在配置了注册路由器的项目中该字段会获得完整的类型自动补全与校验。手动指定根路由你也可以通过向notFound函数的route属性传入导出的rootRouteId变量来指定根路由export const Route createFileRoute(/posts/$postId)({ loader: async ({ params: { postId } }) { const post await getPost(postId) if (!post) throw notFound({ routeId: rootRouteId }) return { post } }, })rootRouteId由 packages/router-core/src/root.ts 导出并经由 packages/router-core/src/index.ts 对外暴露。值得留意的是NotFoundError类型中有一个deprecated的global选项见 packages/router-core/src/not-found.ts其注释明确建议改用routeId: rootRouteId——说明旧式的全局 404概念已被显式路由定位取代。在组件中抛出 not-found 错误你也可以在组件中抛出 not-found 错误。不过官方建议优先在 loader 中抛出以便正确推导 loader 数据类型并避免闪烁flickering。TanStack Router 提供了与CatchBoundary类似的CatchNotFound组件用于在组件中捕获 not-found 错误并展示对应 UI。其实现位于 packages/react-router/src/not-found.tsx内部复用CatchBoundary在onCatch与错误渲染分支中通过isNotFound(error)判断错误类型——若是 not-found 错误则执行fallback/onCatch否则原样重新抛出同时基于pathname与路由状态生成resetKey确保导航到新路径后边界能够正确重置。import { CatchNotFound } from tanstack/react-router function Posts() { return ( CatchNotFound fallback{(error) pPost not found./p} PostDetail / /CatchNotFound ) }该组件已从 packages/react-router/src/index.tsx 对外导出同时导出的还有DefaultGlobalNotFound。notFoundComponent中的数据加载notFoundComponent在数据加载方面是一个特例SomeRoute.useLoaderData可能未定义具体取决于你正在访问哪个路由、以及 not-found 错误是在哪里抛出的。而Route.useParams、Route.useSearch、Route.useRouteContext等 Hook 会返回确定的值。若需要把不完整的 loader 数据传给notFoundComponent可通过notFound函数中的data选项传递并在notFoundComponent中自行校验export const Route createFileRoute(/posts/$postId)({ loader: async ({ params: { postId } }) { const post await getPost(postId) if (!post) throw notFound({ // 把部分数据转发给 notFoundComponent // data: someIncompleteLoaderData }) return { post } }, // 调用 notFound 时通过 data 选项传入的数据会以 { data } 形式到达组件 notFoundComponent: ({ data }) { // ❌ 这里不能使用 useLoaderDataconst { post } Route.useLoaderData() // ✅ 这些 Hook 是安全的 const { postId } Route.useParams() const search Route.useSearch() const context Route.useRouteContext() return pPost with id {postId} not found!/p }, })对应的data字段定义在 packages/router-core/src/not-found.ts类型为anynotFoundComponent渲染时该数据会作为 props 传入见 packages/react-router/src/renderRouteNotFound.tsx。注意NotFoundError中还支持headers?: HeadersInit字段可用于在 SSR 等场景下附带响应头信息。与 SSR 的配合not-found 错误处理在服务端渲染SSR场景下同样生效路由匹配失败或 loader 中抛出的notFound()会在服务端被捕获并将状态同步到客户端完成一致的 404 渲染。详细的服务端渲染配置与流程请参阅 SSR 指南。从源码结构看服务端的加载流程同样参与了 not-found 判定notFoundMode相关逻辑同时存在于 packages/router-core/src/load-client.ts、packages/router-core/src/load-server.ts 与 packages/router-core/src/router.ts 中确保客户端与服务端对由谁处理 404的决策保持一致。从NotFoundRoute迁移NotFoundRouteAPI 已废弃未来版本将移除请改用notFoundComponent。重要使用NotFoundRoute时notFound函数与notFoundComponent将不会生效——二者是互斥的两套机制。两者主要差异如下对比维度NotFoundRoute已废弃notFoundComponent推荐形态一个独立的路由可挂载到任意路由的组件选项渲染前提父路由必须渲染Outlet无此要求自动插入渲染布局支持无法使用布局layout可与布局layout配合使用路径匹配宽松/post/1/2/3会匹配NotFoundRoute严格声明了/post/$postId时访问/post/1/2/3会抛 not-found 错误Outlet 渲染支持不支持迁移只需几步修改。以src/router.tsx为例import { createRouter } from tanstack/react-router import { routeTree } from ./routeTree.gen. - import { notFoundRoute } from ./notFoundRoute // [!code --] export const router createRouter({ routeTree, - notFoundRoute // [!code --] }) // routes/__root.tsx import { createRootRoute } from tanstack/react-router export const Route createRootRoute({ // ... notFoundComponent: () { // [!code ] return pNot found!/p // [!code ] } // [!code ] })迁移中的关键变化在根路由上添加notFoundComponent用于全局 not-found 处理也可以在路由树中的任意其他路由上添加notFoundComponent以处理该路由专属的 not-found 错误移除传给createRouter的notFoundRoute选项及其导入牢记notFoundComponent不支持渲染Outlet /原有依赖NotFoundRouteOutlet的布局结构需要调整为在父路由的component中保留Outlet /同时把 404 展示逻辑迁入notFoundComponent。【免费下载链接】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),仅供参考
返回列表