面向 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/>)。
三、导航:router-link ↔ <Link>,push ↔ useNavigate
声明式导航
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 / .query ↔ useParams() / 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' })
两个坑:
- React 的
useSearchParams不是「读一个对象」,而是「读 + 写」的元组 (和useState一样是[值, 设值函数])。第一次见到会懵,记住它是「URL 上的 state」。 - 参数永远是字符串 。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 模式:createWebHistory ↔ BrowserRouter / 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 开发者的「防坑」清单
- 别找
useRouter来跳转 。React 里跳转是useNavigate(),读参数是useParams()/useSearchParams()。useRouter这个名字在 React Router 里不存在(别和 Next.js 搞混)。 <Routes>不是<router-view>。Vue 的<router-view/>是纯占位符;React 的<Routes>本身是「路由表 + 匹配逻辑」,你每放一个<Routes>就开启一次新的 URL 匹配。- 嵌套路由用
<Outlet/>,不是再放一个<Routes>。父布局里写<Outlet/>,子路由才会「嵌」进去。 useSearchParams()返回的是[值, 设值函数],像useState一样用,不是直接拿对象。取值用.get('key')。- 没有
beforeEach。保护路由请写「包装组件 +<Navigate>」,或在数据路由的loader里redirect。 - 路由配置是 JSX,不是数组 ------所以你可以写
{isAdmin && <Route .../>}这类条件路由,这在 Vue 的静态routes数组里没那么自然。但也别在<Routes>里用Array.map动态生成<Route>时忘了给每个<Route>加key。
小结
Vue Router 和 React Router 的「能力集」几乎一一对应,真正的差异只有一件事:Vue 把路由当「配置 + 出口」两件套,React 把路由当「组件」 。一旦你接受「<Routes>/<Route>/<Outlet/> 都是组件、写进 JSX 里」,Vue 里那套 routes 数组、router-view、router-link、beforeEach 就能逐个翻译成 React 的写法。参数读取的「空对象 vs 元组」、守卫的「配置层 vs 组件层」是两个最该记牢的细节。
下一篇讲状态管理------Zustand/Redux 对标 Pinia