
一句话:Vue Router 是「路由表 + 全局守卫(
beforeEach)」的模型,React Router 是「声明式组件 + 渲染控制」的模型——React 没有全局守卫钩子,权限要靠「包装组件」或「loader」实现。
一、配置方式
// Vue Router:集中式路由表
const routes = [
{
path: '/users/:id',
name: 'user-detail',
component: () => import('./views/UserDetail.vue'), // 懒加载
meta: { requiresAuth: true },
props: true,
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
router.beforeEach((to, from, next) => { // ⭐ 全局守卫
if (to.meta.requiresAuth && !isLoggedIn()) {
next({ name: 'login', query: { redirect: to.fullPath } });
} else {
next();
}
});
// React Router:声明式(JSX)或对象数组
const router = createBrowserRouter([
{
path: '/users/:id',
element: <UserDetail />,
loader: ({ params }) => fetchUser(params.id!),
},
]);
// 或者声明式
<Routes>
<Route path="/users/:id" element={<UserDetail />} />
</Routes>
| 维度 | Vue Router | React Router |
|---|---|---|
| 配置形态 | 集中式路由表(对象数组) | JSX 组件 或 对象数组(数据路由) |
| 视图出口 | <router-view /> |
<Outlet /> |
| 导航组件 | <router-link> |
<Link> / <NavLink> |
| 激活类名 | 自动加 .router-link-active |
自己用 NavLink 的 isActive |
| 命名路由 | ✅ { name: 'x' } |
❌ 只能用路径 |
| 路由别名 | ✅ alias |
❌ |
meta 字段 |
✅ | ❌(用 loader 或组件内处理) |
| 全局守卫 | ✅ beforeEach / afterEach |
❌(用包装组件 / loader) |
二、守卫模型对比(核心差异)
// Vue:三层守卫
// ① 全局
router.beforeEach((to, from, next) => {});
router.beforeResolve((to, from, next) => {});
router.afterEach((to, from) => {});
// ② 路由独享
{ path: '/admin', component: Admin, beforeEnter: (to, from, next) => {} }
// ③ 组件内
beforeRouteEnter(to, from, next) {}
beforeRouteUpdate(to, from, next) {}
beforeRouteLeave(to, from, next) {}
// React:没有守卫钩子,用「渲染控制」实现
// ① 包装组件(渲染时判断)
function RequireAuth({ children }: { children: React.ReactNode }) {
const { user, isLoading } = useAuth();
const location = useLocation();
if (isLoading) return <FullPageSkeleton />;
if (!user) return <Navigate to="/login" state={{ from: location }} replace />;
return <>{children}</>;
}
// ② 布局路由(保护一组)
{
path: '/',
element: <RequireAuth />, // 内部渲染 <Outlet />
children: [/* 受保护的路由 */],
}
// ③ loader 里校验(渲染前,无闪烁)
{
path: '/admin',
loader: () => {
if (!isAdmin()) throw redirect('/403');
return null;
},
element: <Admin />,
}
| 能力 | Vue Router | React Router |
|---|---|---|
| 全局前置守卫 | ✅ beforeEach |
❌(用布局路由 / loader) |
| 全局后置守卫 | ✅ afterEach |
❌(用 useLocation + useEffect) |
| 路由独享守卫 | ✅ beforeEnter |
✅ loader |
| 组件内「离开前」守卫 | ✅ beforeRouteLeave |
⚠️ useBlocker(仅数据路由) |
| 异步守卫 | ✅(next() 异步调用) |
✅(loader 可以是 async) |
| 「渲染前」校验 | ⚠️(守卫 + 组件) | ✅ loader(更彻底) |
// React 里实现「afterEach」的等价物:监听 location
function RouteAnalytics() {
const location = useLocation();
useEffect(() => {
trackPageView(location.pathname + location.search);
}, [location]);
return null;
}
// 放在根布局里即可(等价于全局 afterEach)
三、数据获取对比
<!-- Vue:组件内 + 组件守卫 或 onMounted -->
<script setup>
import { onMounted, ref } from 'vue';
import { onBeforeRouteUpdate } from 'vue-router';
const user = ref(null);
const route = useRoute();
// ① 组件挂载时获取
onMounted(async () => {
user.value = await fetchUser(route.params.id);
});
// ② 参数变化时重新获取(需要显式处理)
onBeforeRouteUpdate(async (to) => {
user.value = await fetchUser(to.params.id);
});
</script>
// React:三种方式
// ① 组件内 useEffect(需要自己处理参数变化与竞态)
function UserDetail() {
const { id } = useParams();
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
let cancelled = false;
fetchUser(id!).then((data) => { if (!cancelled) setUser(data); });
return () => { cancelled = true; };
}, [id]);
return <div>{user?.name}</div>;
}
// ② 查询库(推荐)
function UserDetail2() {
const { id } = useParams();
const { data } = useQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id!) });
return <div>{data?.name}</div>;
}
// ③ loader(数据路由,渲染前取)
const router = createBrowserRouter([
{ path: 'users/:id', element: <UserDetail />, loader: ({ params }) => fetchUser(params.id!) },
]);
| 维度 | Vue | React |
|---|---|---|
| 组件内获取 | onMounted / created |
useEffect |
| 参数变化时重新获取 | onBeforeRouteUpdate / watch |
useEffect 依赖参数 / 查询库 key |
| 「渲染前」预取 | ⚠️ 需配合 Suspense 或 beforeRouteEnter |
✅ loader |
| 内置缓存 | ❌(需自己实现或用 Pinia + 手写) | ✅ TanStack Query |
四、动态路由与参数
<!-- Vue:可以用 props 传参(推荐) -->
<script setup>
// 路由配置里写 props: true
const props = defineProps({ id: String });
</script>
// React:用 Hook 读取
function UserDetail() {
const { id } = useParams();
return <div>{id}</div>;
}
| 维度 | Vue | React |
|---|---|---|
| 读取参数 | useRoute().params.id |
useParams().id |
| 传给组件的方式 | props: true(推荐,解耦) |
Hook(组件自己取) |
| 参数变化时组件是否重建 | 默认不重建(同 React) | 默认不重建 |
| 强制重建 | :key="$route.fullPath" |
<Comp key={id} /> |
<!-- Vue 里用 key 强制重建(与 React 的 key 思路一致) -->
<router-view :key="$route.fullPath" />
五、嵌套路由与布局
<!-- Vue:嵌套 router-view -->
<!-- layout/DefaultLayout.vue -->
<template>
<div>
<TopNav />
<router-view /> <!-- ⭐ 子路由出口 -->
</div>
</template>
// React:Outlet
function DefaultLayout() {
return (
<div>
<TopNav />
<Outlet /> {/* ⭐ 子路由出口 */}
</div>
);
}
| 维度 | Vue | React |
|---|---|---|
| 子路由出口 | <router-view /> |
<Outlet /> |
| 具名出口 | ✅ <router-view name="sidebar" /> |
❌(用 props 传元素) |
| 子路由路径 | 相对父路径(同 React) | 相对父路径 |
| 索引路由 | path: '' |
{ index: true } |
六、编程式导航与查询参数
// Vue
const router = useRouter();
const route = useRoute();
router.push('/about'); // 字符串
router.push({ name: 'user', params: { id: '1' } }); // ⭐ 命名路由
router.push({ path: '/search', query: { q: 'react' } }); // ⭐ query 对象
router.replace('/login');
router.back();
router.go(-1);
// React
const navigate = useNavigate();
navigate('/about');
navigate(`/users/${id}`); // ⚠️ 没有命名路由,要拼路径
navigate('/search?q=react');
navigate('/search', { state: { q: 'react' } }); // state 不进 URL
navigate(-1); // 后退
// Vue 的 query 对应 React 的 useSearchParams / URL 拼接
// Vue:
router.push({ query: { page: 2, q: 'react' } });
// React:
const [searchParams, setSearchParams] = useSearchParams();
setSearchParams({ page: '2', q: 'react' });
| 维度 | Vue | React |
|---|---|---|
| 跳转 | router.push / replace |
navigate |
| 按名字跳转 | ✅ { name } |
❌ 只能拼路径 |
| 传查询参数 | ✅ query: {} 对象 |
useSearchParams 或用模板串 |
| 传「不进 URL」的数据 | state(history) |
navigate(..., { state }) |
| 后退 | router.back() / go(-1) |
navigate(-1) |
七、心智转换(Vue → React)
① 从「路由表 + 守卫」变成「组件树 + 渲染控制」
Vue 的「拦在跳转前」→ React 的「拦在渲染时」(或 loader)
② 从「全局守卫」变成「布局路由」
Vue: router.beforeEach → React: <RequireAuth><Outlet /></RequireAuth>
③ 从「meta 字段」变成「loader / 组件内判断」
Vue: to.meta.requiresAuth → React: loader 里读上下文 / 包装组件里判断
④ 从「命名路由」变成「路径常量」
Vue: router.push({ name: 'user', params: { id } })
React: navigate(PATHS.userDetail(id)) ← 建议定义路径常量
⑤ 从「props: true 传参」变成「useParams」
React 里组件自己用 Hook 取参数
⑥ 从「beforeRouteLeave」变成「useBlocker」
只在数据路由下可用(且浏览器前进/后退需额外处理 beforeunload)
// ④ 的实践:定义路径常量(弥补「没有命名路由」的不足)
export const PATHS = {
home: '/',
login: '/login',
userDetail: (id: string) => `/users/${id}`,
settings: '/settings',
admin: {
root: '/admin',
users: '/admin/users',
},
} as const;
// 使用
navigate(PATHS.userDetail(id));
<Link to={PATHS.settings}>设置</Link>;
八、常见坑
// ① 在 React 里找「beforeEach」的等价物(没有,要用布局路由)
// ② 期待「命名路由」跳转(React 不支持,用路径常量替代)
// ③ 用 meta 存权限信息(React 里应该存在「路由配置对象」或「服务端返回」)
// ④ 从 Vue 的「props 传参」思维出发,试图给路由组件传 props
<Route path="/users/:id" element={<UserDetail id={id} />} /> // ❌ 拿不到 id(外层没有)
// ✅ 组件内 useParams
// ⑤ 用 watch 的思维写 useEffect(依赖数组写错的概率更高)
// ⑥ 以为「路由变化会自动重新请求数据」(Vue 也需显式处理,React 也一样)
// ⑦ 从 Vue 过来「忘了 useParams 返回的是字符串」
// ⑧ 混淆「Vue 的 query」与「React 的 state」
// ⑨ 在 React 里「用事件总线做全局守卫」(应该用布局路由)
// ⑩ 用「beforeRouteLeave」的思路做「未保存提示」但没有 useBlocker
// ⑪ 认为「Vue Router 的守卫更强大」(其实只是模型不同——React 的 loader 能「渲染前」校验)
// ⑫ 忽略「React Router 的 loader 会阻塞导航」(Vue 的守卫默认不阻塞)
九、面试延伸
- 「React Router 和 Vue Router 的主要区别?」
四方面:① 配置形态——Vue 是「集中式路由表」(对象数组),React 可以是 JSX 组件或对象数组(数据路由);② 视图出口——Vue 用<router-view />,React 用<Outlet />;③ 守卫模型——Vue 有「全局beforeEach/ 路由独享beforeEnter/ 组件内beforeRouteLeave」三层守卫,而 React 没有全局守卫钩子,权限通过「包装组件」「布局路由」或「loader里校验」实现;④ 数据获取——Vue 主要在组件内(onMounted/beforeRouteUpdate),React 除了组件内(useEffect/ 查询库)还能用loader在渲染前取数据。 -
「React 里怎么等价实现
beforeEach?」
三种方式:① 布局路由——写一个<RequireAuth><Outlet /></RequireAuth>作为「需要保护的路由」的父级,RequireAuth内部做校验(未登录就<Navigate to="/login" />);②loader里校验——在路由配置的loader中throw redirect('/login')(渲染前完成跳转,没有闪烁,最接近beforeEach的语义);③ 包装组件——<Route element={<RequireAuth><Page /></RequireAuth>} />(粒度更细)。注意要处理「鉴权状态加载中」的三态,否则刷新页面会闪一下登录页。 -
「
beforeRouteLeave(离开前守卫)在 React 里怎么做?」
用useBlocker(仅在数据路由下可用):const blocker = useBlocker(({ currentLocation, nextLocation }) => dirty && currentLocation.pathname !== nextLocation.pathname),然后根据blocker.state === 'blocked'显示确认弹窗,用户点「离开」调blocker.proceed()、点「留下」调blocker.reset()。注意:useBlocker只拦「站内导航」,浏览器前进/后退与关闭标签页需要额外监听beforeunload事件。 -
「数据获取的方式有什么不同?」
Vue 主要在「组件内」——onMounted(首次)与onBeforeRouteUpdate/watch(参数变化时重新获取);React 有三种:① 组件内useEffect(依赖参数,需自己处理竞态);② 查询库(TanStack Query)(把参数放进queryKey,自动重新请求 + 缓存);③loader(渲染前取数据,组件里用useLoaderData同步读取)。React 的loader是 Vue 相对少见的能力——它能做到「数据就绪才渲染」,避免骨架闪烁。 -
「Vue 的
meta字段在 React 里怎么实现?」
React Router 没有内置meta。等价做法有三种:① 在自己的路由配置对象上加自定义字段(如permission: 'user:list'),在loader或包装组件里读取;② 在包装组件里显式判断(<RequireRole role="admin">);③ 用 Context 提供「权限工具」(useAuth().hasPermission('x'))。此外「页面标题」这类 meta 信息,React 19 支持直接在组件里渲染<title>(会自动提升到<head>),或用react-helmet/ 框架的metadataAPI。 -
「Vue 的命名路由在 React 里怎么替代?」
React Router 不支持命名路由,必须用路径字符串。为了避免「路径散落在各处」,推荐定义路径常量字典:const PATHS = { userDetail: (id: string) => \/users/${id}` },然后统一用navigate(PATHS.userDetail(id))`。这样至少有「集中管理 + 改路径只需改一处」的好处(等价于命名路由的部分价值),而且能被 TypeScript 校验参数。 -
「Vue 的
props: true在 React 里怎么实现?」
React 里路由组件不能通过路由配置传 props(<Route path="/users/:id" element={<UserDetail id={id} />} />里的id在配置时并不存在)。等价做法是「组件自己用useParams()取参数」:function UserDetail() { const { id } = useParams(); }。如果确实想保持「组件只依赖 props」的可测试性,可以写一个「容器组件」负责取参数再传给展示组件:function UserDetailRoute() { const { id } = useParams(); return <UserDetail id={id} />; }。 -
「两边的守卫哪个更强大?」
各有所长:Vue Router 的守卫更「丰富」——有全局前置/后置、路由独享、组件内的进入/更新/离开三类时机,且能「取消导航」(next(false));React 的loader更「彻底」——它把「校验 + 取数据」放到渲染之前,所以「未授权时根本不会渲染组件、也不会闪一下」;而 Vue 的beforeEach虽然也在导航前,但「数据获取」通常还在组件里(可能先渲染再取数)。模型不同:Vue 是「拦截导航」,React 是「控制渲染 + 渲染前准备数据」。
一句话速记
Vue Router 是「集中式路由表 + 三层守卫(
beforeEach/beforeEnter/beforeRouteLeave)+ 命名路由 +meta」,React Router 是「声明式组件 + 渲染控制」——没有全局守卫(用布局路由或loader里throw redirect实现,后者能做到「渲染前校验」)、没有命名路由(用路径常量替代)、没有 meta(用自定义配置字段/包装组件)、没有props: true(用useParams);视图出口是Outlet(对<router-view />)、参数变化两边都不重建组件(都要显式处理或加key);「离开前守卫」用useBlocker(仅数据路由,且需配合beforeunload);数据获取 React 多了loader(渲染前就绪)这一路径。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!