本页面介绍使用 @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/ # 类型定义
()
组织相关功能✅ 推荐
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}
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}
遵循这些最佳实践可以帮助你: