0.2.8 • Published 10 months ago

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

Weekly downloads
-
License
MIT
Repository
github
Last release
10 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

11 months ago

0.1.11

11 months ago

0.1.12

11 months ago

0.1.13

11 months ago

0.1.14

11 months ago

0.0.11

11 months ago

0.0.12

11 months ago

0.1.0

11 months ago

0.2.1

10 months ago

0.1.2

11 months ago

0.2.0

10 months ago

0.1.1

11 months ago

0.2.7

10 months ago

0.1.8

11 months ago

0.2.6

10 months ago

0.1.7

11 months ago

0.2.8

10 months ago

0.1.9

11 months ago

0.2.3

10 months ago

0.2.2

10 months ago

0.2.5

10 months ago

0.1.6

11 months ago

0.2.4

10 months ago

0.1.5

11 months ago

0.0.10

2 years ago

0.0.9

2 years ago

0.0.8

2 years ago

0.0.7

2 years ago

0.0.6

2 years ago

0.0.5

2 years ago

0.0.4

2 years ago

0.0.3

2 years ago

0.0.2

2 years ago

0.0.1

2 years ago