路由✅
React Router 中 mode 有哪几种有什么区别?
答案
Mode 就是使用 React Router 的方式,分为三种
- Declarative 基本使用,支持基础路由导航等逻辑
- Data 配置化使用,额外支持 loader、action 等能力
- Framework 框架化使用,支持不同渲染策略,如 SSR、SPA、RSC 等
- DeclarativeMode
- DataMode
- FrameworkMode
import { HashRouter, Routes, Route, Link } from 'react-router' function Home () { return <div>首页</div> } function About () { return <div>关于</div> } export default function App () { return ( <HashRouter> <nav style={{ display: 'flex', gap: '10px' }}> <Link to="/">首页</Link> <Link to="/about">关于</Link> </nav> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> </Routes> </HashRouter> ) }
import { createBrowserRouter, RouterProvider, Link } from 'react-router' function Layout ({ children }) { return ( <div> <nav style={{ display: 'flex', gap: '10px' }}> <Link to="/">首页</Link> <Link to="/about">关于</Link> </nav> <div>{children}</div> </div> ) } function Home () { return <div>首页</div> } function About () { return <div>关于</div> } async function loader () { // 模拟数据加载 return { message: '数据已加载' } } async function action ({ request }) { // 模拟表单处理 return { result: '表单已提交' } } const router = createBrowserRouter([ { path: '/', element: <Layout><Home /></Layout>, loader, action }, { path: '/about', element: <Layout><About /></Layout>, loader, action } ]) export default function App () { return <RouterProvider router={router} /> }
import { createFrameworkRouter, index, route, Link } from '@react-router/dev' const Layout = ({ children }) => ( <div> <nav style={{ display: 'flex', gap: '10px', marginBottom: '20px' }}> <Link to="/">首页</Link> <Link to="/about">关于</Link> </nav> <div>{children}</div> </div> ) const Home = () => ( <div> <h1>🏠 首页</h1> <p>欢迎来到首页。</p> </div> ) const About = () => ( <div> <h1>📘 关于页面</h1> <p>这里是关于我们的介绍。</p> </div> ) // 2️⃣ 路由配置:Framework Mode 虚拟路径 + 内联组件 const routes = [ index(() => ( // Corrected: No path needed for the root index <Layout> <Home /> </Layout> )), route('about', () => ( // Corrected: 'about' is the path <Layout> <About /> </Layout> )) ] // 3️⃣ 创建 Router (Framework 模式) const Router = createFrameworkRouter({ routes }) export default Router
更详细的区别可以参考 API 和 Mode 说明
BrowserRouter、 HashRouter 、MemoryRouter 有什么区别?
答案
路由器 | URL 表现 | 原理 | 典型场景 | 是否依赖服务器 |
---|---|---|---|---|
BrowserRouter | 普通路径 /about | HTML5 history API | SPA、生产环境 | 是 |
HashRouter | #/about | 监听 location.hash | 静态站点、无服务端 | 否 |
MemoryRouter | 无 URL 变化 | 内存中维护 history | 测试、非浏览器环境 | 否 |
- BrowserRouter
- HashRouter
- MemoryRouter
import { BrowserRouter, Route, Link, Routes, useLocation } from 'react-router' import { useEffect } from 'react' function App () { // eslint-disable-next-line let location = useLocation() useEffect(() => { console.log('✅ 路由变化,当前地址为:', window?.location?.href) }, [location]) return ( <> <nav style={{ display: 'flex', gap: '10px' }}> <Link to="/">首页</Link> <Link to="/about">关于</Link> </nav> <Routes> <Route path="/" element={<div>首页</div>} /> <Route path="/about" element={<div>关于</div>} /> </Routes> </> ) } export default function BrowserRouterApp () { return ( <BrowserRouter> <App /> </BrowserRouter> ) }
import { HashRouter, Routes, Route, Link, useLocation } from 'react-router' import { useEffect } from 'react' function App () { // eslint-disable-next-line let location = useLocation() useEffect(() => { console.log('✅ 路由变化,当前地址为:', window?.location?.href) }, [location]) return ( <> <nav style={{ display: 'flex', gap: '10px' }}> <Link to="/">首页</Link> <Link to="/about">关于</Link> </nav> <Routes> <Route path="/" element={<div>首页</div>} /> <Route path="/about" element={<div>关于</div>} /> </Routes> </> ) } export default function HashRouterApp () { return ( <HashRouter> <App /> </HashRouter> ) }
import { MemoryRouter, Route, Link, Routes, useLocation } from 'react-router' import { useEffect } from 'react' function App () { // eslint-disable-next-line let location = useLocation() useEffect(() => { console.log('✅ 路由变化,当前地址为:', window?.location?.href) }, [location]) return ( <> <nav style={{ display: 'flex', gap: '10px' }}> <Link to="/">首页</Link> <Link to="/about">关于</Link> </nav> <Routes> <Route path="/" element={<div>首页</div>} /> <Route path="/about" element={<div>关于</div>} /> </Routes> </> ) } export default function MemoryRouterApp () { return ( <MemoryRouter> <App /> </MemoryRouter> ) }
提示
注意对比三个 Demo 输出路径区别可以直观反应不同模式对 url 的影响, 对于 BrowserRouter 需要服务端配置路由重写逻辑,以 nginx 为例
location / {
try_files $uri $uri/ /index.html;
}
延伸阅读
React Router 的路径支持哪些匹配模式?
答案
核心匹配逻辑如下
匹配模式 | 描述 | 示例 |
---|---|---|
精确匹配 | 完全匹配路径 | /about 只匹配 /about |
动态片段 | 匹配动态路径 | /users/:id 匹配 /users/123 ,123 可以通过 const { id } = useParams() 获取 |
可选片段 | 匹配可选路径 | /users/:id? 匹配 /users/123 和 /users/ |
通配符 | 匹配任意路径 | /users/* 匹配 /users/123 、/users/abc 等, 注意通配符只能有一个且出现在最后一层, 可以通过 const { '*': wildcard } = useParams() 获取 |
索引路由 | 匹配父路径 | /users/* 中的 /users 是索引路由,匹配 /users , 效果和嵌套路由中配置 path='' 等效,但是 index 路由更清晰 |
组合模式 | 支持多种匹配 | /users/:id/* 匹配 /users/123/details ,可以获取 id 和通配符部分 |
import { HashRouter, Routes, Route, Link, useParams } from 'react-router' function User () { const { id, status } = useParams() return ( <div> {!!id && <strong>用户 ID: {id}</strong>} {!!status && <strong>状态: {status}</strong>} </div> ) } function Spalt () { const { '*': splat } = useParams() return <div>{!!splat && <strong>{splat}</strong>}</div> } function Multi () { const { '*': splat, size, type } = useParams() return ( <div> {!!splat && <strong>{splat}</strong>} {!!size && <strong>大小: {size}</strong>} {!!type && <strong>类型: {type}</strong>} </div> ) } // 验证 react-router 支持的路由配置 const RouteConfig = [ { path: '/', element: <div>首页</div> }, { path: '/about', element: <div>关于</div> }, { path: '/user/:id', to: '/user/1', // 默认重定向到用户 ID 1 element: <User /> }, { path: '/user/:id/profile/:status?', to: ['/user/2/profile/edit', '/user/2/profile'], element: <User /> }, { path: '/spalt/*', to: ['/spalt', '/spalt/:test', '/spalt/2/3'], // 默认重定向到用户 ID 1 element: <Spalt /> }, { path: '/multi/file/:size/:type?/*', to: ['/multi/file/0', '/multi/file/10KB/png/a.png', '/multi/file/20KB/b.jpg', '/multi/file/20KB/pdf/d/g.pdf'], element: <Multi /> } ] function App () { return ( <> <nav style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}> {RouteConfig.map((route, index) => Array.isArray(route.to) ? ( route.to.map((link) => ( <Link key={link} to={link}> {link} </Link> )) ) : ( <Link key={index} to={route.to ?? route.path}> {route.to ?? route.path} </Link> ) )} </nav> <Routes> {RouteConfig.map(({ path, ...options }, index) => ( <Route key={index} path={path} {...options} /> ))} </Routes> </> ) } export default function HashRouterApp () { return ( <HashRouter> <App /> </HashRouter> ) }
提示
注意当存在多个匹配规则时,遵循如下规则
- 越精确越优先匹配
- 若优先级相同则按定义顺序匹配
React Router 内部实际上是通过一个权重计算来评估匹配的优先级,具体代码详见 computeScore 各规则的分值如下,多条会进行累加计算。
const dynamicSegmentValue = 3 // 动态片段
const indexRouteValue = 2 // 索引路由
const emptySegmentValue = 1 // 空片段
const staticSegmentValue = 10 // 静态路由例如 user
const splatPenalty = -2 // 通配符路由 *
如何监听路由变化?
答案
React Router 提供了 useLocation
钩子来获取当前路由信息,并可以通过 useEffect
钩子监听路由变化。
import { BrowserRouter, Route, Link, Routes, useLocation } from 'react-router' import { useEffect } from 'react' function App () { // eslint-disable-next-line let location = useLocation() useEffect(() => { console.log('✅ 路由变化,当前地址为:', window?.location?.href) }, [location]) return ( <> <nav style={{ display: 'flex', gap: '10px' }}> <Link to="/">首页</Link> <Link to="/about">关于</Link> </nav> <Routes> <Route path="/" element={<div>首页</div>} /> <Route path="/about" element={<div>关于</div>} /> </Routes> </> ) } export default function BrowserRouterApp () { return ( <BrowserRouter> <App /> </BrowserRouter> ) }
延伸阅读
react-router 页面间如何传递信息?
答案
方式 | 传递入口 | 获取方式 | 典型场景 | 持久性 |
---|---|---|---|---|
路由参数 | /users/:id | useParams() | 资源唯一标识 | URL 可见,刷新保留 |
查询参数 | /users?id=123 | useSearchParams() | 筛选、分页 | URL 可见,刷新保留 |
路由状态 | navigate('/a', {state}) | useLocation().state | 页面跳转携带临时数据 | 内存,刷新丢失 |
上下文 | Context | useContext | 全局/多层级共享 | 依赖组件树 |
import { BrowserRouter, Routes, Route, useParams, useSearchParams, useNavigate, Link } from 'react-router' import { useContext, createContext } from 'react' // ---------- 上下文定义 ---------- const UserContext = createContext({ name: 'DefaultUser' }) // ---------- 页面组件:主页 ---------- function Home () { const navigate = useNavigate() return ( <div> <h2>首页</h2> <ul> <li> <Link to="/user/123?tab=profile">查看用户 123(带查询参数)</Link> </li> <li> <button onClick={() => { navigate('/user/456', { state: { source: 'FromHomeButton' } }) }} > 跳转用户 456(带状态) </button> </li> </ul> </div> ) } // ---------- 页面组件:用户 ---------- function UserPage () { const { id } = useParams() const [query, setQuery] = useSearchParams() const tab = query.get('tab') || 'default' const userCtx = useContext(UserContext) const handleChangeTab = (newTab) => { // 修改查询参数,同时保留其他参数 query.set('tab', newTab) setQuery(query, { replace: true }) // 避免 push 到历史记录 } return ( <div> <h2>👤 用户页</h2> <p>路径参数 id: {id}</p> <p>查询参数 tab: <strong>{tab}</strong></p> <p>上下文用户名称: {userCtx.name}</p> <div> 切换 Tab: <button onClick={() => handleChangeTab('profile')}>Profile</button> <button onClick={() => handleChangeTab('settings')}>Settings</button> </div> <div style={{ marginTop: '10px' }}> <Link to="/">← 返回首页</Link> </div> </div> ) } // ---------- 顶层组件 ---------- export default function AppRouter () { const user = { name: 'TomUser' } return ( <UserContext.Provider value={user}> <BrowserRouter> <Routes> <Route path="/" element={<Home />} /> <Route path="/user/:id" element={<UserPage />} /> </Routes> </BrowserRouter> </UserContext.Provider> ) }
提示
在实际开发中建议遵循 Restful 风格,优先考虑通过 URL 映射资源,避免使用内部状态保存一些筛选信息等,这样的好处是用户实际使用应用时,比如搜索或者查询时,URL 记录了用户的操作状态便于链接的分享和保存,避免由于存储在内存导致需要重新点选才能获取结果。
延伸阅读
React Router 如何实现类似 Vue 导航守卫效果?
答案
Vue 的导航守卫包含全局守卫、路由独享守卫和组件内守卫。 React Router 原生并不支持此能力,典型策略如下
- 组件导航守卫 通过 Hoc 包裹组件,在包裹组件中实现逻辑判断
- 路由导航守卫 通过
loader
和action
实现路由级别的守卫逻辑 - 全局导航守卫 通过
useEffect
钩子监听路由变化,结合useLocation
获取当前路由信息注册在全局页面实现
- ComponentGuard
- RouteGuard
- GlobalGuard
import { BrowserRouter, Routes, Route, Navigate, useLocation, useNavigate, Link } from 'react-router' import { useState, createContext, useContext } from 'react' // ---------- 模拟 Auth 状态 ---------- const AuthContext = createContext(null) function useAuth () { return useContext(AuthContext) } // ---------- 登录页 ---------- function LoginPage () { const navigate = useNavigate() const location = useLocation() const { setLogin } = useAuth() const from = location.state?.from?.pathname || '/' function handleLogin () { setLogin(true) navigate(from, { replace: true }) // 登录成功后返回原始页面 } return ( <div> <h2>🔐 登录页</h2> <p>登录后将跳转到: <code>{from}</code></p> <button onClick={handleLogin}>点我登录</button> </div> ) } // ---------- 受保护页 ---------- function Dashboard () { return ( <div> <h2>📊 仪表盘(受保护)</h2> <Link to="/">返回首页</Link> </div> ) } // ---------- 守卫高阶组件 ---------- function withAuthGuard (Component) { return function Guarded (props) { const { isLogin } = useAuth() const location = useLocation() if (!isLogin) { return <Navigate to="/login" state={{ from: location }} replace /> } return <Component {...props} /> } } // ---------- 首页 ---------- function Home () { const { isLogin, setLogin } = useAuth() return ( <div> <h2>🏠 首页</h2> <p>当前状态: {isLogin ? '已登录 ✅' : '未登录 ❌'}</p> <Link to="/dashboard">访问仪表盘</Link> <br /> <button onClick={() => setLogin(false)}>退出登录</button> </div> ) } const ProtectedDashboard = withAuthGuard(Dashboard) // ---------- 顶层路由 ---------- export default function App () { const [isLogin, setLogin] = useState(false) const AuthValue = { isLogin, setLogin } return ( <AuthContext value={AuthValue}> <BrowserRouter> <Routes> <Route path="/" element={<Home />} /> <Route path="/login" element={<LoginPage />} /> <Route path="/dashboard" element={<ProtectedDashboard/>} /> </Routes> </BrowserRouter> </AuthContext> ) }
import { createBrowserRouter, RouterProvider, redirect, useNavigate, useLocation, Link, useRouteError } from 'react-router' import React, { createContext, useContext, useState } from 'react' // --------- 模拟登录上下文 --------- const AuthContext = createContext(null) function useAuth () { return useContext(AuthContext) } // --------- loader 守卫函数 --------- function authLoader () { const isLogin = localStorage.getItem('isLogin') === 'true' if (!isLogin) { throw redirect('/login') } return null } // --------- 首页组件 --------- function Home () { const { isLogin, logout } = useAuth() return ( <div> <h2>🏠 首页</h2> <p>当前状态: {isLogin ? '✅ 已登录' : '❌ 未登录'}</p> <nav style={{ marginBottom: '10px' }}> <Link to="/dashboard">进入 Dashboard</Link> </nav> {isLogin && <button onClick={logout}>退出登录</button>} </div> ) } // --------- 登录页组件 --------- function Login () { const { login } = useAuth() const navigate = useNavigate() const location = useLocation() const from = location.state?.from?.pathname || '/dashboard' function handleLogin () { login() navigate(from, { replace: true }) } return ( <div> <h2>🔐 登录页</h2> <p>登录后将跳转到:<code>{from}</code></p> <button onClick={handleLogin}>点我登录</button> </div> ) } // --------- 受保护页组件 --------- function Dashboard () { return ( <div> <h2>📊 Dashboard</h2> <p>此页面需登录后才能访问。</p> <Link to="/">返回首页</Link> </div> ) } // --------- 错误处理组件(可选) --------- function ErrorBoundary () { const error = useRouteError() return ( <div style={{ color: 'red' }}> <h2>出错啦 🚨</h2> <pre>{error.statusText || error.message}</pre> </div> ) } // --------- 创建路由 --------- const router = createBrowserRouter([ { path: '/', element: <Home /> }, { path: '/login', element: <Login /> }, { path: '/dashboard', element: <Dashboard />, loader: authLoader, errorElement: <ErrorBoundary /> } ]) // --------- 应用根组件 --------- export default function App () { const [isLogin, setLogin] = useState( localStorage.getItem('isLogin') === 'true' ) const login = () => { localStorage.setItem('isLogin', 'true') setLogin(true) } const logout = () => { localStorage.setItem('isLogin', 'false') setLogin(false) } return ( <AuthContext value={{ isLogin, login, logout }}> <RouterProvider router={router} /> </AuthContext> ) }
import { BrowserRouter, Routes, Route, useNavigate, useLocation, Link } from 'react-router' import React, { useContext, createContext, useState, useEffect } from 'react' // ---------- Auth 上下文 ---------- const AuthContext = createContext() function useAuth () { return useContext(AuthContext) } // ---------- 全局导航守卫组件 ---------- function GlobalGuard () { const location = useLocation() const navigate = useNavigate() const { isLogin } = useAuth() useEffect(() => { if (!isLogin && location.pathname !== '/login') { navigate('/login', { replace: true }) } }, [isLogin, location, navigate]) return null } // ---------- 首页 ---------- function Home () { const { isLogin, logout } = useAuth() return ( <div> <h2>🏠 首页</h2> <p>当前状态:{isLogin ? '✅ 已登录' : '❌ 未登录'}</p> <nav> <Link to="/dashboard">前往 Dashboard</Link> </nav> {isLogin && <button onClick={logout}>退出登录</button>} </div> ) } // ---------- 登录页 ---------- function Login () { const { login } = useAuth() const navigate = useNavigate() return ( <div> <h2>🔐 登录页</h2> <button onClick={() => { login() navigate('/', { replace: true }) }} > 登录 </button> </div> ) } // ---------- 受保护页面 ---------- function Dashboard () { return ( <div> <h2>📊 Dashboard</h2> <p>这个页面需要登录才能访问</p> <Link to="/">返回首页</Link> </div> ) } // ---------- App 根组件 ---------- export default function App () { const [isLogin, setLogin] = useState( localStorage.getItem('isLogin') === 'true' ) const login = () => { localStorage.setItem('isLogin', 'true') setLogin(true) } const logout = () => { localStorage.setItem('isLogin', 'false') setLogin(false) } return ( <AuthContext.Provider value={{ isLogin, login, logout }}> <BrowserRouter> {/* 👇 全局守卫放在路由前 */} <GlobalGuard /> <Routes> <Route path="/" element={<Home />} /> <Route path="/login" element={<Login />} /> <Route path="/dashboard" element={<Dashboard />} /> </Routes> </BrowserRouter> </AuthContext.Provider> ) }