如果你同时写 Vue 和 React,一定懂那种感觉:切回 React 项目,想用
useRoute()拿参数,却发现根本没有这个 hook。
起因
我平时 Vue 和 React 都写。Vue Router 的体验一直让我很满意——useRoute、useRouter、导航守卫、嵌套路由、文件路由……每一块都设计得恰到好处。
切回 React 项目,用 React Router 时总觉得哪里别扭:
useParams和useSearchParams是两个 hook,而不是一个统一的route对象- 没有全局导航守卫,鉴权逻辑得自己包一层
- 文件路由要靠框架(Next.js / Remix),单独用 Vite 就得手写
- 路由切换动画没有官方方案
于是我决定自己搓一个:把 Vue Router 的 API 完整搬到 React,同时加上文件系统路由和转场动画。
它长什么样
先看三步起步:
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')) },
{ path: '/about', name: 'about', component: lazy(() => import('@/pages/About')) },
{ path: '/users/:id', name: 'user-detail', component: lazy(() => import('@/pages/User')) },
{ path: '*', component: lazy(() => import('@/pages/NotFound')) },
],
})
// 全局鉴权守卫
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !localStorage.getItem('token')) {
next('/login')
} else {
next()
}
})
第二步:注入 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()
// 动态参数
const { id } = route.params
// 查询参数
const page = route.query.page
// 路由元信息
const title = route.meta.title
// 完整路径、matched 链……都在这里
}
对比 React Router:useParams() + useSearchParams() + 自己实现 meta。
2. 导航守卫 —— 完整的四阶段执行链
每次路由切换,守卫按以下顺序执行:
组件 useBeforeRouteLeave → 全局 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()
})
}
3. RouterLink —— 智能激活状态
// 前缀匹配时加 active 类,精确匹配时加 active-exact 类
<RouterLink to="/home" activeClass="active" exactActiveClass="active-exact">
首页
</RouterLink>
// 精确匹配时自动添加 aria-current="page",满足无障碍标准
// Ctrl/Meta/Shift 点击时走浏览器默认行为(新标签页打开)
// disabled 状态渲染为 <a> 但阻止跳转
<RouterLink to="/admin" disabled>管理员</RouterLink>
重头戏一:文件系统路由
这是我最花时间的部分。只需要一个 Vite 插件,创建文件就等于注册路由。
// 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'
加上 _layout.tsx 就能做嵌套路由:
src/pages/
_layout.tsx ← 根布局
index.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: 'users',
name: 'users',
component: lazy(() => import('./pages/users/_layout.tsx')),
children: [
{ path: '', name: 'users', component: lazy(...) },
{ path: ':id', name: 'users-id', component: lazy(...) },
],
},
],
}]
HMR 支持: 新增/删除文件自动触发路由更新,开发体验丝滑。
同级路由配置文件(*.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 到路由对象上。页面逻辑和路由配置完全分离,整洁。
重头戏二:路由转场动画
这块我参照 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 |
几个常用的动画效果
/* 水平滑动 */
.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?也支持
<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',
}}
/>
不同路由用不同动画
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' } },
其他细节
动态路由
// 登录后按权限动态添加路由
router.addRoute({ path: '/admin', component: AdminPage })
router.addRoute({ path: 'logs', component: Logs }, 'admin') // 添加到 admin 子路由
// 退出时清理
router.removeRoute('admin')
// 检查是否存在
router.hasRoute('admin')
useIsNavigating —— 全局加载指示器
function GlobalProgressBar() {
const isNavigating = useIsNavigating()
return isNavigating ? <ProgressBar /> : null
}
router.isReady() —— 等待初始导航
// SSR 或需要在路由就绪后再执行某些逻辑
await router.isReady()
三种历史模式
createWebHistory() // /path 需要服务器配置
createHashHistory() // /#/path 无需服务器配置
createMemoryHistory() // 内存 SSR / 测试
技术实现简记
几个有意思的实现细节:
响应式路由:基于 useSyncExternalStore,保证所有订阅者在路由变化时同步更新,不会出现撕裂(tearing)。
转场动画时序:用双帧 requestAnimationFrame(nextFrame)确保浏览器在类名变化之间完成一次 paint,这样 CSS transition 才能正确触发。自动从 getComputedStyle 读取 transition-duration + transition-delay 计算最大时长,不需要手动指定。
文件路由路径匹配:路由按静态 > 动态 > 通配符排序,避免 :id 把 about 拦截掉。无 _layout 的子目录路由会"提升"到父层并拼接路径前缀,保持扁平结构。
守卫取消函数:beforeEach、afterEach、onError 均返回取消函数,便于动态注册/注销,不会内存泄漏。
与 React Router 的对比
| 功能 | rvue-router | React Router |
|---|---|---|
| 统一路由对象 | useRoute() | useParams() + useSearchParams() |
| 全局导航守卫 | router.beforeEach | 需自己实现 |
| 组件级守卫 | useBeforeRouteLeave | 无原生支持 |
| 文件系统路由 | 内置 Vite 插件 | 需要框架(Remix/Next.js) |
| 转场动画 | 内置,零依赖 | 需要 Framer Motion 等 |
| 动态路由 | addRoute / removeRoute | 有,但 API 不同 |
| 路由元信息 | meta 字段 | 无原生支持 |
| TypeScript | 完整类型 | 完整类型 |
安装
npm install @tangmu1121/rvue-router
# or
pnpm add @tangmu1121/rvue-router
# or
yarn add @tangmu1121/rvue-router
npm 地址:www.npmjs.com/package/@ta…
最后
这个库目前已发布 v0.3.1,核心功能都已稳定:
- ✅ Vue Router 风格的完整 API
- ✅ 文件系统路由 + 自动路由名称 +
.route.ts配置文件 - ✅ 路由转场动画(支持 Tailwind / CSS Modules)
- ✅ 完整 TypeScript 类型
- ✅ 零运行时依赖(只有 React 作为 peer dep)
如果你也是个 Vue 转 React(或者两个都写)的开发者,欢迎试试。有问题或建议欢迎提 issue。