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

如果你同时写 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


它长什么样

先看三步起步:

bash 复制代码
npm install @tangmu1121/rvue-router

第一步:创建路由

tsx 复制代码
// 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

tsx 复制代码
// src/main.tsx
const { RouterProvider } = router

createRoot(document.getElementById('root')!).render(
  <RouterProvider>
    <App />
  </RouterProvider>
)

第三步:渲染出口

tsx 复制代码
// 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 拿到所有路由信息

tsx 复制代码
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 直接注册:

tsx 复制代码
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()
  })
}
tsx 复制代码
// 前缀匹配时加 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 插件,创建文件就等于注册路由

ts 复制代码
// vite.config.ts
import { fileRouter } from '@tangmu1121/rvue-router/vite'

export default defineConfig({
  plugins: [react(), fileRouter({ dir: 'src/pages' })],
})
ts 复制代码
// src/router/index.ts
import routes from 'virtual:rvue-routes'  // 自动生成!
import { createRouter, createWebHistory } from '@tangmu1121/rvue-router'

export const router = createRouter({ history: createWebHistory(), routes })

文件命名约定

bash 复制代码
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 就能做嵌套路由:

bash 复制代码
src/pages/
  _layout.tsx         ← 根布局
  index.tsx
  users/
    _layout.tsx       ← /users 布局
    index.tsx
    [id].tsx

生成结果:

ts 复制代码
[{
  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

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> 设计,做到了零额外依赖。

tsx 复制代码
// 一行开启动画
<RouterView transition="fade" />
css 复制代码
/* 在全局 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-to name-leave-from
离开结束 --- name-leave-activename-leave-to
进入开始 name-enter-fromname-enter-active ---
下一帧 name-enter-to name-enter-from
进入结束 --- name-enter-activename-enter-to

几个常用的动画效果

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?也支持

tsx 复制代码
<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',
  }}
/>

不同路由用不同动画

tsx 复制代码
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'  } },

其他细节

动态路由

ts 复制代码
// 登录后按权限动态添加路由
router.addRoute({ path: '/admin', component: AdminPage })
router.addRoute({ path: 'logs', component: Logs }, 'admin') // 添加到 admin 子路由

// 退出时清理
router.removeRoute('admin')

// 检查是否存在
router.hasRoute('admin')

useIsNavigating ------ 全局加载指示器

tsx 复制代码
function GlobalProgressBar() {
  const isNavigating = useIsNavigating()
  return isNavigating ? <ProgressBar /> : null
}

router.isReady() ------ 等待初始导航

ts 复制代码
// SSR 或需要在路由就绪后再执行某些逻辑
await router.isReady()

三种历史模式

ts 复制代码
createWebHistory()    // /path        需要服务器配置
createHashHistory()   // /#/path      无需服务器配置
createMemoryHistory() // 内存         SSR / 测试

技术实现简记

几个有意思的实现细节:

响应式路由 :基于 useSyncExternalStore,保证所有订阅者在路由变化时同步更新,不会出现撕裂(tearing)。

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

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

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


与 React Router 的对比

功能 rvue-router React Router
统一路由对象 useRoute() useParams() + useSearchParams()
全局导航守卫 router.beforeEach 需自己实现
组件级守卫 useBeforeRouteLeave 无原生支持
文件系统路由 内置 Vite 插件 需要框架(Remix/Next.js)
转场动画 内置,零依赖 需要 Framer Motion 等
动态路由 addRoute / removeRoute 有,但 API 不同
路由元信息 meta 字段 无原生支持
TypeScript 完整类型 完整类型

安装

bash 复制代码
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。

相关推荐
网络点点滴2 小时前
Vue组件通信-mitt
前端·javascript·vue.js
拾贰_C2 小时前
[spring boot | springboot web ] spring boot web项目启动失败问题
前端·spring boot·后端
王家视频教程图书馆2 小时前
大前端(原生开发的尽头是html css js)
前端·javascript·css
indexsunny2 小时前
互联网大厂Java面试实录:Spring Boot与微服务在电商场景中的应用解析
java·spring boot·面试·kafka·spring security·电商·microservices
低保和光头哪个先来2 小时前
TinyEditor 篇2:剪贴板粘贴图片并同步上传至服务器
服务器·前端·javascript·css·vue.js
无巧不成书02182 小时前
React Native 深度解析:跨平台移动开发框架(2026实战版)
javascript·react native·react.js
青柠代码录2 小时前
【Vue3】SCSS 基础篇
前端·scss
为美好的生活献上中指2 小时前
*Java 沉淀重走长征路*之——《Java Web 应用开发完全指南:从零到企业实战(两万字深度解析)》
java·开发语言·前端·html·javaweb·js