0.2.8 • Published 5 months ago

@gulibs/vite-plugin-react-auto-routes v0.2.8

Weekly downloads
-
License
MIT
Repository
github
Last release
5 months ago

@gulibs/vite-plugin-react-auto-routes

一个基于文件系统的自动路由生成 Vite 插件,为 React 应用程序提供基于文件系统的自动路由生成功能,支持路由守卫、中间件、性能优化等高级特性。

npm version License: MIT

📚 目录

✨ 功能特性

🚀 核心功能

  • 基于文件系统的路由生成:根据文件结构自动生成路由,零配置即可使用
  • 动态路由支持:支持 [param](单参数)和 [...slug](通配符)语法
  • 嵌套路由:支持布局组件和多层嵌套路由结构
  • TypeScript 支持:完整的类型定义和 IntelliSense 支持,开发体验更佳

🛡️ 高级功能

  • 路由守卫:基于角色和权限的访问控制,支持自定义守卫条件
  • 中间件支持:路由级中间件处理,支持全局和局部中间件
  • 用户认证:内置认证守卫和用户状态管理,支持双 Token 机制
  • 错误处理:优雅的错误边界和 404 页面处理,提升用户体验

⚡ 性能优化

  • 智能缓存:LRU 缓存策略,自动检测文件变更
  • 并行处理:多目录并行路由生成,大幅提升构建速度
  • 防抖节流:避免频繁重新构建,优化开发体验
  • 内存优化:自动垃圾回收和清理机制
  • 性能监控:内置性能追踪器,实时监控执行时间

🎯 开发体验

  • 热重载:文件变更自动更新路由,无需重启开发服务器
  • 调试模式:详细的路由生成日志和性能统计
  • 错误提示:友好的错误信息和修复建议
  • IDE 支持:完整的代码补全和类型检查

📊 性能表现

基于文件系统的路由生成,具有以下优势:

  • 快速构建:增量式路由生成,只处理变更文件
  • 智能缓存:自动缓存路由配置,减少重复解析
  • 并行处理:多文件并行解析,提升大项目构建速度
  • 内存优化:按需加载路由组件,减少内存占用

🚀 快速开始

安装

npm install @gulibs/vite-plugin-react-auto-routes
# 或
yarn add @gulibs/vite-plugin-react-auto-routes
# 或
pnpm add @gulibs/vite-plugin-react-auto-routes

注意:从 v0.2.5 开始,客户端功能(如 RouteProtectionWrapper、hooks 等)已分离到独立的包 @gulibs/auto-routes-client。如果您需要使用路由保护、中间件或其他客户端功能,请同时安装:

npm install @gulibs/auto-routes-client
# 或
yarn add @gulibs/auto-routes-client
# 或
pnpm add @gulibs/auto-routes-client

基础配置

vite.config.ts中配置插件:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import reactAutoRoutes from '@gulibs/vite-plugin-react-auto-routes'

export default defineConfig({
  plugins: [
    react(),
    reactAutoRoutes({
      dirs: [
        {
          dir: 'src/pages',
          baseRoute: '/'
        }
      ]
    })
  ]
})

vite-env.d.ts中配置

/// <reference types="vite/client" />
/// <reference types="@gulibs/vite-plugin-react-auto-routes/react-routes" />

高级配置

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import reactAutoRoutes from '@gulibs/vite-plugin-react-auto-routes'

export default defineConfig({
  plugins: [
    react(),
    reactAutoRoutes({
      dirs: [
        {
          dir: 'src/pages',
          baseRoute: '/',
          include: ['**/*.tsx', '**/*.ts'],
          exclude: ['**/components/**', '**/*.test.*']
        }
      ],
      // 基础配置
      cache: true,
      cacheTime: 60000, // 1分钟缓存
      debug: true,
      dynamic: false,
      routeMode: 'flat' // 'flat' | 'nested'
    })
  ]
})

使用路由

// src/App.tsx
import { Routes, Route } from 'react-router'
import { routes } from 'virtual:react-routes'

function App() {
  return (
    <Routes>
      {routes.map((route, index) => (
        <Route key={index} {...route} />
      ))}
    </Routes>
  )
}

🎯 应用程序入口配置

基础配置

在应用程序的主入口文件(通常是 src/main.tsx)中,您需要导入自动生成的路由并配置 React Router。

// src/main.tsx
import { createRoot } from 'react-dom/client';
import { createBrowserRouter, RouterProvider } from 'react-router';
import { routes } from "~react-auto-pages";
import './index.css';

// 创建浏览器路由器
const router = createBrowserRouter(routes);

const root = createRoot(document.getElementById('root')!)

root.render(
  <RouterProvider router={router} />
)

不同路由器类型配置

根据您的应用需求,可以选择不同类型的路由器:

浏览器路由器(推荐)

// src/main.tsx
import { createRoot } from 'react-dom/client';
import { createBrowserRouter, RouterProvider } from 'react-router';
import { routes } from "~react-auto-pages";

const router = createBrowserRouter(routes);

const root = createRoot(document.getElementById('root')!)
root.render(<RouterProvider router={router} />)

哈希路由器

适用于不支持 HTML5 History API 的环境:

// src/main.tsx
import { createRoot } from 'react-dom/client';
import { createHashRouter, RouterProvider } from 'react-router';
import { routes } from "~react-auto-pages";

const router = createHashRouter(routes);

const root = createRoot(document.getElementById('root')!)
root.render(<RouterProvider router={router} />)

内存路由器

适用于测试环境或非浏览器环境:

// src/main.tsx
import { createRoot } from 'react-dom/client';
import { createMemoryRouter, RouterProvider } from 'react-router';
import { routes } from "~react-auto-pages";

const router = createMemoryRouter(routes, {
  initialEntries: ['/'], // 初始路由
  initialIndex: 0
});

const root = createRoot(document.getElementById('root')!)
root.render(<RouterProvider router={router} />)

完整配置示例

结合状态管理、SEO 优化等功能的完整配置:

// src/main.tsx
import { createRoot } from 'react-dom/client';
import { HelmetProvider } from 'react-helmet-async';
import { createHashRouter, RouterProvider } from 'react-router';
import { RecoilRoot } from 'recoil';

import { routes } from "~react-auto-pages";
import './index.css';

// 创建路由器
const router = createHashRouter(routes);

const root = createRoot(document.getElementById('root')!)

root.render(
  <RecoilRoot>
    <HelmetProvider>
      <RouterProvider router={router} />
    </HelmetProvider>
  </RecoilRoot>
)

添加全局错误边界

为了更好的用户体验,建议添加全局错误边界:

// src/main.tsx
import { createRoot } from 'react-dom/client';
import { createBrowserRouter, RouterProvider } from 'react-router';
import { ErrorBoundary } from 'react-error-boundary';
import { routes } from "~react-auto-pages";

const router = createBrowserRouter(routes);

function ErrorFallback({error, resetErrorBoundary}) {
  return (
    <div className="error-fallback">
      <h2>出现了错误</h2>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>重试</button>
    </div>
  )
}

const root = createRoot(document.getElementById('root')!)

root.render(
  <ErrorBoundary
    FallbackComponent={ErrorFallback}
    onError={(error) => console.error('Application error:', error)}
  >
    <RouterProvider router={router} />
  </ErrorBoundary>
)

路由器配置选项

您可以为路由器添加额外的配置选项:

// src/main.tsx
import { createBrowserRouter, RouterProvider } from 'react-router';
import { routes } from "~react-auto-pages";

const router = createBrowserRouter(routes, {
  // 基础路径配置
  basename: process.env.NODE_ENV === 'production' ? '/my-app' : '/',

  // 未来路由配置
  future: {
    v7_startTransition: true,
    v7_normalizeFormMethod: true
  }
});

// 添加路由监听器
router.subscribe((state) => {
  console.log('Route changed:', state.location.pathname);
});

const root = createRoot(document.getElementById('root')!)
root.render(<RouterProvider router={router} />)

开发环境调试

在开发环境中,您可以添加一些调试功能:

main.tsx

// src/main.tsx
import { createRoot } from 'react-dom/client';
import { createBrowserRouter, RouterProvider } from 'react-router';
import { routes } from "~react-auto-pages";

const router = createBrowserRouter(routes);

// 开发环境路由调试
if (process.env.NODE_ENV === 'development') {
  console.log('Generated routes:', routes);

  // 监听路由变化
  router.subscribe((state) => {
    console.log('Navigation:', {
      pathname: state.location.pathname,
      search: state.location.search,
      hash: state.location.hash
    });
  });
}

const root = createRoot(document.getElementById('root')!)
root.render(<RouterProvider router={router} />)

📁 文件系统路由

约定式路由规则

React Auto Routes 遵循以下文件系统路由约定:

支持的文件类型

文件类型文件名约定说明
页面组件page.tsx, page.ts, index.tsx, index.ts定义路由页面组件
布局组件layout.tsx, layout.ts, _layout.tsx, _layout.ts定义嵌套布局
错误页面error.tsx, 404.tsx, _error.tsx错误边界和 404 页面
加载组件loading.tsx, _loading.tsx加载状态组件
路由处理器handle.ts, _handle.ts路由数据处理
中间件middleware.ts, _middleware.ts路由中间件
路由守卫guard.ts, _guard.ts路由守卫
认证守卫auth.ts, _auth.ts认证检查

动态路由约定

路由模式文件路径生成路由说明
基础路由src/pages/about/page.tsx/about静态路由
单参数路由src/pages/users/[id]/page.tsx/users/:id动态参数
多参数路由src/pages/blog/[year]/[month]/page.tsx/blog/:year/:month多个参数
通配符路由src/pages/docs/[...slug]/page.tsx/docs/*捕获所有

文件优先级

当同一目录下存在多个文件时,按以下优先级处理:

  1. page.tsx > index.tsx > page.ts > index.ts
  2. layout.tsx > _layout.tsx > layout.ts > _layout.ts
  3. error.tsx > 404.tsx > _error.tsx

基本路由

src/pages/
├── page.tsx          # / (首页)
├── about/
│   └── page.tsx      # /about
├── contact/
│   └── page.tsx      # /contact
└── 404.tsx           # 404 页面

动态路由

src/pages/
├── users/
│   ├── page.tsx      # /users
│   └── [id]/
│       └── page.tsx  # /users/:id
├── blog/
│   ├── page.tsx      # /blog
│   └── [slug]/
│       └── page.tsx  # /blog/:slug
└── docs/
    └── [...path]/
        └── page.tsx  # /docs/* (通配符路由)

嵌套路由和布局

src/pages/
├── layout.tsx        # 根布局
├── page.tsx          # /
├── admin/
│   ├── layout.tsx    # 管理布局
│   ├── page.tsx      # /admin
│   ├── users/
│   │   └── page.tsx  # /admin/users
│   └── settings/
│       └── page.tsx  # /admin/settings

特殊文件

src/pages/
├── layout.tsx        # 布局组件
├── error.tsx         # 错误边界
├── loading.tsx       # 加载组件
├── handle.ts         # 路由处理器
├── middleware.ts     # 中间件
├── guard.ts          # 路由守卫
└── auth.ts           # 认证守卫

🛡️ 路由守卫

定义守卫

// src/pages/admin/guard.ts
import { defineGuard } from '@gulibs/auto-routes-client'

export default defineGuard({
  name: 'admin-guard',
  type: 'role',
  redirectTo: '/403',
  errorMessage: '需要管理员权限',
  condition: (context) => {
    return context.user?.role === 'admin'
  }
})

认证守卫

// src/pages/profile/auth.ts
import { defineAuth } from '@gulibs/auto-routes-client'

export default defineAuth({
  name: 'auth-check',
  redirectTo: '/login',
  errorMessage: '请先登录',
  check: (context) => !!context.user,
  roles: ['user', 'admin'],
  permissions: ['read']
})

GuardOptions 接口

interface GuardOptions {
  /** 守卫名称 */
  name?: string;
  /** 守卫类型 */
  type?: 'auth' | 'role' | 'permission' | 'custom';
  /** 访问被拒绝时的重定向路径 */
  redirectTo?: string;
  /** 错误信息 */
  errorMessage?: string;
  /** 守卫条件函数 */
  condition: (context: GuardContext) => boolean | Promise<boolean>;
}

🔧 中间件

定义中间件

// src/pages/blog/middleware.ts
import { defineMiddleware } from '@gulibs/auto-routes-client'

export default defineMiddleware({
  name: 'blog-logger',
  handler: async (context, next) => {
    console.log(`访问博客: ${context.path}`)

    // 执行下一个中间件
    await next()
  }
})

MiddlewareOptions 接口

interface MiddlewareOptions {
  /** 中间件名称 */
  name?: string;
  /** 执行优先级(数字越小越先执行)*/
  priority?: number;
  /** 是否仅在开发环境执行 */
  devOnly?: boolean;
  /** 中间件处理函数 */
  handler: (context: MiddlewareContext, next: () => Promise<void> | void) => Promise<void> | void;
}

全局中间件

// src/middleware/logger.ts
export default defineMiddleware({
  name: 'global-logger',
  handler: async (context, next) => {
    const start = Date.now()

    try {
      await next()
    } finally {
      const duration = Date.now() - start
      console.log(`[${context.path}] ${duration}ms`)
    }
  },
  priority: 0
})

📈 性能监控

使用性能追踪

import { globalPerformanceTracker } from '@gulibs/vite-plugin-react-auto-routes/performance'

// 启动性能监控
globalPerformanceTracker.startTimer('route_generation')

// 结束监控
const duration = globalPerformanceTracker.endTimer('route_generation')

// 获取统计数据
const stats = globalPerformanceTracker.getStats()
console.log('性能统计:', stats)

性能优化建议

// 开发环境配置
{
  cache: true,
  cacheTime: 10000, // 10秒快速响应
  debug: true,
  performance: {
    enableRouteCache: true,
    routeCacheSize: 50,
    debounceDelay: 200,
    enablePerformanceTracking: true
  }
}

// 生产环境配置
{
  cache: true,
  cacheTime: 300000, // 5分钟长缓存
  debug: false,
  performance: {
    enableRouteCache: true,
    routeCacheSize: 500,
    enableGuardCache: true,
    guardCacheSize: 200,
    enableParallelProcessing: true,
    enableMemoryOptimization: true
  }
}

🎯 React Hooks

useRouteProtection

综合的路由保护 Hook:

import { useRouteProtection } from '@gulibs/vite-plugin-react-auto-routes/hooks'

function MyComponent() {
  const { isLoading, guardError, user, hasAccess, performanceStats } = useRouteProtection(
    guards,
    middlewares
  )

  return (
    <div>
      {isLoading && <div>加载中...</div>}
      {guardError && <div>错误: {guardError}</div>}
      {hasAccess && <div>欢迎, {user?.name}!</div>}
    </div>
  )
}

useRouteGuards

专门的守卫 Hook:

import { useRouteGuards } from '@gulibs/vite-plugin-react-auto-routes/hooks'

function GuardedComponent({ guards }: { guards: any[] }) {
  const { isChecking, guardError, hasAccess, retryCheck } = useRouteGuards(guards)

  if (isChecking) return <div>检查权限...</div>
  if (!hasAccess) return <div>访问被拒绝: {guardError}</div>

  return <div>内容</div>
}

useRouteMiddleware

中间件执行 Hook:

import { useRouteMiddleware } from '@gulibs/vite-plugin-react-auto-routes/hooks'

function MiddlewareComponent({ middlewares }: { middlewares: any[] }) {
  const { isExecuting, executionTime, middlewareError, executeAgain } = useRouteMiddleware(middlewares)

  return (
    <div>
      {isExecuting && <div>执行中间件...</div>}
      {executionTime && <div>执行时间: {executionTime}ms</div>}
      {middlewareError && (
        <div>
          中间件错误: {middlewareError}
          <button onClick={executeAgain}>重试</button>
        </div>
      )}
    </div>
  )
}

usePageConfig

页面配置综合 Hook:

import { usePageConfig } from '@gulibs/vite-plugin-react-auto-routes/hooks'

function PageLayout() {
  const { breadcrumbs, meta, layoutSettings, handle } = usePageConfig()

  return (
    <div>
      {/* 面包屑导航 */}
      <nav className="breadcrumbs">
        {breadcrumbs.map((item, index) => (
          <a key={index} href={item.href} className={item.className}>
            {item.startContent}
            {item.children}
            {item.endContent}
          </a>
        ))}
      </nav>

      {/* 页面标题 */}
      <h1>{meta?.title}</h1>
      <p>{meta?.description}</p>

      {/* 根据布局设置调整页面 */}
      <main className={layoutSettings?.layout ? 'with-layout' : 'no-layout'}>
        {/* 页面内容 */}
      </main>

      {/* 页脚 */}
      {layoutSettings?.footer && <footer>页脚内容</footer>}
    </div>
  )
}

useHandle

获取页面 Handle 配置:

import { useHandle } from '@gulibs/vite-plugin-react-auto-routes/hooks'

function PageComponent() {
  const { meta, extras, layoutSettings, breadcrumbs, matches } = useHandle()

  return (
    <div>
      <h1>{meta?.title}</h1>

      {/* 渲染额外内容 */}
      {extras}

      {/* 显示当前匹配的路由信息 */}
      <div>
        当前路由层级: {matches?.length}
      </div>
    </div>
  )
}

useBreadcrumbs

面包屑导航 Hook:

import { useBreadcrumbs } from '@gulibs/vite-plugin-react-auto-routes/hooks'

function BreadcrumbNav({ handle }) {
  const breadcrumbs = useBreadcrumbs(handle)

  if (breadcrumbs.length === 0) return null

  return (
    <nav className="breadcrumb-nav">
      {breadcrumbs.map((crumb, index) => (
        <div key={index} className="breadcrumb-item">
          {crumb.startContent}
          <a
            href={crumb.href}
            className={crumb.className}
            aria-disabled={crumb.isDisabled}
            aria-current={crumb.isCurrent ? 'page' : undefined}
          >
            {crumb.children}
          </a>
          {crumb.endContent}
        </div>
      ))}
    </nav>
  )
}

usePageMeta 和 useLayoutSettings

单独使用元数据和布局设置:

import { usePageMeta, useLayoutSettings } from '@gulibs/vite-plugin-react-auto-routes/hooks'

function DocumentHead() {
  const meta = usePageMeta()

  useEffect(() => {
    if (meta?.title) {
      document.title = meta.title
    }

    if (meta?.description) {
      const metaDesc = document.querySelector('meta[name="description"]')
      if (metaDesc) {
        metaDesc.setAttribute('content', meta.description)
      }
    }
  }, [meta])

  return null
}

function LayoutController() {
  const layoutSettings = useLayoutSettings()

  return (
    <div className={`page-container ${layoutSettings?.layout ? 'with-layout' : 'simple-layout'}`}>
      {/* 根据布局设置渲染不同的结构 */}
    </div>
  )
}

用户状态管理

基于 @gulibs/react-storage 的持久化用户状态管理,支持浏览器刷新后状态保持。

快速开始

import { useUser } from '@gulibs/auto-routes-client';

function App() {
    const { user, isAuthenticated, login, logout } = useUser({
        loginApi: async ({ username, password }) => {
            // 调用你的登录 API
            const response = await fetch('/api/login', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ username, password })
            });
            const data = await response.json();
            return { user: data.user, token: data.token };
        }
    });

    if (!isAuthenticated) {
        return <LoginPage onLogin={login} />;
    }

    return (
        <div>
            <h1>欢迎,{user?.username}!</h1>
            <button onClick={logout}>退出登录</button>
        </div>
    );
}

完整配置

import { useUser, useUserState, useAuthToken } from '@gulibs/auto-routes-client';

function App() {
    // 完整的用户状态管理
    const {
        user,
        token,
        refreshToken,
        isAuthenticated,
        isLoading,
        error,
        login,
        logout,
        refreshUser,
        refreshTokenAction,
        updateUser,
        setToken,
        setRefreshToken,
        clearError
    } = useUser({
        // 可选配置
        storagePrefix: 'myapp',  // 存储键前缀,默认 'auth'
        useSessionStorage: false, // 是否使用 sessionStorage,默认 false(使用 localStorage)

        // 登录 API 函数
        loginApi: async (credentials) => {
            const response = await fetch('/api/login', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(credentials)
            });

            if (!response.ok) throw new Error('Login failed');

            const data = await response.json();
            return { user: data.user, token: data.token, refreshToken: data.refreshToken };
        },

        // 刷新Token API
        refreshTokenApi: async (refreshToken) => {
            const response = await fetch('/api/refresh', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${refreshToken}`
                }
            });

            if (!response.ok) throw new Error('Token refresh failed');

            const data = await response.json();
            return {
                token: data.newToken,
                refreshToken: data.newRefreshToken,
                user: data.user
            };
        },

        // 获取用户信息函数
        fetchUser: async (token) => {
            const response = await fetch('/api/user', {
                headers: { 'Authorization': `Bearer ${token}` }
            });

            if (!response.ok) throw new Error('Failed to fetch user');

            return response.json();
        },

        // 自动刷新用户信息间隔(毫秒,0表示禁用)
        autoRefreshInterval: 300000, // 5分钟

        // Token 过期检查函数
        isTokenExpired: (token) => {
            try {
                const payload = JSON.parse(atob(token.split('.')[1]));
                return Date.now() >= payload.exp * 1000;
            } catch {
                return true;
            }
        },

        // RefreshToken 过期检查函数
        isRefreshTokenExpired: (refreshToken) => {
            try {
                const payload = JSON.parse(atob(refreshToken.split('.')[1]));
                return Date.now() >= payload.exp * 1000;
            } catch {
                return true;
            }
        }
    });

    // 手动刷新Token示例
    const handleRefreshToken = async () => {
        try {
            await refreshTokenAction();
            console.log('Token刷新成功');
        } catch (error) {
            console.error('Token刷新失败:', error);
            // Token刷新失败,通常意味着需要重新登录
        }
    };

    return (
        <div>
            {isLoading && <div>加载中...</div>}
            {error && (
                <div style={{ color: 'red' }}>
                    错误:{error}
                    <button onClick={clearError}>清除错误</button>
                </div>
            )}

            {isAuthenticated ? (
                <div>
                    <h1>欢迎,{user?.username}!</h1>
                    <p>当前Token: {token?.substring(0, 20)}...</p>
                    <p>RefreshToken: {refreshToken ? '已设置' : '未设置'}</p>

                    <div>
                        <button onClick={logout}>登出</button>
                        <button onClick={refreshUser}>刷新用户信息</button>
                        <button onClick={handleRefreshToken}>刷新Token</button>
                        <button onClick={() => updateUser({ lastActive: new Date().toISOString() })}>
                            更新用户信息
                        </button>
                    </div>
                </div>
            ) : (
                <LoginForm onLogin={login} />
            )}
        </div>
    );
}

function LoginForm({ onLogin }) {
    const [credentials, setCredentials] = useState({ username: '', password: '' });

    const handleSubmit = async (e) => {
        e.preventDefault();
        try {
            await onLogin(credentials);
        } catch (error) {
            console.error('Login failed:', error);
        }
    };

    return (
        <form onSubmit={handleSubmit}>
            <input
                type="text"
                placeholder="用户名"
                value={credentials.username}
                onChange={(e) => setCredentials(prev => ({ ...prev, username: e.target.value }))}
            />
            <input
                type="password"
                placeholder="密码"
                value={credentials.password}
                onChange={(e) => setCredentials(prev => ({ ...prev, password: e.target.value }))}
            />
            <button type="submit">登录</button>
        </form>
    );
}

只读用户状态

如果只需要读取用户状态,不需要登录等操作:

import { useUserState } from '@gulibs/auto-routes-client';

function UserProfile() {
    const { user, token, refreshToken, isAuthenticated } = useUserState('myapp');

    if (!isAuthenticated) {
        return <div>请先登录</div>;
    }

    return (
        <div>
            <h2>{user?.username}</h2>
            <p>邮箱:{user?.email}</p>
            <p>角色:{user?.roles?.join(', ')}</p>
            <p>Token状态:{token ? '有效' : '无效'}</p>
            <p>RefreshToken状态:{refreshToken ? '有效' : '无效'}</p>
        </div>
    );
}

Token 管理

单独管理 Token:

import { useAuthToken } from '@gulibs/auto-routes-client';

function ApiService() {
    const {
        token,
        refreshToken,
        setToken,
        setRefreshToken,
        clearToken,
        clearRefreshToken,
        clearAllTokens,
        hasToken,
        hasRefreshToken
    } = useAuthToken('myapp');

    const apiCall = async () => {
        if (!hasToken) {
            throw new Error('No token available');
        }

        const response = await fetch('/api/data', {
            headers: {
                'Authorization': `Bearer ${token}`
            }
        });

        return response.json();
    };

    const handleTokenRotation = async () => {
        if (!hasRefreshToken) {
            throw new Error('No refresh token available');
        }

        try {
            const response = await fetch('/api/refresh', {
                method: 'POST',
                headers: { 'Authorization': `Bearer ${refreshToken}` }
            });

            const data = await response.json();
            setToken(data.newToken);
            setRefreshToken(data.newRefreshToken);
        } catch (error) {
            // 刷新失败,清除所有Token
            clearAllTokens();
            throw error;
        }
    };

    return {
        apiCall,
        handleTokenRotation,
        hasValidTokens: hasToken && hasRefreshToken,
        clearAllTokens
    };
}

⚙️ 配置选项

基础选项

interface UserOptions {
  dirs: RouteOption[]           // 路由目录配置
  cache?: boolean              // 启用缓存
  cacheTime?: number           // 缓存时间(ms)
  debug?: boolean              // 调试模式
  dynamic?: boolean            // 动态导入
  routeMode?: 'nested' | 'flat' // 路由模式
}

路由目录配置

interface RouteOption {
  dir: string                  // 目录路径
  baseRoute: string            // 基础路由
  include?: string[]           // 包含文件模式
  exclude?: string[]           // 排除文件模式
}

📚 示例项目

完整演示项目

查看 examples/auto-routes-demo/ 目录中的完整示例项目,包含:

📁 项目结构

src/pages/
├── page.tsx                    # 首页
├── layout.tsx                  # 根布局
├── loading.tsx                 # 全局加载组件
├── 404.tsx                     # 404 错误页面
├── getting-started/            # 快速开始指南
├── file-system-routing/        # 文件系统路由说明
├── dynamic-routes/             # 动态路由示例
├── route-guards/               # 路由守卫演示
├── middleware/                 # 中间件使用
├── performance/                # 性能优化展示
├── hooks/                      # React Hooks 示例
├── user-management/            # 用户状态管理
├── users/                      # 用户列表和详情(动态路由)
│   ├── page.tsx               # 用户列表页
│   └── [id]/page.tsx          # 用户详情页
├── blog/                       # 博客文章(动态路由)
│   ├── page.tsx               # 文章列表页
│   └── [slug]/page.tsx        # 文章详情页
└── test-nested/                # 嵌套路由测试
    ├── layout.tsx             # 嵌套布局
    ├── page.tsx               # 嵌套首页
    ├── loading.tsx            # 嵌套加载
    ├── middleware.ts          # 嵌套中间件
    ├── guard.ts               # 嵌套守卫
    └── admin/                 # 管理员子路由
        └── auth.ts            # 认证配置

🚀 运行示例

cd examples/auto-routes-demo
npm install
npm run dev

🎮 功能演示

  • 基础路由:文件系统自动路由生成
  • 动态路由:用户详情页 /users/:id 和博客文章 /blog/:slug
  • 嵌套路由:多层布局和子路由结构
  • 路由守卫:基于角色的访问控制
  • 中间件:路由级数据预处理
  • 性能监控:实时性能数据展示
  • 用户状态:持久化用户认证状态
  • 错误处理:优雅的 404 和错误边界
  • TypeScript 支持:完整的类型定义

🔍 调试和开发

开启调试模式

reactAutoRoutes({
  debug: true, // 开启调试输出
  dirs: [...]
})

调试输出示例:

[react-auto-routes] Generated 12 route(s) with 8 import(s) in 45ms
[react-auto-routes] Cache hit rate: 85%
[react-auto-routes] Guard execution: 0.02ms
[react-auto-routes] Middleware execution: 1.15ms
[react-auto-routes] Using cached routes (12 routes, 8 imports)

性能分析

# 运行性能测试
npm run performance-test

# 查看构建分析
npm run build -- --analyze

🛠️ 高级用法

自定义路由处理器

// src/pages/api/users/handle.ts
export default {
  loader: async ({ params, request }) => {
    // 数据加载逻辑
    return await fetchUsers()
  },
  action: async ({ params, request }) => {
    // 表单处理逻辑
    return await createUser(await request.formData())
  },
  shouldRevalidate: ({ currentParams, nextParams }) => {
    // 重新验证条件
    return currentParams.id !== nextParams.id
  }
}

元数据和面包屑

// src/pages/blog/[slug]/handle.ts
export default {
  meta: {
    title: '博客详情',
    description: '查看博客文章详情',
    keywords: ['博客', '文章']
  },
  breadcrumbs: {
    href: '/blog',
    className: 'breadcrumb-item',
    startContent: <HomeIcon />,
    endContent: <ChevronRightIcon />
  }
}

错误边界

// src/pages/error.tsx
import { useRouteError } from 'react-router'

export default function ErrorBoundary() {
  const error = useRouteError()

  return (
    <div className="error-page">
      <h1>出错了!</h1>
      <p>{error?.message || '发生了未知错误'}</p>
    </div>
  )
}

📦 导出模块

主模块

import reactAutoRoutes from '@gulibs/vite-plugin-react-auto-routes'

客户端工具

import {
  defineHandle,
  defineGuard,
  defineAuth,
  defineMiddleware,
  defineRoleGuard,
  definePermissionGuard,
  defineCustomGuard
} from '@gulibs/auto-routes-client'

React Hooks

注意:从 v0.2.5 开始,所有 React Hooks 已迁移到 @gulibs/auto-routes-client 包中。

import {
  useRouteProtection,
  useRouteGuards,
  useRouteMiddleware,
  useUser,
  useUserState,
  useAuthToken,
  useHandle,
  useBreadcrumbs,
  usePageMeta,
  useLayoutSettings,
  usePageConfig
} from '@gulibs/auto-routes-client'

工具函数

注意:从 v0.2.5 开始,所有工具函数已迁移到 @gulibs/auto-routes-client 包中。

import {
  // 路由相关
  normalizePath,
  joinPath,
  matchPath,
  extractParams,

  // 字符串处理
  toCamelCase,
  toKebabCase,
  capitalize,
  truncate,

  // 对象和数组操作
  deepClone,
  deepMerge,
  unique,
  groupBy,

  // 缓存
  SimpleCache,

  // 性能相关
  debounce,
  throttle,
  measureTime,

  // URL处理
  parseQuery,
  buildQuery,
  updateQuery,

  // 错误处理
  safely,
  safelyAsync
} from '@gulibs/auto-routes-client'

性能工具

注意:从 v0.2.5 开始,所有性能工具已迁移到 @gulibs/auto-routes-client 包中。

import {
  PerformanceCache,
  PerformanceTracker,
  BatchProcessor,
  Debouncer,
  MemoryOptimizer,
  globalPerformanceTracker,
  globalMemoryOptimizer,
  measureAsync,
  throttle
} from '@gulibs/auto-routes-client'

运行时

注意:从 v0.2.5 开始,所有运行时功能已迁移到 @gulibs/auto-routes-client 包中。

import {
  RuntimeExecutor,
  createRuntimeContext
} from '@gulibs/auto-routes-client'

错误处理

import {
  AutoRouteError,
  ErrorBuilder,
  logError,
  logWarning,
  logInfo,
  logSuccess
} from '@gulibs/vite-plugin-react-auto-routes/errors'

📄 许可证

MIT © GuLibs

🙏 致谢

感谢所有贡献者和用户的支持!


📞 支持

如果这个项目对你有帮助,请给我们一个 ⭐️ !

功能特性

  • 持久化存储:基于 localStorage/sessionStorage,刷新浏览器状态不丢失
  • 跨标签页同步:多个标签页状态自动同步
  • 完全灵活的泛型支持:无强制字段约束,用户可以定义任意类型结构
  • 智能字段映射:支持任意 API 响应格式,自动映射字段名
  • 强大的数据转换:登录前 / 后、存储前 / 后的完整数据转换链
  • 双 Token 机制:支持 access token 和 refresh token 的完整认证流程
  • 自动 Token 刷新:检测 token 过期并自动使用 refresh token 刷新
  • 自动用户信息刷新:可配置定时刷新用户数据
  • 完善的错误处理:全面的错误捕获和处理机制
  • 完整的 TypeScript 支持:提供完整的类型定义和智能提示
  • 多场景适配:支持微服务、第三方认证(Firebase、Auth0)等各种架构

最佳实践

  1. 合理设置存储类型

    • 敏感应用使用 sessionStorage(关闭浏览器后清除)
    • 一般应用使用 localStorage(持久保存)
  2. Token 安全

    • 设置合理的过期时间
    • 实现自动刷新机制
    • 敏感操作时重新验证
  3. 错误处理

    • 监听 error 状态
    • 提供用户友好的错误提示
    • 实现重试机制
  4. 性能优化

    • 使用 useUserState 进行只读访问
    • 合理设置自动刷新间隔
    • 避免不必要的 API 调用

⚙️ 配置选项

完全自定义的用户类型

import { useUser, FieldMapping, DataTransformers } from '@gulibs/auto-routes-client';

// 完全自定义的用户类型(无需继承任何基础接口)
interface MyCustomUser {
    userId: string;
    fullName: string;
    email: string;
    department: {
        id: number;
        name: string;
    };
    settings: {
        theme: 'light' | 'dark';
        notifications: boolean;
    };
}

// 完全自定义的登录凭据
interface MyLoginData {
    email: string;
    password: string;
    captcha: string;
    rememberMe: boolean;
}

function App() {
    const { user, login, logout } = useUser<MyCustomUser, MyLoginData>({
        loginApi: async (credentials) => {
            const response = await fetch('/api/auth/login', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(credentials)
            });
            const data = await response.json();

            // 你的API返回什么格式都可以,无需遵循任何规则
            return data; // 例如: { userData: {...}, accessToken: "...", refreshToken: "..." }
        },

        // 字段映射:告诉hook你的API字段名
        fieldMapping: {
            userField: 'userData',        // 你的API用户数据字段名
            tokenField: 'accessToken',    // 你的API token字段名
            refreshTokenField: 'refreshToken'
        },

        // 数据转换:完全控制数据的转换过程
        transformers: {
            // 登录前转换凭据(例如加密密码)
            transformCredentials: (credentials) => ({
                ...credentials,
                password: btoa(credentials.password), // base64编码密码
                deviceInfo: navigator.userAgent
            }),

            // 从API响应转换用户数据
            transformUser: (apiUserData) => ({
                userId: apiUserData.id,
                fullName: `${apiUserData.firstName} ${apiUserData.lastName}`,
                email: apiUserData.emailAddress,
                department: apiUserData.dept,
                settings: apiUserData.userSettings
            }),

            // 存储前转换(例如数据压缩、敏感信息移除)
            beforeStore: (userData) => ({
                ...userData,
                // 移除敏感信息,只存储必要数据
                department: { ...userData.department },
                settings: { ...userData.settings }
            }),

            // 读取后转换(例如数据恢复、默认值设置)
            afterRestore: (storedData) => ({
                ...storedData,
                // 添加默认值或计算属性
                settings: {
                    theme: 'light',
                    notifications: true,
                    ...storedData.settings
                }
            })
        }
    });

    return (
        <div>
            {user ? (
                <div>
                    <h1>欢迎,{user.fullName}!</h1>
                    <p>部门:{user.department.name}</p>
                    <p>主题:{user.settings.theme}</p>
                    <button onClick={logout}>登出</button>
                </div>
            ) : (
                <LoginForm onLogin={login} />
            )}
        </div>
    );
}

function LoginForm({ onLogin }) {
    const [credentials, setCredentials] = useState<MyLoginData>({
        email: '',
        password: '',
        captcha: '',
        rememberMe: false
    });

    const handleSubmit = async (e) => {
        e.preventDefault();
        await onLogin(credentials);
    };

    return (
        <form onSubmit={handleSubmit}>
            <input
                type="email"
                value={credentials.email}
                onChange={(e) => setCredentials(prev => ({ ...prev, email: e.target.value }))}
            />
            <input
                type="password"
                value={credentials.password}
                onChange={(e) => setCredentials(prev => ({ ...prev, password: e.target.value }))}
            />
            <input
                type="text"
                placeholder="验证码"
                value={credentials.captcha}
                onChange={(e) => setCredentials(prev => ({ ...prev, captcha: e.target.value }))}
            />
            <label>
                <input
                    type="checkbox"
                    checked={credentials.rememberMe}
                    onChange={(e) => setCredentials(prev => ({ ...prev, rememberMe: e.target.checked }))}
                />
                记住我
            </label>
            <button type="submit">登录</button>
        </form>
    );
}

字段映射和数据转换

// 场景1:API返回的字段名与标准不同
const { user } = useUser<CustomUser, CustomCredentials>({
    fieldMapping: {
        userField: 'profile',           // API返回 { profile: {...}, token: "...", refresh_token: "..." }
        tokenField: 'token',
        refreshTokenField: 'refresh_token'
    }
});

// 场景2:复杂的数据转换
const { user } = useUser<CustomUser, CustomCredentials>({
    transformers: {
        // 转换API响应的用户数据
        transformUser: (apiData) => ({
            id: apiData.user_id,
            name: `${apiData.first_name} ${apiData.last_name}`,
            email: apiData.email_address,
            roles: apiData.permissions.map(p => p.role),
            avatar: apiData.profile_image_url || '/default-avatar.png'
        }),

        // 存储前数据处理
        beforeStore: (userData) => {
            // 移除大对象,只存储关键信息
            const { avatar, ...essentialData } = userData;
            return essentialData;
        },

        // 读取后数据恢复
        afterRestore: (storedData) => ({
            ...storedData,
            // 恢复默认头像
            avatar: storedData.avatar || '/default-avatar.png',
            // 添加计算属性
            displayName: storedData.name || storedData.email
        })
    }
});

// 场景3:第三方API集成(例如Firebase、Auth0等)
const { user } = useUser<FirebaseUser, FirebaseCredentials>({
    loginApi: async (credentials) => {
        const firebaseResponse = await signInWithEmailAndPassword(auth, credentials.email, credentials.password);
        const token = await firebaseResponse.user.getIdToken();

        return {
            user: firebaseResponse.user,
            token: token,
            refreshToken: firebaseResponse.user.refreshToken
        };
    },

    transformers: {
        transformUser: (firebaseUser) => ({
            uid: firebaseUser.uid,
            email: firebaseUser.email,
            displayName: firebaseUser.displayName,
            photoURL: firebaseUser.photoURL,
            emailVerified: firebaseUser.emailVerified
        })
    }
});

微服务架构场景

// 适配不同微服务的响应格式
interface UserServiceResponse {
    success: boolean;
    data: {
        userInfo: CustomUser;
        authToken: string;
        refreshToken: string;
    };
    message: string;
}

const { user } = useUser<CustomUser, CustomCredentials>({
    loginApi: async (credentials) => {
        const response = await fetch('/api/user-service/login', {
            method: 'POST',
            body: JSON.stringify(credentials)
        });
        const result: UserServiceResponse = await response.json();

        if (!result.success) {
            throw new Error(result.message);
        }

        // 返回标准格式
        return {
            user: result.data.userInfo,
            token: result.data.authToken,
            refreshToken: result.data.refreshToken
        };
    },

    // 或者使用字段映射
    fieldMapping: {
        userField: 'data.userInfo',
        tokenField: 'data.authToken',
        refreshTokenField: 'data.refreshToken'
    }
});
0.1.10

5 months ago

0.1.11

5 months ago

0.1.12

5 months ago

0.1.13

5 months ago

0.1.14

5 months ago

0.0.11

5 months ago

0.0.12

5 months ago

0.1.0

5 months ago

0.2.1

5 months ago

0.1.2

5 months ago

0.2.0

5 months ago

0.1.1

5 months ago

0.2.7

5 months ago

0.1.8

5 months ago

0.2.6

5 months ago

0.1.7

5 months ago

0.2.8

5 months ago

0.1.9

5 months ago

0.2.3

5 months ago

0.2.2

5 months ago

0.2.5

5 months ago

0.1.6

5 months ago

0.2.4

5 months ago

0.1.5

5 months ago

0.0.10

1 year ago

0.0.9

1 year ago

0.0.8

1 year ago

0.0.7

1 year ago

0.0.6

1 year ago

0.0.5

1 year ago

0.0.4

1 year ago

0.0.3

1 year ago

0.0.2

1 year ago

0.0.1

1 year ago