跳到主要内容

路由✅

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. 路由导航守卫 通过 loaderaction 实现路由级别的守卫逻辑
  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>
  )
}