路由生成

深入了解 @feoe/fs-router 的路由生成机制和自定义选项。

生成机制概述

@feoe/fs-router 通过扫描文件系统结构,自动生成 React Router 兼容的路由配置。生成过程包括:

  1. 文件扫描 - 递归扫描路由目录
  2. 路径解析 - 将文件路径转换为路由路径
  3. 组件导入 - 生成动态导入语句
  4. 路由配置 - 构建路由对象
  5. 代码生成 - 输出最终的路由文件

生成的文件结构

基础路由文件

1// 生成的 src/routes.tsx
2import { lazy } from 'react'
3import type { RouteObject } from 'react-router-dom'
4
5// 动态导入组件
6const RootLayout = lazy(() => import('./routes/layout'))
7const HomePage = lazy(() => import('./routes/page'))
8const AboutPage = lazy(() => import('./routes/about/page'))
9const UserLayout = lazy(() => import('./routes/user/layout'))
10const UserPage = lazy(() => import('./routes/user/page'))
11const UserDetailPage = lazy(() => import('./routes/user/[id]/page'))
12
13// 路由配置
14export const routes: RouteObject[] = [
15  {
16    path: '/',
17    element: <RootLayout />,
18    children: [
19      {
20        index: true,
21        element: <HomePage />
22      },
23      {
24        path: 'about',
25        element: <AboutPage />
26      },
27      {
28        path: 'user',
29        element: <UserLayout />,
30        children: [
31          {
32            index: true,
33            element: <UserPage />
34          },
35          {
36            path: ':id',
37            element: <UserDetailPage />
38          }
39        ]
40      }
41    ]
42  }
43]

DataRouter 支持

当启用 DataRouter 支持时,会生成包含 loader 和 action 的路由配置:

1// 生成的 src/routes.tsx (DataRouter 模式)
2import { lazy } from 'react'
3import type { RouteObject } from 'react-router-dom'
4
5// 组件导入
6const UserDetailPage = lazy(() => import('./routes/user/[id]/page'))
7
8// Loader 导入
9import { loader as userDetailLoader } from './routes/user/[id]/page.data'
10import { action as userDetailAction } from './routes/user/[id]/page.data'
11
12export const routes: RouteObject[] = [
13  {
14    path: '/',
15    element: <RootLayout />,
16    children: [
17      {
18        path: 'user/:id',
19        element: <UserDetailPage />,
20        loader: userDetailLoader,
21        action: userDetailAction
22      }
23    ]
24  }
25]

路径转换规则

文件路径到路由路径

文件路径 生成路由 说明
page.tsx / 根路由
about/page.tsx /about 静态路由
[id]/page.tsx /:id 动态参数
[[id]]/page.tsx /:id? 可选参数
[...slug]/page.tsx /* 捕获所有
(group)/page.tsx /page 路由组(不影响路径)

特殊文件处理

src/routes/ ├── layout.tsx → 布局组件,包装子路由 ├── layout.data.ts → 布局组件 loader 用于数据请求 ├── page.tsx → 页面组件,对应路由 ├── page.data.ts → 页面组件 loader 用于数据请求 ├── error.tsx → 错误边界组件 ├── loading.tsx → 加载状态组件,当 loader 执行时起外层 Suspense 的 fallback 组件 └── not-found.tsx → 404 页面组件

自定义生成选项

基础生成配置

1// vite.config.ts
2export default defineConfig({
3  plugins: [
4    FileBasedRouterVite({
5      routesDirectory: 'src/routes',
6      generatedRoutesPath: 'src/routes.tsx',
7      
8      typeGenerateOptions: {
9        routesTypeFile: 'src/routes-type.ts',
10        generateRouteParams: true,
11        generateLoaderTypes: true,
12        routesDirectories: []
13      }
14    })
15  ]
16})

高级生成配置

1export default defineConfig({
2  plugins: [
3    FileBasedRouterVite({
4      typeGenerateOptions: {
5        // 代码生成选项
6        generateOptions: {
7          importStyle: 'dynamic',      // 'static' | 'dynamic'
8          exportStyle: 'named',        // 'default' | 'named'
9          moduleFormat: 'esm',         // 'esm' | 'cjs'
10          target: 'es2020'             // 目标 ES 版本
11        },
12        
13        // 路由元数据
14        includeMetadata: true,         // 包含路由元数据
15        metadataFields: [              // 元数据字段
16          'title',
17          'description',
18          'requiresAuth'
19        ],
20        
21        // 路由排序
22        sortRoutes: true,              // 启用路由排序
23        sortStrategy: 'alphabetical'   // 'alphabetical' | 'depth' | 'custom'
24      }
25    })
26  ]
27})

自定义路由包装器

创建认证路由包装器

1// src/components/AuthRoute.tsx
2import { Navigate, useLocation } from 'react-router-dom'
3import { useAuth } from '@/hooks/useAuth'
4
5interface AuthRouteProps {
6  children: React.ReactNode
7  requiresAuth?: boolean
8  roles?: string[]
9}
10
11export default function AuthRoute({ 
12  children, 
13  requiresAuth = false, 
14  roles = [] 
15}: AuthRouteProps) {
16  const { user, isAuthenticated } = useAuth()
17  const location = useLocation()
18  
19  if (requiresAuth && !isAuthenticated) {
20    return <Navigate to="/login" state={{ from: location }} replace />
21  }
22  
23  if (roles.length > 0 && !roles.some(role => user?.roles?.includes(role))) {
24    return <Navigate to="/unauthorized" replace />
25  }
26  
27  return <>{children}</>
28}

配置自定义包装器

1// vite.config.ts
2export default defineConfig({
3  plugins: [
4    FileBasedRouterVite({
5      typeGenerateOptions: {
6        routesTypeFile: 'src/routes-type.ts',
7        generateRouteParams: true,
8        generateLoaderTypes: true,
9        routesDirectories: []
10      }
11    })
12  ]
13})

生成的路由将使用自定义包装器:

1// 生成的路由文件
2import AuthRoute from '@/components/AuthRoute'
3
4export const routes: RouteObject[] = [
5  {
6    path: '/admin',
7    element: (
8      <AuthRoute requiresAuth={true} roles={['admin']}>
9        <AdminPage />
10      </AuthRoute>
11    )
12  }
13]

元数据生成

路由元数据定义

1// src/routes/user/[id]/page.tsx
2export const meta = {
3  title: '用户详情',
4  description: '查看用户详细信息',
5  requiresAuth: true,
6  roles: ['user', 'admin']
7}
8
9export default function UserDetailPage() {
10  return <div>用户详情</div>
11}

生成包含元数据的路由

1// 生成的路由文件
2export const routes: RouteObject[] = [
3  {
4    path: '/user/:id',
5    element: <UserDetailPage />,
6    handle: {
7      meta: {
8        title: '用户详情',
9        description: '查看用户详细信息',
10        requiresAuth: true,
11        roles: ['user', 'admin']
12      }
13    }
14  }
15]

代码分割策略

按路由分割

1// 默认策略:每个页面组件单独分割
2const HomePage = lazy(() => import('./routes/page'))
3const AboutPage = lazy(() => import('./routes/about/page'))

按功能模块分割

1// 配置按模块分割
2export default defineConfig({
3  plugins: [
4    FileBasedRouterVite({
5      typeGenerateOptions: {
6        routesTypeFile: 'src/routes-type.ts',
7        generateRouteParams: true,
8        generateLoaderTypes: true,
9        routesDirectories: [
10          { prefix: 'user', path: 'src/routes/user' },
11          { prefix: 'admin', path: 'src/routes/admin' },
12          { prefix: 'auth', path: 'src/routes/(auth)' }
13        ]
14      }
15    })
16  ]
17})

生成的代码:

1// 按模块分割的导入
2const UserModule = lazy(() => import('./chunks/user-module'))
3const AdminModule = lazy(() => import('./chunks/admin-module'))

性能优化

预加载策略

1export default defineConfig({
2  plugins: [
3    FileBasedRouterVite({
4      typeGenerateOptions: {
5        routesTypeFile: 'src/routes-type.ts',
6        generateRouteParams: true,
7        generateLoaderTypes: true,
8        routesDirectories: []
9      }
10    })
11  ]
12})

生成优化的导入

1// 优化的导入语句
2const HomePage = lazy(() => 
3  import(
4    /* webpackChunkName: "home" */
5    /* webpackPreload: true */
6    './routes/page'
7  )
8)

调试和开发工具

开发模式增强

1export default defineConfig({
2  plugins: [
3    FileBasedRouterVite({
4      development: {
5        generateDebugInfo: true,     // 生成调试信息
6        includeSourceMaps: true,     // 包含源码映射
7        validateRoutes: true,        // 验证路由配置
8        logGeneration: true          // 记录生成过程
9      }
10    })
11  ]
12})

生成的调试信息

1// 开发模式下生成的调试信息
2if (process.env.NODE_ENV === 'development') {
3  console.log('路由生成信息:', {
4    totalRoutes: 15,
5    generatedAt: new Date().toISOString(),
6    fileCount: {
7      pages: 10,
8      layouts: 3,
9      errors: 2
10    }
11  })
12}

自定义生成器

创建自定义生成器

1// src/custom-generator.ts
2import type { RouteGenerator } from '@feoe/fs-router'
3
4export class CustomRouteGenerator implements RouteGenerator {
5  generate(routes: RouteNode[]): string {
6    // 自定义生成逻辑
7    return this.generateCustomRoutes(routes)
8  }
9  
10  private generateCustomRoutes(routes: RouteNode[]): string {
11    // 实现自定义路由生成
12    return `
13      // 自定义生成的路由
14      export const routes = ${JSON.stringify(routes, null, 2)}
15    `
16  }
17}

使用自定义生成器

1// vite.config.ts
2import { CustomRouteGenerator } from './src/custom-generator'
3
4export default defineConfig({
5  plugins: [
6    FileBasedRouterVite({
7      customGenerator: new CustomRouteGenerator()
8    })
9  ]
10})

故障排除

常见生成问题

  1. 路由未生成

    • 检查文件命名是否符合约定
    • 确认文件导出是否正确
    • 验证目录结构是否正确
  2. 导入路径错误

    • 检查相对路径计算
    • 确认文件扩展名处理
    • 验证别名配置
  3. 类型错误

    • 确保组件导出类型正确
    • 检查 loader/action 函数签名
    • 验证 TypeScript 配置