TanStack Router 认证路由实战:基于 React 的受保护路由、路由守卫与登录重定向完整实现

发布时间:2026/9/15 12:39:04
TanStack Router 认证路由实战:基于 React 的受保护路由、路由守卫与登录重定向完整实现 TanStack Router 认证路由实战基于 React 的受保护路由、路由守卫与登录重定向完整实现【免费下载链接】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/react/authenticated-routes 示例系统讲解 TanStack Router本仓库即该项目的 React 实现中认证流程与受保护路由的标准实现方式。示例完整覆盖了 AuthProvider 认证状态管理、根路由上下文注入、beforeLoad路由守卫、登录后重定向携带redirect搜索参数以及登出后路由失效router.invalidate()等核心链路。读完本文你将掌握如何在 TanStack Router 中以路径前缀布局 守卫钩子的架构搭建一套可复用的认证路由体系并理解其底层调用机制与类型安全设计。一、示例概览它演示了什么该示例是一个基于 Vite React 19 TanStack Router 的完整可运行应用依赖配置见 package.jsonRouter 版本^1.170.35核心演示内容包括认证流程登录 / 登出的完整状态流转受保护路由Protected routes未登录用户无法访问的页面集合路由守卫Route guards通过beforeLoad在路由进入前拦截登录 / 登出功能带模拟网络延迟的异步实现认证后重定向登录成功后跳回用户原本想访问的页面公共路由 vs 私有路由以路径前缀_auth区分的布局分组。值得强调的是示例首页src/routes/index.tsx中明确提示这只是一个演示认证路由用法的示例并非生产级认证系统的参考实现。真实项目中应将这里的模式与自己的认证体系如 Session、JWT、OAuth 服务端校验结合。二、快速开始基于示例创建新项目使用gitpick直接从仓库创建npx gitpick TanStack/router/tree/main/examples/react/authenticated-routes authenticated-routes安装依赖并启动pnpm install pnpm dev开发服务器默认运行在3000端口见 package.json 中dev: vite --port 3000。生产构建与类型检查pnpm buildbuild脚本实际执行vite build tsc --noEmit即打包的同时做严格类型检查——这正体现了 TanStack Router全程类型安全的设计理念路由树、上下文、搜索参数在编译期即可被完整校验。三、项目结构与路由树examples/react/authenticated-routes/ └── src/ ├── main.tsx # 应用入口创建 Router、注册类型、挂载 Provider ├── auth.tsx # AuthContext / AuthProvider / useAuth ├── posts.tsx # 模拟数据源jsonplaceholder 前 10 条 loader 延迟 ├── utils.ts # sleep 工具函数 ├── routeTree.gen.ts # 由 router-plugin 自动生成的路由树 └── routes/ ├── __root.tsx # 根路由声明带 auth 上下文的 RouterContext ├── index.tsx # 公共首页 / ├── login.tsx # 公共登录页 /login ├── _auth.tsx # 受保护布局守卫 /_auth ├── _auth.dashboard.tsx # 受保护页 /dashboard ├── _auth.invoices.tsx # 受保护嵌套布局 /invoices带 loader ├── _auth.invoices.index.tsx # 默认子路由 └── _auth.invoices.$invoiceId.tsx # 动态参数子路由路由布局的完整形态最终由tanstack/router-plugin依据文件系统自动生成到 routeTree.gen.ts开发者通常无需手写路由树。四、认证状态管理AuthProvider 与 useAuth认证状态由 React Context 承载实现在 src/auth.tsxexport interface AuthContext { isAuthenticated: boolean login: (username: string) Promisevoid logout: () Promisevoid user: string | null } const AuthContext React.createContextAuthContext | null(null)关键设计点localStorage 持久化以tanstack.auth.user为 key 存储用户名页面刷新后通过getStoredUser()恢复登录态并在useEffect中重新同步到 state异步登录 / 登出login/logout分别通过sleep(500)/sleep(250)模拟网络延迟utils.ts 中的sleep让 UI 层可以真实体验提交中的加载态useAuth守卫useAuth在 Provider 之外调用时会抛出useAuth must be used within an AuthProvider从源头约束了使用边界。五、把认证上下文注入 Router根路由与入口装配5.1 根路由声明上下文类型src/routes/__root.tsx 中通过createRootRouteWithContext声明 Router 上下文的类型形状interface MyRouterContext { auth: AuthContext } export const Route createRootRouteWithContextMyRouterContext()({ component: () ( Outlet / TanStackRouterDevtools positionbottom-right initialIsOpen{false} / / ), })这是整个类型安全链路的起点MyRouterContext的类型一旦声明所有路由的beforeLoad中context.auth都会获得完整的类型推导。5.2 入口装配先 Provider 后 Routersrc/main.tsx 的装配顺序至关重要const router createRouter({ routeTree, defaultPreload: intent, scrollRestoration: true, context: { auth: undefined!, // 先占位待包裹 AuthProvider 后再注入 }, }) declare module tanstack/react-router { interface Register { router: typeof router } } function InnerApp() { const auth useAuth() return RouterProvider router{router} context{{ auth }} / } function App() { return ( AuthProvider InnerApp / /AuthProvider ) }需要理解的两个要点创建 Router 时无法直接拿到 ContextcreateRouter在组件树之外执行因此先用undefined!占位RouterProvider的context属性运行时注入InnerApp在AuthProvider内部调用useAuth()拿到真实认证状态再通过context{{ auth }}传入 Router。运行时上下文与根路由声明的类型保持一致最终实现编译期类型安全 运行期状态共享。六、路由守卫用 beforeLoad 保护整个布局6.1 受保护布局/_authsrc/routes/_auth.tsx 是整个示例的核心。文件名的下划线前缀_auth表示路径段不参与 URL因此/dashboard、/invoices等子路由在 URL 中并不会出现/_auth但它们在路由树中都属于_auth布局的后代——这就是 TanStack Router 实现一组路由共享一个守卫的惯用法export const Route createFileRoute(/_auth)({ beforeLoad: ({ context, location }) { if (!context.auth.isAuthenticated) { throw redirect({ to: /login, search: { redirect: location.href, }, }) } }, component: AuthLayout, })守卫逻辑的要点beforeLoad在路由加载组件渲染与数据 loader 执行之前运行因此未登录用户连受保护页面的 loader 都不会触发避免了不必要的请求拦截手段是throw redirect(...)这是 TanStack Router 推荐的以异常中断导航的方式与return返回新目标相比能确保后续 loader/组件彻底不执行重定向时通过search: { redirect: location.href }携带原始目标地址为登录后的回跳提供依据。6.2 布局组件登出与嵌套出口AuthLayout渲染导航Dashboard / Invoices 链接与登出按钮并保留Outlet /作为嵌套路由出口。登出的完整流程体现了状态变更后必须使路由数据失效的关键实践const handleLogout () { if (window.confirm(Are you sure you want to logout?)) { auth.logout().then(() { router.invalidate().finally(() { navigate({ to: / }) }) }) } }router.invalidate()会重新执行当前匹配路由树上的 loader 并刷新 UI 数据确保登出后任何基于登录态缓存的数据被清掉随后再导航回首页。七、登录页重定向回跳与表单处理src/routes/login.tsx 演示了两件事守卫的反向使用与重定向回跳。7.1 用 zod 校验 search 参数export const Route createFileRoute(/login)({ validateSearch: z.object({ redirect: z.string().optional().catch(), }), ... })TanStack Router 支持用验证器Validator解析搜索参数这里使用zod声明redirect为可选的字符串并通过.catch()在异常时回退为空字符串保证beforeLoad与组件中读取search.redirect永远安全。7.2 已登录用户访问登录页时反向重定向const fallback /dashboard as const beforeLoad: ({ context, search }) { if (context.auth.isAuthenticated) { throw redirect({ to: search.redirect || fallback }) } },这与_auth的守卫正好互补未登录访问私有页 → 踢到登录页已登录访问登录页 → 直接送回目标页或默认的/dashboard。7.3 登录成功后的回跳流程const onFormSubmit async (evt) { setIsSubmitting(true) try { evt.preventDefault() const data new FormData(evt.currentTarget) const fieldValue data.get(username) if (!fieldValue) return const username fieldValue.toString() await auth.login(username) await router.invalidate() await sleep(1) // 等待 auth 状态更新示例中的简单 hack await navigate({ to: search.redirect || fallback }) } catch (error) { console.error(Error logging in: , error) } finally { setIsSubmitting(false) } }完整链路为auth.login()更新 Context →router.invalidate()让受保护路由的守卫重新评估 → 短暂等待状态同步 →navigate到search.redirect即用户被拦截前的地址或默认/dashboard。同时useRouterState({ select: (s) s.isLoading })驱动按钮的 Loading 态配合fieldset disabled防止重复提交。八、受保护页面与数据加载8.1 Dashboard_auth.dashboard.tsx 通过useAuth()读取当前用户并渲染Hi {auth.user}!是受保护布局内消费认证上下文的最小示例。8.2 Invoices嵌套布局 loader 动态参数_auth.invoices.tsx 展示了带数据加载的嵌套路由export const Route createFileRoute(/_auth/invoices)({ loader: async () ({ invoices: await fetchInvoices(), }), component: InvoicesRoute, })组件内用Route.useLoaderData()消费 loader 数据左列渲染发票链接列表Link to/invoices/$invoiceId params{{ invoiceId: invoice.id.toString() }} /右侧通过Outlet /渲染子路由。子路由 _auth.invoices.$invoiceId.tsx 演示动态路径参数loader 从params.invoiceId读取参数并调用fetchInvoiceById找不到时抛出Invoice not found。数据源 _posts.tsx 通过redaxios请求jsonplaceholder的/posts并缓存前 10 条同时支持sessionStorage中的loaderDelay键来控制模拟加载延迟便于调试骨架屏/加载态。九、底层原理守卫为什么在 loader 之前执行从源码结构看beforeLoad之所以能成为守卫是因为 TanStack Router 的路由匹配流程严格按照beforeLoad→loader→component的顺序推进beforeLoad抛出的redirect会以异常形式中断匹配管线使后续的 loader 数据请求与组件渲染全部短路router-core 中redirect的判定与beforeLoad执行逻辑可印证这一点。这正是该示例架构的底层保障——认证检查永远先于任何数据请求发生从而在逻辑层面杜绝了未授权数据泄露。十、实战要点总结关注点示例中的实现位置认证状态容器AuthProvideruseAuthContext localStoragesrc/auth.tsx上下文类型声明createRootRouteWithContextMyRouterContextsrc/routes/__root.tsx运行时注入RouterProvider context{{ auth }}src/main.tsx受保护分组布局文件前缀_auth路径段不占 URLsrc/routes/_auth.tsx守卫钩子beforeLoadthrow redirect同上重定向回跳search.redirectzod 校验navigatesrc/routes/login.tsx登出刷新router.invalidate()后导航src/routes/_auth.tsx数据加载布局级loader 动态参数 loadersrc/routes/_auth.invoices.tsx将这套模式落地到生产项目时建议把auth.login/auth.logout替换为真实的接口调用与会话校验如 HttpOnly Cookie 服务端会话并将localStorage持久化替换为更安全的会话方案但根上下文注入 beforeLoad 守卫 redirect 回跳 invalidate 刷新这一整套路由层骨架可以直接复用。【免费下载链接】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),仅供参考