深入了解 @feoe/fs-router 的路由生成机制和自定义选项。
@feoe/fs-router 通过扫描文件系统结构,自动生成 React Router 兼容的路由配置。生成过程包括:
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 支持时,会生成包含 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})
路由未生成
导入路径错误
类型错误