从 Vue3 到 React · 快速上手系列第 10 篇:路由

面向 Vue3 开发者的 React 快速上手系列

Vue Router 是「在配置里声明路由,在模板里留个出口 」;React Router 是「路由本身就是 JSX 组件,你把页面渲染写进组件树」。记住这句话,后面所有差异都是它的展开。

最大的心智冲击:在 Vue 里路由是「一份配置 + 一个出口标签」;在 React 里路由是「一段 JSX」 ------<Routes> 不是「出口占位符」,它本身就是根据当前 URL 决定渲染哪个 <Route> 的组件。


一、路由表配置:集中数组 vs 组件树

Vue3:集中式数组配置

ts 复制代码
// Vue3:一份 routes 数组,再 createRouter 一把梭
import { createRouter, createWebHistory } from 'vue-router'
import Home from './pages/Home.vue'
import About from './pages/About.vue'

const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About },
]

const router = createRouter({
  history: createWebHistory(),
  routes,
})

// main.ts 里 app.use(router) 后,路由才生效

React:用 JSX 在组件树里写

tsx 复制代码
// React:路由就是 JSX,写在组件树里
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import Home from './pages/Home'
import About from './pages/About'

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </BrowserRouter>
  )
}

关键差异

  • Vue 的 routes 是一份纯数据 (数组),和任何组件都无关;React 的 <Route>组件,散落在 JSX 里。
  • Vue 用 app.use(router) 把路由「插」进应用;React 用 <BrowserRouter> 直接包裹 根组件,路由能力从这一层开始向下传递(对应 Vue 的 createWebHistory())。
  • 没有「配置」和「出口」的分离------<Routes> 既是路由表,也是渲染出口。

习惯上 React 项目也会把 <Route> 抽到一个 AppRoutes.tsx 里集中管理,但语法上它仍是 JSX 组件,不是一份数据配置。


二、出口:router-view<Routes> / 渲染位置

Vue3:在模板里放 <router-view/>

vue 复制代码
<!-- Layout.vue -->
<template>
  <header>网站头部</header>
  <main>
    <router-view />   <!-- 当前匹配的页面组件渲染在这里 -->
  </main>
  <footer>网站底部</footer>
</template>

React:在想显示页面的地方放 <Routes>

tsx 复制代码
// Layout.tsx
export default function Layout() {
  return (
    <div>
      <header>网站头部</header>
      <main>
        <Routes>   {/* 当前匹配的页面组件渲染在这里 */}
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </main>
      <footer>网站底部</footer>
    </div>
  )
}

注意坑点 :Vue 的 <router-view/> 是「占位符 」,可以出现在任意组件任意层级;React 的 <Routes> 虽然也能放任意层级,但每对 <Routes> 都是独立的一次「URL → 组件」匹配<Routes> 内部只会匹配它下面直接挂的 <Route>,不会自动联动外层的 <Routes>。嵌套路由的正确姿势见第 5 节(用 <Outlet/>)。


声明式导航

vue 复制代码
<!-- Vue3 -->
<router-link to="/about">关于我们</router-link>
tsx 复制代码
// React
import { Link } from 'react-router-dom'

<Link to="/about">关于我们</Link>

两者都渲染成 <a> 标签,都做「无刷新跳转」。差异很小,to 的写法基本一致(也支持对象形式 { pathname, query } / 在 React 里是 { pathname, search })。

编程式导航

ts 复制代码
// Vue3
import { useRouter } from 'vue-router'
const router = useRouter()
router.push('/about')
router.push({ path: '/user', query: { id: 1 } })
tsx 复制代码
// React
import { useNavigate } from 'react-router-dom'
const navigate = useNavigate()
navigate('/about')
navigate('/user?id=1')

差异点

  • Vue 是 useRouter().push();React 是 useNavigate() 返回的函数 navigate()useRouter 在 React Router 里不是 干这个的------React 把「路由信息」和「导航动作」拆成了不同 Hook(useParams/useLocation 管读,useNavigate 管跳)。别顺手写 useRouter,React 没有这玩意儿(除非你用的是旧版或别的库)。
  • 传 query 时,Vue 用 query: { id: 1 };React 直接把参数拼进字符串 '/user?id=1',或用对象 { pathname: '/user', search: '?id=1' }

四、取路由参数:useRoute().params / .queryuseParams() / useSearchParams()

动态参数 params

ts 复制代码
// Vue3
import { useRoute } from 'vue-router'
const route = useRoute()
// 路由配置 path: '/user/:id'
console.log(route.params.id)
tsx 复制代码
// React
import { useParams } from 'react-router-dom'
// 路由配置 <Route path="/user/:id" element={<User/>} />
const { id } = useParams()
console.log(id)

查询参数 query

ts 复制代码
// Vue3:route.query 是个普通对象
const route = useRoute()
console.log(route.query.id)   // '1'(字符串)
tsx 复制代码
// React:useSearchParams 返回 [params, setParams],像 useState
import { useSearchParams } from 'react-router-dom'
const [searchParams, setSearchParams] = useSearchParams()
console.log(searchParams.get('id'))   // '1'(字符串)

// 想改 query,像改状态一样 set
setSearchParams({ id: '2' })

两个坑

  1. React 的 useSearchParams 不是「读一个对象」,而是「读 + 写」的元组 (和 useState 一样是 [值, 设值函数])。第一次见到会懵,记住它是「URL 上的 state」。
  2. 参数永远是字符串 。Vue 的 route.params.id 也是字符串,这点一致;但 Vue 的 route.query 能直接当对象用,React 的 searchParams 要用 .get('key') 取值。

五、嵌套路由:children + router-view ↔ 嵌套 <Route> + <Outlet/>

这是最容易翻车的一节,对照着看。

Vue3:children + 子路由再放 <router-view/>

ts 复制代码
// router 配置
const routes = [
  {
    path: '/dashboard',
    component: DashboardLayout,
    children: [
      { path: 'profile', component: Profile },
      { path: 'settings', component: Settings },
    ],
  },
]
vue 复制代码
<!-- DashboardLayout.vue -->
<template>
  <aside>侧边栏</aside>
  <section>
    <router-view />   <!-- Profile / Settings 渲染在这里 -->
  </section>
</template>

React:嵌套 <Route> + 子组件里放 <Outlet/>

tsx 复制代码
// App.tsx
<Routes>
  <Route path="/dashboard" element={<DashboardLayout />}>
    <Route path="profile" element={<Profile />} />
    <Route path="settings" element={<Settings />} />
  </Route>
</Routes>
tsx 复制代码
// DashboardLayout.tsx
import { Outlet } from 'react-router-dom'

export default function DashboardLayout() {
  return (
    <div>
      <aside>侧边栏</aside>
      <section>
        <Outlet />   {/* Profile / Settings 渲染在这里 */}
      </section>
    </div>
  )
}

对照要点

  • Vue 的 children 数组 ↔ React 直接把 <Route> 写成嵌套的子元素(缩进即层级)。
  • Vue 在父布局里写 <router-view/> ↔ React 在父布局里写 <Outlet/>
  • <Outlet/> 就是 React 版的 <router-view/>,专门用来「在父布局里给子路由留位置」。

加个「默认子路由」(index)

Vue 用 path: '' 或重定向;React 用 index 属性:

tsx 复制代码
<Route path="/dashboard" element={<DashboardLayout />}>
  <Route index element={<DashboardHome />} />   {/* 访问 /dashboard 时显示 */}
  <Route path="profile" element={<Profile />} />
</Route>

六、路由守卫:beforeEach<Navigate> / 组件内判断 / loader

Vue3:全局前置守卫

ts 复制代码
// Vue3:在配置层写 beforeEach
router.beforeEach((to, from) => {
  if (to.meta.requiresAuth && !isLoggedIn()) {
    return { path: '/login' }
  }
})

React:没有「全局守卫」这个函数,改用组件 / <Navigate>

React Router 没有 beforeEach 这种全局钩子。保护路由这件事,下沉到了「组件层面」来办。最直观的写法------写一个「受保护路由」组件:

tsx 复制代码
// RequireAuth.tsx:包一层,未登录就跳走
import { Navigate } from 'react-router-dom'

function RequireAuth({ children }: { children: React.ReactNode }) {
  if (!isLoggedIn()) {
    return <Navigate to="/login" replace />
  }
  return children
}

// 使用:在路由表里包住受保护的页面
<Route
  path="/dashboard"
  element={
    <RequireAuth>
      <DashboardLayout />
    </RequireAuth>
  }
/>

<Navigate to="/login" /> 就是 React 版的「重定向」,渲染它的瞬间就跳转。

进阶:在 loader 里拦截(v6.4+ 数据 API / v7)

如果你用的是 React Router 的数据路由(createBrowserRouter),可以在 loader 里做守卫,跳转会发生在渲染之前,更像 beforeEach 的时机:

tsx 复制代码
// 数据路由下的 loader 守卫
const router = createBrowserRouter([
  {
    path: '/dashboard',
    loader: () => {
      if (!isLoggedIn()) return redirect('/login')
      return null
    },
    element: <DashboardLayout />,
  },
])

结论 :Vue 的「守卫在配置里」,React 的「守卫在组件/loader 里」。Vue 开发者最容易犯的错是到处找 beforeEach------它不存在,换成「渲染时判断 + <Navigate> 跳走」的思维方式。


七、History 模式:createWebHistoryBrowserRouter / HashRouter

对应关系的本质------「用哪种 URL 策略」:

Vue Router React Router 说明
createWebHistory() <BrowserRouter> 干净 URL,如 /user/1,需要服务端兜底
createWebHashHistory() <HashRouter> # 的 URL,如 /#/user/1,无需服务端配置
tsx 复制代码
import { BrowserRouter, HashRouter } from 'react-router-dom'

// 干净模式(默认推荐)
<BrowserRouter><App /></BrowserRouter>

// 哈希模式(静态托管 / 没配服务端时)
<HashRouter><App /></HashRouter>

提醒:用 BrowserRouter(对应 createWebHistory)时,生产环境刷新子路由可能 404,需要服务端把所有路径回退到 index.html------这和 Vue 的 history 模式一模一样,不是 React 特有的坑。


八、一张对照表:Vue Router 概念 ↔ React Router 概念

概念 Vue Router React Router (v6/v7)
路由表 routes: [{ path, component }] 数组 <Routes><Route path element /></Routes>(JSX)
挂载方式 app.use(router) <BrowserRouter> 包裹根组件
页面出口 <router-view /> <Routes>(顶层)/ <Outlet/>(嵌套)
声明式导航 <router-link to> <Link to>
编程式导航 useRouter().push() useNavigate()navigate()
动态参数 useRoute().params.id useParams().id
查询参数 useRoute().query useSearchParams()(读 + 写元组)
嵌套路由 children: [] + 子 <router-view/> 嵌套 <Route> + <Outlet/>
默认子路由 path: '' / 重定向 <Route index />
全局守卫 router.beforeEach 无;用 <Navigate> / 保护组件 / loader
重定向 redirect / router.replace <Navigate replace /> / navigate(url, { replace: true })
history 模式 createWebHistory() <BrowserRouter>
hash 模式 createWebHashHistory() <HashRouter>
数据预取 beforeRouteEnter / async setup loader(数据路由)

九、给 Vue3 开发者的「防坑」清单

  1. 别找 useRouter 来跳转 。React 里跳转是 useNavigate(),读参数是 useParams()/useSearchParams()useRouter 这个名字在 React Router 里不存在(别和 Next.js 搞混)。
  2. <Routes> 不是 <router-view> 。Vue 的 <router-view/> 是纯占位符;React 的 <Routes> 本身是「路由表 + 匹配逻辑」,你每放一个 <Routes> 就开启一次新的 URL 匹配。
  3. 嵌套路由用 <Outlet/> ,不是再放一个 <Routes>。父布局里写 <Outlet/>,子路由才会「嵌」进去。
  4. useSearchParams() 返回的是 [值, 设值函数] ,像 useState 一样用,不是直接拿对象。取值用 .get('key')
  5. 没有 beforeEach 。保护路由请写「包装组件 + <Navigate>」,或在数据路由的 loaderredirect
  6. 路由配置是 JSX,不是数组 ------所以你可以写 {isAdmin && <Route .../>} 这类条件路由,这在 Vue 的静态 routes 数组里没那么自然。但也别在 <Routes> 里用 Array.map 动态生成 <Route> 时忘了给每个 <Route>key

小结

Vue Router 和 React Router 的「能力集」几乎一一对应,真正的差异只有一件事:Vue 把路由当「配置 + 出口」两件套,React 把路由当「组件」 。一旦你接受「<Routes>/<Route>/<Outlet/> 都是组件、写进 JSX 里」,Vue 里那套 routes 数组、router-viewrouter-linkbeforeEach 就能逐个翻译成 React 的写法。参数读取的「空对象 vs 元组」、守卫的「配置层 vs 组件层」是两个最该记牢的细节。

下一篇讲状态管理------Zustand/Redux 对标 Pinia

相关推荐
杉氧1 小时前
跨平台持久化:Flutter 本地数据库的多线程安全与架构设计实践
android·前端·flutter
毅yi1 小时前
里程碑1:实现elpis-core以及基于elpis-core的基础设置搭建
前端
FogLetter1 小时前
进程VS线程:你的电脑到底是怎么同时干那么多活的?
前端·面试
rememberme0011 小时前
华为云 CodeArts Pipeline前端 Vue 项目自动打包发布配置指南
前端·vue.js·华为云
deepThinking1 小时前
从项目里回看 React 生态:React 18/19 新特性、我们踩过的坑,以及那些"用旧写法写出的新思想
react.js
默_笙1 小时前
🍔 我用 React 写了一个 TodoList,终于搞懂了"状态该放谁家"
前端·javascript
FogLetter1 小时前
我真的写了个“诈尸式”缓存组件:手撕React KeepAlive
前端·react.js·面试
A24207349302 小时前
Vue.js 初学者注意事项与项目开发实践指南
前端·javascript·vue.js
用户059540174462 小时前
「记仇」记忆存储测试踩坑实录:用 Pytest+Docker 把 BUG 扼杀在凌晨 3 点前
前端·css