npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tangmu1121/rvue-router

v0.3.4

Published

Vue Router-style routing for React — createRouter, useRoute, useRouter, file-based routing, navigation guards, and more.

Readme

@tangmu1121/rvue-router

为 React 提供 Vue Router 风格的路由方案。如果你喜欢 Vue Router 的 API 设计,这个库将带给你完全一致的开发体验。

目录


特性

  • createRouter 工厂函数,插件化配置风格
  • useRouter / useRoute hooks,完全响应式
  • <RouterView> 支持嵌套布局(多层路由出口)
  • <RouterLink> 支持 activeClassexactActiveClassdisabledaria-current
  • useLink hook,用于构建自定义导航组件
  • 导航守卫:全局 beforeEachafterEach,路由级 beforeEnter
  • 组件级守卫:useBeforeRouteLeaveuseBeforeRouteUpdate
  • router.addRoute / removeRoute 动态添加/删除路由
  • router.resolve — 仅解析路由,不执行跳转
  • router.isReady() — 等待初始导航完成的 Promise
  • useIsNavigating — 全局导航加载状态 hook
  • scrollBehavior — 自定义滚动恢复逻辑
  • 路由 alias 别名支持
  • 命名视图(components: { default, sidebar }
  • 三种历史模式:createWebHistory / createHashHistory / createMemoryHistory
  • 完整的 TypeScript 类型支持
  • 文件系统路由:Vite 插件,按目录结构自动生成路由配置
  • 路由缓存(Keep-Alive):include / exclude / max,useKeepAliveActive 实现激活/未激活

安装

npm install @tangmu1121/rvue-router
pnpm add @tangmu1121/rvue-router
yarn add @tangmu1121/rvue-router

快速上手

第一步:创建路由器

// src/router/index.tsx
import { lazy } from 'react'
import { createRouter, createWebHistory } from '@tangmu1121/rvue-router'

const Home     = lazy(() => import('@/pages/Home'))
const About    = lazy(() => import('@/pages/About'))
const User     = lazy(() => import('@/pages/User'))
const NotFound = lazy(() => import('@/pages/NotFound'))

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

  // 滚动行为:前进/后退时恢复原位置,新导航时回到顶部
  scrollBehavior(_to, _from, savedPosition) {
    return savedPosition ?? { top: 0 }
  },

  routes: [
    { path: '/', redirect: '/home' },
    { path: '/home',  name: 'home',  component: Home,  meta: { title: '首页' } },
    { path: '/about', name: 'about', component: About, meta: { title: '关于' } },
    {
      path: '/users',
      children: [
        { path: '',    name: 'user-list',   component: User },
        { path: ':id', name: 'user-detail', component: User },
      ],
    },
    { path: '*', component: NotFound },
  ],
})

// 注册全局前置守卫
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')
})

第二步:注入路由上下文

// src/main.tsx
import { createRoot } from 'react-dom/client'
import App from './App'
import { router } from './router'

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" exactActiveClass="active-exact">首页</RouterLink>
        <RouterLink to="/about" activeClass="active">关于</RouterLink>
      </nav>
      {/* 路由组件在此渲染 */}
      <RouterView />
    </div>
  )
}

路由配置

基础路由

routes: [
  { path: '/home',  component: Home  },
  { path: '/about', component: About },
]

动态路由参数

使用 :参数名 定义动态段,在组件内通过 useRoute().params 获取:

routes: [
  { path: '/users/:id', component: UserDetail },
  // 多段参数
  { path: '/posts/:year/:month/:slug', component: Post },
]
// 在组件中读取
function UserDetail() {
  const route = useRoute()
  const { id } = route.params  // string 类型
  // ...
}

嵌套路由

子路由的 path 相对于父路由,使用空字符串 '' 表示默认子路由:

routes: [
  {
    path: '/users',
    component: UsersLayout,   // 父布局组件,内部需要 <RouterView />
    children: [
      { path: '',    component: UserList   },  // 匹配 /users
      { path: ':id', component: UserDetail },  // 匹配 /users/123
      {
        path: ':id/posts',
        component: UserPosts,
        children: [
          { path: ':postId', component: UserPost }, // 匹配 /users/123/posts/456
        ]
      }
    ],
  },
]

父布局组件中需放置 <RouterView /> 以渲染子路由:

function UsersLayout() {
  return (
    <div>
      <aside>用户侧边栏</aside>
      <main>
        <RouterView />  {/* 子路由渲染在这里 */}
      </main>
    </div>
  )
}

命名路由

为路由添加 name 字段后,可以通过名称进行导航,避免硬编码路径:

routes: [
  { path: '/users/:id', name: 'user-detail', component: UserDetail },
]
// 通过名称导航(name 导航目前需要配合 path 使用,推荐直接用 path)
router.push({ path: '/users/123' })

// RouterLink 同样支持对象形式
<RouterLink to={{ path: '/users/123', query: { tab: 'profile' } }}>
  用户详情
</RouterLink>

路由别名

alias 允许用多个路径访问同一个路由组件:

routes: [
  {
    path: '/users/:id',
    alias: '/u/:id',        // 单个别名
    // alias: ['/u/:id', '/member/:id'],  // 多个别名
    component: UserDetail,
  },
]

重定向

routes: [
  // 字符串重定向
  { path: '/', redirect: '/home' },

  // 对象形式
  { path: '/old-path', redirect: { path: '/new-path' } },

  // 带查询参数
  { path: '/legacy', redirect: { path: '/modern', query: { version: '2' } } },
]

通配符(404 页面)

使用 * 匹配所有未匹配的路径,通常放在路由列表末尾:

routes: [
  // ... 其他路由
  { path: '*', component: NotFound },
]

路由元信息

通过 meta 字段挂载任意数据,在守卫和组件中均可访问:

routes: [
  {
    path: '/dashboard',
    component: Dashboard,
    meta: {
      requiresAuth: true,
      title: '控制台',
      roles: ['admin', 'editor'],
    },
  },
]
// 在守卫中使用
router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth) {
    // 鉴权逻辑
  }
  next()
})

// 在组件中使用
function Dashboard() {
  const route = useRoute()
  console.log(route.meta.title) // '控制台'
}

历史模式

import {
  createWebHistory,
  createHashHistory,
  createMemoryHistory,
} from '@tangmu1121/rvue-router'

| 模式 | URL 形式 | 说明 | |------|----------|------| | createWebHistory() | /path/to/page | 基于 HTML5 History API,需服务器配置回退到 index.html | | createHashHistory() | /#/path/to/page | Hash 模式,无需服务器配置,兼容性好 | | createMemoryHistory() | 内存中 | 不依赖浏览器 API,适合 SSR 或单元测试 |

// Web History(推荐,需要服务器支持)
createRouter({ history: createWebHistory(), routes })

// Hash History(不需要服务器配置)
createRouter({ history: createHashHistory(), routes })

// Memory History(SSR / 测试环境)
createRouter({ history: createMemoryHistory('/initial-path'), routes })

// 也可以直接用字符串简写
createRouter({ history: 'browser', routes })  // createWebHistory
createRouter({ history: 'hash',    routes })  // createHashHistory
createRouter({ history: 'memory',  routes })  // createMemoryHistory

Nginx 服务器配置示例(Web History 模式)

location / {
  try_files $uri $uri/ /index.html;
}

导航守卫

导航守卫按以下顺序依次执行:

  1. 组件 useBeforeRouteLeave(离开旧组件)
  2. 全局 beforeEach
  3. 组件 useBeforeRouteUpdate(参数更新但组件复用时)
  4. 路由级 beforeEnter(进入新路由时)

每个守卫都接收 (to, from, next) 三个参数:

  • next() — 确认导航
  • next(false) — 取消导航,停留在当前页面
  • next('/path') — 重定向到新路径

全局前置守卫

const removeGuard = router.beforeEach((to, from, next) => {
  // 鉴权示例
  if (to.meta.requiresAuth && !isLoggedIn()) {
    next('/login')
  } else {
    next()
  }
})

// 调用返回值可以移除该守卫
removeGuard()

全局后置守卫

afterEach 在导航完成后执行,不需要调用 next

router.afterEach((to, from) => {
  // 更新页面标题
  document.title = String(to.meta.title ?? 'My App')
  
  // 上报埋点
  analytics.track('page_view', { path: to.fullPath })
})

路由级别守卫

在路由配置中使用 beforeEnter,仅在进入该路由时执行:

routes: [
  {
    path: '/admin',
    component: AdminPage,
    beforeEnter: (to, from, next) => {
      if (!hasAdminRole()) {
        next('/403')
      } else {
        next()
      }
    },
  },
  {
    path: '/profile',
    component: ProfilePage,
    // 也可以是守卫数组,按顺序执行
    beforeEnter: [checkAuth, checkEmailVerified],
  },
]

组件级别守卫

在组件内部使用 hooks 注册守卫:

import { useBeforeRouteLeave, useBeforeRouteUpdate } from '@tangmu1121/rvue-router'

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()
  })
  
  // ...
}

错误处理

router.onError((error) => {
  console.error('路由导航出错:', error)
})

API 参考

createRouter

createRouter(options: RouterOptions): RouterInstance & { RouterProvider }

| 选项 | 类型 | 默认值 | 说明 | |------|------|--------|------| | routes | RouteRecordRaw[] | — | 路由配置数组(必填) | | history | RouterHistory \| 'browser' \| 'hash' \| 'memory' | 'browser' | 历史模式 | | scrollBehavior | ScrollBehaviorHandler | — | 滚动行为控制函数 |


router 实例方法

导航方法

// 推入新历史记录
router.push('/path')
router.push({ path: '/users', query: { page: '2' } })
router.push({ path: '/users/123', hash: '#comments' })

// 替换当前历史记录(不新增历史条目)
router.replace('/path')
router.replace({ path: '/new-page' })

// 历史栈操作
router.back()         // 等同 history.go(-1)
router.forward()      // 等同 history.go(1)
router.go(-2)         // 后退两步
router.go(3)          // 前进三步

路由信息

// 解析路由(不执行跳转)
const resolved = router.resolve('/users/123')
// → RouteLocationNormalized { path, fullPath, params, query, hash, meta, matched }

// 等待初始导航完成
await router.isReady()

// 获取当前路由
const current = router.currentRoute

// 检查是否有某个命名路由
router.hasRoute('user-detail')  // boolean

// 获取所有路由配置
router.getRoutes()  // RouteRecordRaw[]

动态路由

// 添加顶层路由
router.addRoute({ path: '/new-page', name: 'new-page', component: NewPage })

// 添加子路由(指定父路由名称)
router.addRoute({ path: 'settings', name: 'user-settings', component: Settings }, 'user-detail')

// 删除路由(通过名称)
router.removeRoute('new-page')

守卫注册

// 所有注册方法都返回取消函数
const removeBeforeEach = router.beforeEach((to, from, next) => { next() })
const removeAfterEach  = router.afterEach((to, from) => {})
const removeOnError    = router.onError((err) => {})

// 取消注册
removeBeforeEach()

useRouter

获取路由器实例,等同 Vue Router 的 useRouter()

import { useRouter } from '@tangmu1121/rvue-router'

function MyComponent() {
  const router = useRouter()

  const handleClick = () => {
    router.push('/dashboard')
  }

  return <button onClick={handleClick}>跳转</button>
}

useRoute

获取当前路由对象,响应式,路由变化时自动触发重渲染。等同 Vue Router 的 useRoute()

import { useRoute } from '@tangmu1121/rvue-router'

function UserDetail() {
  const route = useRoute()

  return (
    <div>
      <p>路径:{route.path}</p>
      <p>完整路径:{route.fullPath}</p>
      <p>路由名称:{route.name}</p>
      <p>参数 id:{route.params.id}</p>
      <p>查询参数 page:{route.query.page}</p>
      <p>Hash:{route.hash}</p>
    </div>
  )
}

RouteLocationNormalized 结构:

| 字段 | 类型 | 说明 | |------|------|------| | path | string | 当前路径(不含 query 和 hash) | | fullPath | string | 完整路径(含 query 和 hash) | | name | string \| undefined | 路由名称 | | params | Record<string, string> | 动态路由参数 | | query | Record<string, string> | URL 查询参数 | | hash | string | URL hash(含 # 前缀) | | meta | RouteMeta | 路由元信息 | | matched | RouteRecordRaw[] | 从根到当前路由的匹配链 | | redirectedFrom | RouteLocationNormalized \| undefined | 若由重定向触发,记录原始目标 |


useIsNavigating

返回当前是否有导航守卫正在执行,可用于全局加载指示器:

import { useIsNavigating } from '@tangmu1121/rvue-router'

function GlobalLoader() {
  const isNavigating = useIsNavigating()

  if (!isNavigating) return null

  return (
    <div className="global-loader">
      <Spinner />
    </div>
  )
}

useLink

暴露 RouterLink 的核心逻辑,用于构建完全自定义的导航组件:

import { useLink } from '@tangmu1121/rvue-router'

function MyNavLink({ to, children }: { to: string; children: React.ReactNode }) {
  const { href, isActive, isExactActive, navigate } = useLink({ to })

  return (
    <a
      href={href}
      onClick={navigate}
      className={[
        'nav-link',
        isActive      ? 'nav-link--active'       : '',
        isExactActive ? 'nav-link--exact-active' : '',
      ].filter(Boolean).join(' ')}
    >
      {children}
    </a>
  )
}

useLink 参数:

| 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | to | string \| RouteLocationRaw | — | 目标路由(必填) | | replace | boolean | false | 是否使用 router.replace |

useLink 返回值:

| 字段 | 类型 | 说明 | |------|------|------| | href | string | 解析后的 href 字符串,用于 <a href> | | isActive | boolean | 当前路径以 to 开头时为 true(前缀匹配) | | isExactActive | boolean | 当前路径与 to 完全匹配时为 true | | navigate | (e?: MouseEvent) => void | 执行导航,传入 MouseEvent 时自动处理修饰键 |


RouterLink 组件

声明式导航组件,对应 Vue Router 的 <RouterLink>

import { RouterLink } from '@tangmu1121/rvue-router'

// 基础用法
<RouterLink to="/home">首页</RouterLink>

// 带激活样式
<RouterLink to="/home" activeClass="active" exactActiveClass="active-exact">
  首页
</RouterLink>

// 对象形式(支持 query 和 hash)
<RouterLink to={{ path: '/users', query: { page: '2' }, hash: '#list' }}>
  用户列表第 2 页
</RouterLink>

// 替换历史记录(不新增历史条目)
<RouterLink to="/login" replace>登录</RouterLink>

// 禁用状态(渲染为 <a> 但阻止跳转)
<RouterLink to="/admin" disabled>管理员(无权限)</RouterLink>

Props 说明:

| Prop | 类型 | 默认值 | 说明 | |------|------|--------|------| | to | string \| RouteLocationRaw | — | 目标路由(必填) | | replace | boolean | false | 使用 router.replace 而非 router.push | | activeClass | string | '' | 路径前缀匹配时追加的 CSS class | | exactActiveClass | string | '' | 路径精确匹配时追加的 CSS class | | disabled | boolean | false | 禁用导航(追加 aria-disabled) |

RouterLink 支持所有原生 <a> 属性(除 href 外),如 classNamestyleonClick 等。

精确匹配时会自动添加 aria-current="page" 属性,满足无障碍标准。

按住 Ctrl / Meta / Shift / Alt 点击时,会走浏览器默认行为(如在新标签页打开),不触发路由导航。


RouterView 组件

路由出口组件,渲染当前路由匹配的组件:

import { RouterView } from '@tangmu1121/rvue-router'

// 基础用法(渲染默认视图)
<RouterView />

// 命名视图
<RouterView name="sidebar" />

// 自定义加载占位内容
<RouterView fallback={<div>页面加载中...</div>} />

Props 说明:

| Prop | 类型 | 默认值 | 说明 | |------|------|--------|------| | name | string | 'default' | 命名视图名称,对应路由配置中 components 的 key | | fallback | ReactNode | null(不渲染) | Suspense 加载占位内容,不传则静默等待 | | keepAlive | boolean \| KeepAliveConfig | — | 是否缓存路由组件(等同 Vue keep-alive),见下方「路由缓存」 | | transition | string \| TransitionConfig | — | 路由切换转场动画 |

RouterView 内置 <Suspense> 支持,配合 React.lazy() 懒加载组件时无需手动包裹。


路由缓存(Keep-Alive)

与 Vue 的 <keep-alive> 等价:离开页面时不销毁组件,再次进入时保留状态(表单、滚动位置、定时器等)。

基础用法

// 全部路由都缓存
<RouterView keepAlive />

// 按配置缓存
<RouterView
  keepAlive={{
    include: ['user-detail', 'settings'],  // 只缓存这些 name
    exclude: /^Admin/,                     // 不缓存 name 匹配该正则的
    max: 10,                               // 最多缓存 10 个,超出按 LRU 淘汰
  }}
/>

include / exclude

  • include:只有匹配到的路由会被缓存;不传则与「是否缓存」无关(由 exclude 决定)。
  • exclude:匹配到的路由不会被缓存,优先级高于 include。

匹配规则(对 route.nameroute.path 生效):

| 类型 | 说明 | |------|------| | string | 与 route.nameroute.path 精确相等 | | RegExp | 对 route.nameroute.path 执行 test() | | string[] | 任一元素匹配即命中 |

// 只缓存列表和详情
<RouterView keepAlive={{ include: ['user-list', 'user-detail'] }} />

// 不缓存管理后台
<RouterView keepAlive={{ exclude: ['admin', 'admin-login'] }} />

// 正则:只缓存 name 以 Page 结尾的
<RouterView keepAlive={{ include: /Page$/ }} />

max(LRU 淘汰)

设置最多缓存的组件实例数,超出时按访问顺序淘汰最久未用的:

<RouterView keepAlive={{ max: 10 }} />

激活 / 未激活:useKeepAliveActive

缓存中的页面在「被展示」和「在后台」时可用 useKeepAliveActive() 区分,用于暂停轮询、视频等:

import { useKeepAliveActive } from '@tangmu1121/rvue-router'

function DataList() {
  const isActive = useKeepAliveActive()

  useEffect(() => {
    if (!isActive) return
    const id = setInterval(fetchList, 5000)
    return () => clearInterval(id)
  }, [isActive])

  return <div>...</div>
}
  • true:当前是展示中的页面,或未使用 Keep-Alive。
  • false:当前在缓存中但被隐藏(相当于 Vue 的 deactivated)。

与 transition 的关系

开启 keepAlive 时,不会同时应用 transition(避免两层动画逻辑)。若需要「缓存 + 转场」,可在外层自行包一层动画组件。


命名视图

当一个路由需要同时渲染多个并列组件时,可使用命名视图:

// 路由配置
routes: [
  {
    path: '/layout',
    components: {
      default: MainContent,    // 对应 <RouterView />
      sidebar: SidebarPanel,   // 对应 <RouterView name="sidebar" />
      header:  PageHeader,     // 对应 <RouterView name="header" />
    },
  },
]
// 布局组件中同时渲染多个出口
function AppLayout() {
  return (
    <div className="app-layout">
      <header>
        <RouterView name="header" />
      </header>
      <aside>
        <RouterView name="sidebar" />
      </aside>
      <main>
        <RouterView />  {/* 等同 name="default" */}
      </main>
    </div>
  )
}

动态路由

在运行时动态添加或删除路由,适用于权限路由、插件化路由等场景:

// 在用户登录后,根据权限动态添加路由
async function setupRoutes(userRoles: string[]) {
  await router.isReady()

  if (userRoles.includes('admin')) {
    router.addRoute({
      path: '/admin',
      name: 'admin',
      component: AdminDashboard,
      meta: { title: '管理后台' },
    })
  }

  // 添加到指定父路由下
  router.addRoute(
    { path: 'reports', name: 'admin-reports', component: Reports },
    'admin'  // 父路由名称
  )
}

// 注销后清理路由
function cleanup() {
  router.removeRoute('admin')
  router.removeRoute('admin-reports')
}

// 检查路由是否存在
if (router.hasRoute('admin')) {
  console.log('管理员路由已注册')
}

路由转场动画

<RouterView> 内置了与 Vue <Transition> 完全一致的 CSS 类名约定,无需额外依赖即可实现路由切换动画。

基础用法

<RouterView> 传入 transition prop:

// 字符串形式:使用 CSS 类名前缀
<RouterView transition="fade" />

// 对象形式:完整配置
<RouterView transition={{ name: 'slide', duration: 300 }} />

// 首次渲染也执行动画(appear)
<RouterView transition={{ name: 'fade', appear: true }} />

然后在全局 CSS 中定义对应的过渡样式:

/* ─── Fade 淡入淡出 ─────────────────────────────── */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

/* ─── Slide 水平滑动 ────────────────────────────── */
.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 缩放 ─────────────────────────────────── */
.zoom-enter-active,
.zoom-leave-active {
  transition: all 0.25s ease;
}
.zoom-enter-from,
.zoom-leave-to {
  opacity: 0;
  transform: scale(0.95);
}

CSS 类名说明

每次路由切换时,插件按以下顺序在 <RouterView> 的外层 div 上添加/移除 CSS 类(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 |

使用 Tailwind CSS

通过完全自定义类名,可以直接使用 Tailwind 的原子类:

<RouterView
  transition={{
    enterFromClass:   'opacity-0 translate-y-2',
    enterActiveClass: 'transition-all duration-300 ease-out',
    enterToClass:     'opacity-100 translate-y-0',
    leaveFromClass:   'opacity-100 translate-y-0',
    leaveActiveClass: 'transition-all duration-200 ease-in',
    leaveToClass:     'opacity-0 -translate-y-2',
  }}
/>

使用 CSS Modules

import styles from './transitions.module.css'

<RouterView
  transition={{
    enterFromClass:   styles.enterFrom,
    enterActiveClass: styles.enterActive,
    enterToClass:     styles.enterTo,
    leaveFromClass:   styles.leaveFrom,
    leaveActiveClass: styles.leaveActive,
    leaveToClass:     styles.leaveTo,
  }}
/>

transition 选项说明

| 选项 | 类型 | 默认值 | 说明 | |------|------|--------|------| | name | string | 'v' | CSS 类名前缀 | | enterFromClass | string | '${name}-enter-from' | 自定义进入起始类 | | enterActiveClass | string | '${name}-enter-active' | 自定义进入激活类 | | enterToClass | string | '${name}-enter-to' | 自定义进入结束类 | | leaveFromClass | string | '${name}-leave-from' | 自定义离开起始类 | | leaveActiveClass | string | '${name}-leave-active' | 自定义离开激活类 | | leaveToClass | string | '${name}-leave-to' | 自定义离开结束类 | | mode | 'out-in' | 'out-in' | 过渡模式(旧出新入) | | duration | number \| { enter: number; leave: number } | 自动检测 | 显式时长(ms) | | appear | boolean | false | 首次渲染时执行进入动画 |

过渡时长检测: 未指定 duration 时,自动从元素的 transition-durationanimation-duration 计算最大时长(含 transition-delay)。

动画期间的快速导航: 若动画进行中触发新的路由跳转,新跳转会被缓存,等当前动画结束后立即执行。

懒加载组件:React.lazy() 完全兼容,进入动画会在组件加载完毕后执行(Suspense 完成时)。

不同路由使用不同动画

通过读取路由 meta 动态决定动画:

import { useRoute } from '@tangmu1121/rvue-router'

function AnimatedRouterView() {
  const route = useRoute()
  const transition = route.meta.transition ?? 'fade'

  return <RouterView transition={transition} />
}
// 路由配置
routes: [
  { path: '/home',      component: Home,     meta: { transition: 'fade' } },
  { path: '/dashboard', component: Dashboard, meta: { transition: 'slide' } },
  { path: '/settings',  component: Settings,  meta: { transition: 'zoom' } },
]

滚动行为

scrollBehavior 函数在每次路由导航后调用,用于控制页面滚动位置:

createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    // 浏览器前进/后退:恢复之前的滚动位置
    if (savedPosition) {
      return savedPosition
    }

    // 带 hash 时滚动到对应锚点
    if (to.hash) {
      return {
        el: to.hash,           // CSS 选择器或 Element
        behavior: 'smooth',    // 平滑滚动
      }
    }

    // 默认回到顶部
    return { top: 0, left: 0 }
  },
})

scrollBehavior 返回值类型:

| 返回值 | 说明 | |--------|------| | { top?: number, left?: number, behavior?: ScrollBehavior } | 滚动到指定坐标 | | { el: string \| Element, top?: number, left?: number, behavior?: ScrollBehavior } | 滚动到指定元素 | | false \| void | 不做任何滚动 |

scrollBehavior 支持返回 Promise,可以在异步操作后再决定滚动位置。


TypeScript 类型

所有类型均从包中导出,可直接使用:

import type {
  // 路由配置
  RouteRecordRaw,
  RouteMeta,
  RouteConfig,          // *.route.ts 同级配置文件的导出类型

  // 转场动画
  TransitionConfig,
  TransitionProp,

  // 路由缓存
  KeepAliveConfig,

  // 路由位置
  RouteLocationRaw,
  RouteLocationNormalized,

  // 守卫
  NavigationGuard,
  NavigationGuardNext,
  AfterEachHook,

  // 滚动行为
  ScrollBehaviorHandler,
  ScrollBehaviorResult,
  ScrollPositionCoords,
  ScrollPositionElement,

  // 路由器
  RouterInstance,
  RouterOptions,
  RouterHistory,
} from '@tangmu1121/rvue-router'

// 运行时辅助
import { defineRouteConfig } from '@tangmu1121/rvue-router'

扩展路由元信息类型

通过 TypeScript 模块扩充可为 RouteMeta 添加类型安全的自定义字段:

// src/types/router.d.ts
import '@tangmu1121/rvue-router'

declare module '@tangmu1121/rvue-router' {
  interface RouteMeta {
    title?: string
    requiresAuth?: boolean
    roles?: string[]
    layout?: 'default' | 'blank' | 'admin'
  }
}

文件系统路由(自动生成)

通过内置的 Vite 插件,按照文件目录结构自动生成路由配置,无需手写 routes 数组。创建页面文件即可完成路由注册。

快速接入

第一步:注册 Vite 插件

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { fileRouter } from '@tangmu1121/rvue-router/vite'

export default defineConfig({
  plugins: [
    react(),
    fileRouter({ dir: 'src/pages' }),  // 扫描 src/pages 目录
  ],
})

第二步:配置 TypeScript 类型(可选)

// tsconfig.json
{
  "compilerOptions": {
    "types": ["@tangmu1121/rvue-router/client"]
  }
}

或在 src/env.d.ts 中添加一行:

/// <reference types="@tangmu1121/rvue-router/client" />

第三步:在路由器中引入

// src/router/index.ts
import routes from 'virtual:rvue-routes'
import { createRouter, createWebHistory } from '@tangmu1121/rvue-router'

export const router = createRouter({
  history: createWebHistory(),
  routes,   // 自动生成的路由配置
})

// 仍然可以注册守卫
router.beforeEach((to, from, next) => {
  next()
})

第四步:创建页面文件

只需在 src/pages 目录下创建文件,路由自动生成:

src/pages/
  index.tsx        →  /
  about.tsx        →  /about
  users/
    index.tsx      →  /users
    [id].tsx       →  /users/:id

文件命名约定

| 文件/目录 | 生成路由 path | 自动 name | 说明 | |-----------|--------------|-----------|------| | index.tsx | ''(子路由)或 /(根) | 'index' | 当前层级的索引页 | | about.tsx | about / /about | 'about' | 静态路径段 | | [id].tsx | :id | '...-id' | 动态路由参数 | | [...all].tsx | * | '...-all' | 通配符(捕获所有未匹配路径) | | 404.tsx | * | '404' | 通配符,用于 404 页面 | | _layout.tsx | — | 取目录名 | 布局组件,将目录内路由变为嵌套子路由 | | [page].route.ts | — | — | 同级路由配置,声明 meta / guards / name 覆盖 | | _*.tsx | — | — | 下划线前缀文件均被忽略(除 _layout) | | .hidden.tsx | — | — | 点前缀文件被忽略 |

动态目录名同样支持:[id]/:id,自动 name 中会展开为 id


目录结构示例

示例一:扁平路由(无布局文件)

src/pages/
  index.tsx
  about.tsx
  users/
    index.tsx
    [id].tsx
  [...404].tsx

生成路由(含自动名称):

[
  { 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/index.tsx')) },
  { path: '/users/:id', name: 'users-id', component: lazy(() => import('./pages/users/[id].tsx')) },
  { path: '*',          name: '404',      component: lazy(() => import('./pages/[...404].tsx')) },
]

示例二:带布局的嵌套路由

src/pages/
  _layout.tsx          ← 根布局(包裹所有页面)
  index.tsx
  about.tsx
  users/
    _layout.tsx        ← /users 布局(包裹 /users/* 页面)
    index.tsx
    [id].tsx
    [id]/
      _layout.tsx      ← /users/:id 布局
      posts.tsx
      settings.tsx
  [...404].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(() => import('./pages/users/index.tsx')) },
          {
            path: ':id',
            name: 'users-id',
            component: lazy(() => import('./pages/users/[id]/_layout.tsx')),
            children: [
              { path: 'posts',    name: 'users-id-posts',    component: lazy(() => import('./pages/users/[id]/posts.tsx')) },
              { path: 'settings', name: 'users-id-settings', component: lazy(() => import('./pages/users/[id]/settings.tsx')) },
            ],
          },
        ],
      },
      { path: '*', name: '404', component: lazy(() => import('./pages/[...404].tsx')) },
    ],
  },
]

布局组件内需要放置 <RouterView /> 来渲染子路由:

// src/pages/_layout.tsx
import { RouterView, RouterLink } from '@tangmu1121/rvue-router'

export default function RootLayout() {
  return (
    <div>
      <nav>
        <RouterLink to="/" exactActiveClass="active">首页</RouterLink>
        <RouterLink to="/about" activeClass="active">关于</RouterLink>
        <RouterLink to="/users" activeClass="active">用户</RouterLink>
      </nav>
      <main>
        <RouterView />   {/* 子页面渲染在这里 */}
      </main>
    </div>
  )
}

自动路由名称

插件会根据文件路径自动生成路由 name,规则如下:

| 文件路径 | 自动生成 name | |----------|--------------| | pages/index.tsx | 'index' | | pages/about.tsx | 'about' | | pages/users/index.tsx | 'users'(去掉末尾的 index)| | pages/users/[id].tsx | 'users-id' | | pages/users/[id]/posts.tsx | 'users-id-posts' | | pages/[...404].tsx | '404' |

自动名称可以直接在 router.push / RouterLink 中使用:

// 命名导航
router.push({ name: 'users-id', params: { id: '123' } })

// RouterLink 使用 name(通过 path 传入解析后的路径)
<RouterLink to="/users/123">用户详情</RouterLink>

若需要覆盖自动生成的 name,可使用同级路由配置文件(见下节)。


同级路由配置文件(*.route.ts

对于需要额外配置(metabeforeEnter、自定义 name)的页面, 可在同目录下创建 [页面名].route.ts,插件会自动将其默认导出 spread 到路由对象中。

src/pages/
  users/
    [id].tsx           ← 页面组件
    [id].route.ts      ← 该路由的额外配置(可选)
  dashboard.tsx
  dashboard.route.ts
// src/pages/users/[id].route.ts
import { defineRouteConfig } from '@tangmu1121/rvue-router'

export default defineRouteConfig({
  // 覆盖自动生成的 name
  name: 'user-detail',

  // 路由元信息
  meta: {
    requiresAuth: true,
    title: '用户详情',
  },

  // 路由级前置守卫
  beforeEnter: (to, from, next) => {
    if (!to.params.id) {
      next('/users')
    } else {
      next()
    }
  },
})
// src/pages/dashboard.route.ts
import { defineRouteConfig } from '@tangmu1121/rvue-router'

export default defineRouteConfig({
  name: 'dashboard',
  meta: { requiresAuth: true, title: '控制台', roles: ['admin'] },
  // 也可以传入守卫数组
  beforeEnter: [checkAuth, checkRole],
})

生成的路由对象(以 [id].tsx 为例):

{
  path: '/users/:id',
  name: 'users-id',                       // 先写自动生成的 name
  component: lazy(() => import('...')),
  ...routeConfig,                          // spread 覆盖(name → 'user-detail',加入 meta 和 beforeEnter)
}

defineRouteConfig 是一个纯透传的辅助函数,运行时开销为零,仅提供 TypeScript 类型检查。
配置文件中不能覆盖 pathcomponentcomponentschildren,这些由插件统一管理。


混合使用(自动 + 手动路由)

自动生成的路由与手动添加的路由可以共存:

import autoRoutes from 'virtual:rvue-routes'
import { createRouter, createWebHistory } from '@tangmu1121/rvue-router'

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

// 登录后动态添加权限路由
router.addRoute({
  path: '/admin',
  name: 'admin',
  component: () => import('./pages/admin/index.tsx'),
})

fileRouter 插件选项

fileRouter({
  dir: 'src/pages',                         // 页面目录(相对于项目根目录)
  extensions: ['.tsx', '.ts', '.jsx', '.js'], // 扫描的文件扩展名
})

| 选项 | 类型 | 默认值 | 说明 | |------|------|--------|------| | dir | string | 'src/pages' | 页面目录路径,相对于 Vite 根目录 | | extensions | string[] | ['.tsx', '.ts', '.jsx', '.js'] | 扫描的文件扩展名 |


HMR 热更新

插件自动监听 pages 目录的变化:

| 操作 | 触发行为 | |------|----------| | 新增页面文件 / 目录 | 重新生成路由配置,触发全页面刷新 | | 删除页面文件 / 目录 | 重新生成路由配置,触发全页面刷新 | | 修改 *.route.ts 内容 | 重新生成路由配置,触发全页面刷新(meta / guards 更新) | | 修改页面组件内容 | Vite 标准 HMR,不触发路由重新生成 |


License

MIT