如果你同时写 Vue 和 React,一定懂那种感觉:切回 React 项目,想用
useRoute()拿参数,却发现根本没有这个 hook。
起因
我平时 Vue 和 React 都写。Vue Router 的体验一直让我很满意——useRoute、useRouter、导航守卫、嵌套路由、文件路由……每一块都设计得恰到好处。
切回 React 项目,用 React Router 时总觉得哪里别扭:
- 路由信息碎片化:
useParams和useSearchParams是两个 hook,拿不到一个统一的route对象;没有meta字段,鉴权信息无处挂载 - 守卫缺失:没有全局导航守卫,鉴权得自己包一层高阶组件或
loader - 文件路由绑定框架:文件系统路由要靠 Next.js / Remix,单独用 Vite 就得手写
- 转场动画没有官方方案:路由切换动画得引入 Framer Motion 等第三方库
- 没有 Keep-Alive:列表页跳详情页再返回,滚动位置和状态全部丢失
于是我决定自己搓一个:把 Vue Router 的 API 完整搬到 React,同时把文件系统路由、Keep-Alive、转场动画这些好东西一并带过来。
这就是 @tangmu1121/rvue-router——一个为 React 打造的 Vue Router 风格路由库。
30 秒速览:它长什么样
三步让你的 React 项目拥有 Vue Router 的开发体验:
npm install @tangmu1121/rvue-router
第一步:创建路由
// src/router/index.ts
import { createRouter, createWebHistory } from '@tangmu1121/rvue-router'
import { lazy } from 'react'
export const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', redirect: '/home' },
{ path: '/home', name: 'home', component: lazy(() => import('@/pages/Home')), meta: { title: '首页' } },
{ path: '/about', name: 'about', component: lazy(() => import('@/pages/About')), meta: { title: '关于' } },
{ path: '/users/:id', name: 'user-detail', component: lazy(() => import('@/pages/User')) },
{ path: '*', component: lazy(() => import('@/pages/NotFound')) },
],
scrollBehavior(_to, _from, savedPosition) {
return savedPosition ?? { top: 0 }
},
})
// 全局鉴权守卫——一行搞定
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !localStorage.getItem('token')) {
next('/login')
} else {
next()
}
})
// 全局后置守卫——更新页面标题
router.afterEach((to) => {
document.title = String(to.meta.title ?? 'App')
})
第二步:注入 Provider
// src/main.tsx
const { RouterProvider } = router
createRoot(document.getElementById('root')!).render(
<RouterProvider>
<App />
</RouterProvider>
)
第三步:渲染路由出口
// src/App.tsx
import { RouterView, RouterLink } from '@tangmu1121/rvue-router'
export default function App() {
return (
<div>
<nav>
<RouterLink to="/home" activeClass="active">首页</RouterLink>
<RouterLink to="/about" activeClass="active">关于</RouterLink>
</nav>
<RouterView transition="fade" /> {/* 自带淡入淡出转场 */}
</div>
)
}
就这些。如果你写过 Vue Router,基本不用看文档就能上手。
核心功能一览
1. useRoute —— 一个 hook 拿到所有路由信息
function UserDetail() {
const route = useRoute()
route.params.id // 动态参数
route.query.page // 查询参数
route.hash // URL hash
route.meta.title // 路由元信息
route.fullPath // 完整路径
route.matched // 从根到当前的匹配链
}
对比 React Router: 需要 useParams() + useSearchParams() + useLocation() + 自己实现 meta,四个 hook 才能凑齐这些信息。
2. 导航守卫 —— 完整的四阶段执行链
每次路由切换,守卫按以下顺序依次执行:
① 组件 useBeforeRouteLeave → 离开旧页面前拦截
② 全局 router.beforeEach → 统一鉴权 / 权限校验
③ 组件 useBeforeRouteUpdate → 参数变化但组件复用时
④ 路由级 beforeEnter → 进入特定路由前拦截
在组件里用 hook 直接注册,不需要包装高阶组件:
function EditForm() {
const [isDirty, setIsDirty] = useState(false)
// 离开前确认——表单未保存时阻止跳转
useBeforeRouteLeave((to, from, next) => {
if (isDirty && !confirm('有未保存的更改,确认离开?')) {
next(false) // 阻止导航
} else {
next() // 放行
}
})
// 路由参数变化时重新加载(/users/1 → /users/2,组件复用不卸载)
useBeforeRouteUpdate((to, from, next) => {
fetchUserData(to.params.id)
next()
})
}
每个守卫都返回取消函数,动态注册/注销不会内存泄漏:
const removeGuard = router.beforeEach((to, from, next) => { /* ... */ })
removeGuard() // 不再需要时移除
3. RouterLink —— 声明式导航 + 智能激活状态
// 前缀匹配加 active 类,精确匹配加 active-exact 类
<RouterLink to="/users" activeClass="active" exactActiveClass="active-exact">
用户管理
</RouterLink>
// 对象形式导航(支持 query、hash)
<RouterLink to={{ path: '/users', query: { page: '2' }, hash: '#list' }}>
用户列表第 2 页
</RouterLink>
// 禁用状态(渲染为 <a> 但阻止跳转,自动追加 aria-disabled)
<RouterLink to="/admin" disabled>管理员(无权限)</RouterLink>
内置行为:
- 精确匹配时自动添加
aria-current="page",满足无障碍标准 - Ctrl/Meta/Shift 点击时走浏览器默认行为(新标签页打开)
- 支持所有原生
<a>属性
需要完全自定义导航组件?用 useLink hook:
function MyNavItem({ to, children }) {
const { href, isActive, isExactActive, navigate } = useLink({ to })
return (
<a href={href} onClick={navigate} className={isActive ? 'active' : ''}>
{children}
</a>
)
}
重头戏一:文件系统路由
这是我最花时间的部分。只需要一个 Vite 插件,创建文件就等于注册路由,告别手写 routes 数组。
接入
// vite.config.ts
import { fileRouter } from '@tangmu1121/rvue-router/vite'
export default defineConfig({
plugins: [react(), fileRouter({ dir: 'src/pages' })],
})
// src/router/index.ts
import routes from 'virtual:rvue-routes' // 自动生成!
import { createRouter, createWebHistory } from '@tangmu1121/rvue-router'
export const router = createRouter({ history: createWebHistory(), routes })
文件命名约定
src/pages/
index.tsx → / name: 'index'
about.tsx → /about name: 'about'
users/
index.tsx → /users name: 'users'
[id].tsx → /users/:id name: 'users-id'
[id]/
posts.tsx → /users/:id/posts name: 'users-id-posts'
404.tsx → * name: '404'
[...all].tsx → * name: 'all'(通配符的另一种写法)
| 文件/目录 | 规则 |
|---|---|
index.tsx | 当前层级的索引页 |
[id].tsx | 动态参数路由 :id |
[...all].tsx / 404.tsx | 通配符路由 * |
_layout.tsx | 布局组件,将目录内路由变为嵌套子路由 |
[page].route.ts | 同级路由配置文件(meta、守卫、name 覆盖) |
_*.tsx | 下划线前缀文件被忽略 |
嵌套布局
加上 _layout.tsx 就能做嵌套路由:
src/pages/
_layout.tsx ← 根布局(导航栏 + 页脚)
index.tsx
about.tsx
users/
_layout.tsx ← /users 布局(侧边栏)
index.tsx
[id].tsx
生成结果:
[{
path: '/',
component: lazy(() => import('./pages/_layout.tsx')),
children: [
{ path: '', name: 'index', component: lazy(() => import('./pages/index.tsx')) },
{ path: 'about', name: 'about', component: lazy(() => import('./pages/about.tsx')) },
{
path: 'users',
name: 'users',
component: lazy(() => import('./pages/users/_layout.tsx')),
children: [
{ path: '', name: 'users', component: lazy(...) },
{ path: ':id', name: 'users-id', component: lazy(...) },
],
},
],
}]
同级路由配置文件(*.route.ts)
想给某个页面加 meta 或路由守卫,但不想污染组件文件?创建一个同名的 .route.ts:
// src/pages/dashboard.route.ts
import { defineRouteConfig } from '@tangmu1121/rvue-router'
export default defineRouteConfig({
name: 'dashboard', // 覆盖自动生成的 name
meta: {
requiresAuth: true,
title: '控制台',
roles: ['admin'],
},
beforeEnter: (to, from, next) => {
if (!hasPermission(to.meta.roles)) next('/403')
else next()
},
})
插件会自动将这个文件的导出 spread 到路由对象上。页面逻辑和路由配置完全分离,整洁。
defineRouteConfig 是一个纯透传函数,运行时零开销,仅提供 TypeScript 类型推断。
HMR 支持
开发时新增/删除页面文件、修改 *.route.ts 都会自动触发路由更新,不需要手动重启。
重头戏二:路由缓存(Keep-Alive)
这是 Vue 开发者切到 React 后最怀念的功能之一:列表页跳到详情页再返回,滚动位置和表单状态全部保留。
基础用法
// 全部路由都缓存
<RouterView keepAlive />
// 精确控制缓存范围
<RouterView
keepAlive={{
include: ['user-list', 'settings'], // 只缓存这些路由
exclude: /^admin/, // 排除管理后台(优先级高于 include)
max: 10, // 最多缓存 10 个实例,超出按 LRU 淘汰
}}
/>
缓存中的组件还"活着"
缓存中的组件不会被卸载,只是通过 display: none 隐藏。这意味着 useState、useRef、定时器等状态都会保留。
但有时候你希望在页面"退到后台"时暂停某些操作(轮询、视频播放等),这就需要区分"激活"和"未激活"状态。useKeepAliveActive 就是为此而生:
import { useKeepAliveActive } from '@tangmu1121/rvue-router'
function DataDashboard() {
const isActive = useKeepAliveActive()
// 只在页面展示时轮询数据
useEffect(() => {
if (!isActive) return // 页面被缓存时跳过
const id = setInterval(fetchData, 5000)
return () => clearInterval(id)
}, [isActive])
return <div>...</div>
}
true:当前页面正在展示,或未使用 Keep-Alivefalse:当前页面在缓存中但被隐藏(等同于 Vue 的deactivated)
这跟 Vue 的 activated / deactivated 生命周期完全对应。
重头戏三:路由转场动画
这块我参照 Vue 的 <Transition> 设计,做到了零额外依赖。
一行开启
<RouterView transition="fade" />
/* 全局 CSS 中定义 */
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
六个生命周期类
动画模式是 out-in:旧组件先完成离开动画,新组件再进入,不会出现两个组件叠加。
离开阶段:
| 时机 | 添加 | 移除 |
|---|---|---|
| 离开开始 | name-leave-from、name-leave-active | — |
| 下一帧 | name-leave-to | name-leave-from |
| 离开结束 | — | name-leave-active、name-leave-to |
进入阶段(旧组件离开完成后):
| 时机 | 添加 | 移除 |
|---|---|---|
| 进入开始 | name-enter-from、name-enter-active | — |
| 下一帧 | name-enter-to | name-enter-from |
| 进入结束 | — | name-enter-active、name-enter-to |
跟 Vue 的 <Transition> 类名约定完全一致,如果你之前写过 Vue 的过渡动画 CSS,直接复制过来就能用。
几个常用的动画效果
/* 水平滑动 */
.slide-enter-active, .slide-leave-active { transition: all 0.35s ease; }
.slide-enter-from { opacity: 0; transform: translateX(30px); }
.slide-leave-to { opacity: 0; transform: translateX(-30px); }
/* 缩放 */
.zoom-enter-active, .zoom-leave-active { transition: all 0.25s ease; }
.zoom-enter-from, .zoom-leave-to { opacity: 0; transform: scale(0.95); }
用 Tailwind CSS?也完美支持
通过自定义类名,直接使用 Tailwind 的原子类,不需要写任何额外 CSS:
<RouterView
transition={{
enterFromClass: 'opacity-0 translate-x-4',
enterActiveClass: 'transition-all duration-300 ease-out',
enterToClass: 'opacity-100 translate-x-0',
leaveFromClass: 'opacity-100 translate-x-0',
leaveActiveClass: 'transition-all duration-200 ease-in',
leaveToClass: 'opacity-0 -translate-x-4',
}}
/>
CSS Modules 同理,传入 styles.enterFrom 等即可。
不同路由用不同动画
通过路由 meta 动态切换:
function App() {
const route = useRoute()
return <RouterView transition={route.meta.transition ?? 'fade'} />
}
// 路由配置
{ path: '/home', meta: { transition: 'fade' } },
{ path: '/dashboard', meta: { transition: 'slide' } },
{ path: '/settings', meta: { transition: 'zoom' } },
其他细节
- 自动检测时长:未指定
duration时,自动从getComputedStyle读取transition-duration/animation-duration,无需手动配置 - 快速导航安全:动画进行中触发新的路由跳转,会缓存起来等当前动画结束后立即执行,不会出错
- 首次渲染动画:设置
appear: true即可在页面首次加载时也执行进入动画 - 与
React.lazy()兼容:进入动画会在 Suspense 完成后再执行
其他实用功能
命名视图
一个路由同时渲染多个并列组件:
// 路由配置
{
path: '/dashboard',
components: {
default: MainContent, // <RouterView />
sidebar: SidebarPanel, // <RouterView name="sidebar" />
header: PageHeader, // <RouterView name="header" />
},
}
// 布局组件中
function DashboardLayout() {
return (
<div className="layout">
<header><RouterView name="header" /></header>
<aside><RouterView name="sidebar" /></aside>
<main><RouterView /></main>
</div>
)
}
动态路由
登录后按权限动态增删路由:
// 登录后动态添加
router.addRoute({ path: '/admin', name: 'admin', component: AdminPage })
router.addRoute({ path: 'logs', component: Logs }, 'admin') // 添加到 admin 的子路由
// 退出时清理
router.removeRoute('admin')
// 检查是否存在
router.hasRoute('admin') // boolean
useIsNavigating —— 全局加载指示器
守卫执行期间(异步鉴权、数据预加载等),可以展示全局 loading:
function GlobalProgressBar() {
const isNavigating = useIsNavigating()
return isNavigating ? <ProgressBar /> : null
}
滚动行为
与 Vue Router 一致的 scrollBehavior 配置:
createRouter({
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) return savedPosition // 前进/后退恢复位置
if (to.hash) return { el: to.hash, behavior: 'smooth' } // 锚点滚动
return { top: 0 } // 默认回到顶部
},
})
三种历史模式
createWebHistory() // /path HTML5 History API,需服务器配置
createHashHistory() // /#/path 无需服务器配置,兼容性好
createMemoryHistory() // 内存模式 SSR / 单元测试
// 也支持字符串简写
createRouter({ history: 'browser', routes })
createRouter({ history: 'hash', routes })
createRouter({ history: 'memory', routes })
router.isReady()
等待初始导航完成:
await router.isReady()
console.log('路由已就绪,可以执行后续逻辑')
技术实现简记
几个有意思的实现细节,也是我在开发过程中花了不少时间打磨的地方:
响应式路由
基于 React 18 的 useSyncExternalStore。路由状态存放在 React 外部(闭包变量),所有订阅者在路由变化时同步更新,不会出现状态撕裂(tearing)。这也是 React 官方推荐的外部 store 接入方式。
Keep-Alive 的缓存策略
缓存用模块级 Map 按 RouterView 的渲染深度(depth)维度存储,而不是 useRef。这样即便在 React 18 的 Strict Mode 下,开发环境那次"先卸载再重挂载"也不会清空缓存。对同一个缓存 key,只在路由变化时更新 route 对象,复用原来的组件引用,避免懒加载组件被重复创建。
转场动画时序
用双帧 requestAnimationFrame(代码里叫 nextFrame)确保浏览器在类名变化之间完成一次 paint,这样 CSS transition 才能正确触发——单帧 rAF 在某些浏览器上不够稳定。过渡结束时间从 getComputedStyle 读取 transition-duration + transition-delay 和 animation-duration + animation-delay 取最大值,开发者不需要在 JS 和 CSS 两边同步维护时长。
文件路由的匹配排序
路由按「静态 > 动态(:param) > 通配符(*)」排序,避免 :id 拦截掉 about 这类静态路径。无 _layout 的子目录路由会"提升"到父层并拼接路径前缀,保持结构扁平。
守卫执行流水线
完整复刻 Vue Router 的守卫顺序:组件 leave → 全局 beforeEach → 组件 update → 路由级 beforeEnter。每个守卫通过 Promise 串行执行,支持 next(false) 取消、next('/path') 重定向。如果守卫抛出异常,会被 router.onError 捕获,不会中断应用。
与 React Router 的对比
| 功能 | rvue-router | React Router |
|---|---|---|
| 统一路由对象 | useRoute() 一个 hook 全包 | useParams() + useSearchParams() + useLocation() |
| 路由元信息 | 内置 meta 字段 | 无原生支持 |
| 全局导航守卫 | router.beforeEach / afterEach | 需自己实现 |
| 组件级守卫 | useBeforeRouteLeave / useBeforeRouteUpdate | 无原生支持 |
| 文件系统路由 | 内置 Vite 插件 | 需要框架(Remix / Next.js) |
| 路由转场动画 | 内置,零依赖,Vue Transition 类名约定 | 需要 Framer Motion 等第三方库 |
| Keep-Alive 缓存 | 内置,支持 include / exclude / max / LRU | 无原生支持 |
| 动态路由 | addRoute / removeRoute | 有,但 API 不同 |
| 命名视图 | components + <RouterView name> | 无原生支持 |
| 滚动行为 | 内置 scrollBehavior | 需自己实现 |
| TypeScript | 完整类型 + 类型增强 | 完整类型 |
| 零运行时依赖 | 仅 React 作为 peer dep | 是 |
并非说 React Router 不好——它在自己的设计理念下做得很出色。但如果你更喜欢 Vue Router 的 API 风格,或者你的团队同时维护 Vue 和 React 项目希望统一心智模型,rvue-router 会是一个值得尝试的选择。
安装
npm install @tangmu1121/rvue-router
# or
pnpm add @tangmu1121/rvue-router
# or
yarn add @tangmu1121/rvue-router
npm 地址:@tangmu1121/rvue-router
最后
这个库目前已发布 v0.3.4,核心功能都已稳定:
- Vue Router 风格的完整 API:
createRouter、useRoute、useRouter、beforeEach/afterEach、命名路由、路由别名…… - 文件系统路由:Vite 插件一键接入,自动路由名称 +
.route.ts配置文件 + HMR 热更新 - 路由缓存(Keep-Alive):include / exclude / max / LRU 淘汰 +
useKeepAliveActive激活状态检测 - 路由转场动画:Vue
<Transition>六阶段类名约定,支持 Tailwind / CSS Modules / 自定义类名 - 完整 TypeScript 类型:类型导出、模块增强、
defineRouteConfig类型推断 - 零运行时依赖:只有 React 作为 peer dep,打包体积友好
如果你也是个 Vue 转 React(或者两个都写)的开发者,欢迎试试看。
有问题或建议欢迎在 npm 页面留言或提 issue,我会持续迭代。