最佳实践

本页面介绍使用 @feoe/fs-router 的最佳实践和推荐模式。

项目结构

推荐的目录结构

src/ ├── components/ # 可复用组件 │ ├── ui/ # 基础 UI 组件 │ ├── layout/ # 布局相关组件 │ └── business/ # 业务组件 ├── routes/ # 路由文件 │ ├── layout.tsx # 根布局 │ ├── page.tsx # 首页 │ ├── (auth)/ # 认证相关路由组 │ │ ├── login/ │ │ └── register/ │ └── (dashboard)/ # 仪表板路由组 │ ├── layout.tsx │ ├── analytics/ │ └── settings/ ├── services/ # API 服务 ├── hooks/ # 自定义 Hooks ├── utils/ # 工具函数 └── types/ # 类型定义

路由组织原则

  1. 按功能模块分组 - 使用路由组 () 组织相关功能
  2. 合理使用布局 - 为相关页面创建共享布局
  3. 分离关注点 - 将业务逻辑、数据获取和 UI 分离

命名约定

文件命名

✅ 推荐 page.tsx # 页面组件 layout.tsx # 布局组件 error.tsx # 错误边界 loading.tsx # 加载状态 not-found.tsx # 404 页面 ❌ 避免 index.tsx # 容易混淆 HomePage.tsx # 不符合约定 user-page.tsx # 不符合约定

组件命名

1// ✅ 推荐 - 使用 PascalCase
2export default function UserProfilePage() {
3  return <div>用户资料</div>
4}
5
6// ✅ 推荐 - 描述性命名
7export default function UserLayout() {
8  return <div>用户布局</div>
9}
10
11// ❌ 避免 - 过于简单
12export default function Page() {
13  return <div>页面</div>
14}

路由参数命名

✅ 推荐 [id]/page.tsx # 简洁明了 [userId]/page.tsx # 具有描述性 [slug]/page.tsx # 语义清晰 ❌ 避免 [param]/page.tsx # 过于通用 [data]/page.tsx # 不够具体

性能优化

代码分割

1// 使用 React.lazy 进行组件懒加载
2const HeavyComponent = React.lazy(() => import('./HeavyComponent'))
3
4export default function MyPage() {
5  return (
6    <div>
7      <Suspense fallback={<div>加载中...</div>}>
8        <HeavyComponent />
9      </Suspense>
10    </div>
11  )
12}

数据预加载

1// 使用 loader 预加载关键数据
2export async function loader({ params }) {
3  // 并行加载多个数据源
4  const [user, posts] = await Promise.all([
5    fetchUser(params.id),
6    fetchUserPosts(params.id)
7  ])
8  
9  return { user, posts }
10}

缓存策略

1// 实现简单的内存缓存
2const cache = new Map()
3
4export async function loader({ params }) {
5  const cacheKey = `user-${params.id}`
6  
7  if (cache.has(cacheKey)) {
8    return cache.get(cacheKey)
9  }
10  
11  const user = await fetchUser(params.id)
12  cache.set(cacheKey, { user })
13  
14  return { user }
15}

错误处理

分层错误处理

src/routes/ ├── error.tsx # 全局错误处理 ├── user/ │ ├── error.tsx # 用户模块错误处理 │ └── [id]/ │ ├── error.tsx # 用户详情错误处理 │ └── page.tsx

错误边界实现

1// src/routes/error.tsx
2import { useRouteError, isRouteErrorResponse } from 'react-router-dom'
3
4export default function ErrorBoundary() {
5  const error = useRouteError()
6  
7  if (isRouteErrorResponse(error)) {
8    if (error.status === 404) {
9      return (
10        <div className="error-page">
11          <h1>页面未找到</h1>
12          <p>抱歉,您访问的页面不存在。</p>
13        </div>
14      )
15    }
16    
17    if (error.status === 401) {
18      return (
19        <div className="error-page">
20          <h1>未授权访问</h1>
21          <p>请先登录后再访问此页面。</p>
22        </div>
23      )
24    }
25  }
26  
27  return (
28    <div className="error-page">
29      <h1>出现错误</h1>
30      <p>抱歉,发生了意外错误。</p>
31      <details>
32        <summary>错误详情</summary>
33        <pre>{error?.message}</pre>
34      </details>
35    </div>
36  )
37}

数据加载错误处理

1export async function loader({ params }) {
2  try {
3    const user = await fetchUser(params.id)
4    
5    if (!user) {
6      throw new Response('用户不存在', { status: 404 })
7    }
8    
9    return { user }
10  } catch (error) {
11    if (error instanceof Response) {
12      throw error
13    }
14    
15    // 记录错误日志
16    console.error('加载用户数据失败:', error)
17    
18    throw new Response('服务器错误', { status: 500 })
19  }
20}

类型安全

严格的 TypeScript 配置

1// tsconfig.json
2{
3  "compilerOptions": {
4    "strict": true,
5    "noImplicitAny": true,
6    "strictNullChecks": true,
7    "noImplicitReturns": true,
8    "noFallthroughCasesInSwitch": true
9  }
10}

路由参数类型定义

1// 定义参数接口
2interface UserParams {
3  id: string
4}
5
6interface BlogParams {
7  category: string
8  slug: string
9}
10
11// 在组件中使用
12export default function UserPage() {
13  const { id } = useParams<UserParams>()
14  // TypeScript 会确保 id 是 string 类型
15}

测试策略

单元测试

1// UserPage.test.tsx
2import { render, screen } from '@testing-library/react'
3import { createMemoryRouter, RouterProvider } from 'react-router-dom'
4import UserPage from './page'
5
6test('渲染用户页面', () => {
7  const router = createMemoryRouter([
8    {
9      path: '/user/:id',
10      element: <UserPage />,
11      loader: () => ({ user: { id: '1', name: '测试用户' } })
12    }
13  ], {
14    initialEntries: ['/user/1']
15  })
16  
17  render(<RouterProvider router={router} />)
18  
19  expect(screen.getByText('测试用户')).toBeInTheDocument()
20})

集成测试

1// 测试路由导航
2test('用户导航流程', async () => {
3  const router = createMemoryRouter(routes, {
4    initialEntries: ['/']
5  })
6  
7  render(<RouterProvider router={router} />)
8  
9  // 点击用户链接
10  fireEvent.click(screen.getByText('用户'))
11  
12  // 验证导航到用户页面
13  await waitFor(() => {
14    expect(screen.getByText('用户列表')).toBeInTheDocument()
15  })
16})

安全考虑

路由权限控制

1// 创建权限检查 Hook
2function useAuth() {
3  const user = useUser()
4  return {
5    isAuthenticated: !!user,
6    hasRole: (role: string) => user?.roles?.includes(role)
7  }
8}
9
10// 在 loader 中检查权限
11export async function loader({ request }) {
12  const user = await getUser(request)
13  
14  if (!user) {
15    throw redirect('/login')
16  }
17  
18  if (!user.roles.includes('admin')) {
19    throw new Response('权限不足', { status: 403 })
20  }
21  
22  return { user }
23}

输入验证

1// 验证路由参数
2export async function loader({ params }) {
3  const { id } = params
4  
5  // 验证 ID 格式
6  if (!/^\d+$/.test(id)) {
7    throw new Response('无效的用户 ID', { status: 400 })
8  }
9  
10  const user = await fetchUser(id)
11  return { user }
12}

开发工具

调试技巧

1// 开发环境下显示路由信息
2if (process.env.NODE_ENV === 'development') {
3  console.log('当前路由:', location.pathname)
4  console.log('路由参数:', params)
5  console.log('加载数据:', loaderData)
6}

性能监控

1// 监控页面加载时间
2export async function loader({ params }) {
3  const start = performance.now()
4  
5  const data = await fetchData(params.id)
6  
7  const end = performance.now()
8  console.log(`数据加载耗时: ${end - start}ms`)
9  
10  return data
11}

总结

遵循这些最佳实践可以帮助你:

  1. 提高代码质量 - 通过一致的命名和结构约定
  2. 增强性能 - 通过合理的代码分割和缓存策略
  3. 改善用户体验 - 通过完善的错误处理和加载状态
  4. 确保类型安全 - 通过严格的 TypeScript 配置
  5. 提升可维护性 - 通过清晰的项目结构和测试策略