我把 Vue Router 搬到了 React —— 从 API 到文件路由、转场动画,一个都不少

81 阅读6分钟

如果你同时写 Vue 和 React,一定懂那种感觉:切回 React 项目,想用 useRoute() 拿参数,却发现根本没有这个 hook。


起因

我平时 Vue 和 React 都写。Vue Router 的体验一直让我很满意——useRouteuseRouter、导航守卫、嵌套路由、文件路由……每一块都设计得恰到好处。

切回 React 项目,用 React Router 时总觉得哪里别扭:

  • useParamsuseSearchParams 是两个 hook,而不是一个统一的 route 对象
  • 没有全局导航守卫,鉴权逻辑得自己包一层
  • 文件路由要靠框架(Next.js / Remix),单独用 Vite 就得手写
  • 路由切换动画没有官方方案

于是我决定自己搓一个:把 Vue Router 的 API 完整搬到 React,同时加上文件系统路由和转场动画。

这就是 @tangmu1121/rvue-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')) },
    { 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-fromname-leave-active
下一帧name-leave-toname-leave-from
离开结束name-leave-activename-leave-to
进入开始name-enter-fromname-enter-active
下一帧name-enter-toname-enter-from
进入结束name-enter-activename-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)。

转场动画时序:用双帧 requestAnimationFramenextFrame)确保浏览器在类名变化之间完成一次 paint,这样 CSS transition 才能正确触发。自动从 getComputedStyle 读取 transition-duration + transition-delay 计算最大时长,不需要手动指定。

文件路由路径匹配:路由按静态 > 动态 > 通配符排序,避免 :idabout 拦截掉。无 _layout 的子目录路由会"提升"到父层并拼接路径前缀,保持扁平结构。

守卫取消函数beforeEachafterEachonError 均返回取消函数,便于动态注册/注销,不会内存泄漏。


与 React Router 的对比

功能rvue-routerReact 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。