跳到主要内容

路由✅

React Router 中 mode 有哪几种有什么区别?​

答案

Mode 就是使用 React Router 的方式,分为三种

  • Declarative 基本使用,支持基础路由导航等逻辑
  • Data 配置化使用,额外支持 loader、action 等能力
  • Framework 框架化使用,支持不同渲染策略,如 SSR、SPA、RSC 等
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>
  )
}

更详细的区别可以参考 API 和 Mode 说明

BrowserRouter、 HashRouter 、MemoryRouter 有什么区别?​

答案
路由器URL 表现原理典型场景是否依赖服务器
BrowserRouter普通路径 /aboutHTML5 history APISPA、生产环境是
HashRouter#/about监听 location.hash静态站点、无服务端否
MemoryRouter无 URL 变化内存中维护 history测试、非浏览器环境否
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>
  )
}

提示

注意对比三个 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>
  )
}

提示

注意当存在多个匹配规则时,遵循如下规则

  1. 越精确越优先匹配
  2. 若优先级相同则按定义顺序匹配

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/:iduseParams()资源唯一标识URL 可见,刷新保留
查询参数/users?id=123useSearchParams()筛选、分页URL 可见,刷新保留
路由状态navigate('/a', {state})useLocation().state页面跳转携带临时数据内存,刷新丢失
上下文ContextuseContext全局/多层级共享依赖组件树
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 原生并不支持此能力,典型策略如下

  1. 组件导航守卫 通过 Hoc 包裹组件,在包裹组件中实现逻辑判断
  2. 路由导航守卫 通过 loader 和 action 实现路由级别的守卫逻辑
  3. 全局导航守卫 通过 useEffect 钩子监听路由变化,结合 useLocation 获取当前路由信息注册在全局页面实现
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>
  )
}