了解如何配置 @feoe/fs-router 的 TypeScript 支持,获得完整的类型安全体验。
1{
2 "compilerOptions": {
3 "target": "ES2020",
4 "lib": ["DOM", "DOM.Iterable", "ES6"],
5 "allowJs": true,
6 "skipLibCheck": true,
7 "esModuleInterop": true,
8 "allowSyntheticDefaultImports": true,
9 "strict": true,
10 "forceConsistentCasingInFileNames": true,
11 "moduleResolution": "node",
12 "resolveJsonModule": true,
13 "isolatedModules": true,
14 "noEmit": true,
15 "jsx": "react-jsx",
16
17 // 路径映射
18 "baseUrl": ".",
19 "paths": {
20 "@/*": ["./src/*"],
21 "@/routes": ["./src/routes.tsx"],
22 "@/routes-type": ["./src/routes-type.ts"]
23 }
24 },
25 "include": [
26 "src/**/*",
27 "src/routes.tsx",
28 "src/routes-type.ts"
29 ],
30 "exclude": [
31 "node_modules",
32 "dist"
33 ]
34}
1// vite.config.ts
2import { defineConfig } from 'vite'
3import { FileBasedRouterVite } from '@feoe/fs-router/vite'
4
5export default defineConfig({
6 plugins: [
7 FileBasedRouterVite({
8 routesDirectory: 'src/routes',
9 generatedRoutesPath: 'src/routes.tsx',
10 enableGeneration: true,
11
12 // TypeScript 配置
13 typeGenerateOptions: {
14 routesTypeFile: 'src/routes-type.ts',
15 generateRouteParams: true,
16 generateLoaderTypes: true,
17 routesDirectories: []
18 }
19 })
20 ]
21})
1interface TypeGenerateOptions {
2 /** 类型文件输出路径 */
3 routesTypeFile: string
4 /** 是否生成路由参数类型 */
5 generateRouteParams?: boolean
6 /** 是否生成 Loader 类型 */
7 generateLoaderTypes?: boolean
8 /** 路由目录配置 */
9 routesDirectories?: RouteDirectory[]
10}
11
12interface RouteDirectory {
13 /** 路由前缀 */
14 prefix?: string
15 /** 路由目录路径 */
16 path: string
17}
1// 生成的 src/routes-type.ts
2export interface RouteParams {
3 '/': {}
4 '/about': {}
5 '/user': {}
6 '/user/:id': { id: string }
7 '/blog/:category/:slug': {
8 category: string
9 slug: string
10 }
11 '/docs/*': { '*': string }
12}
13
14export interface LoaderData {
15 '/user/:id': {
16 user: User
17 posts: Post[]
18 }
19 '/blog/:category/:slug': {
20 post: BlogPost
21 related: BlogPost[]
22 }
23}
24
25export interface ActionData {
26 '/user/:id': {
27 success: boolean
28 message: string
29 }
30}
31
32export interface RouteMeta {
33 '/admin/*': {
34 requiresAuth: true
35 roles: string[]
36 }
37}
1// src/routes/user/[id]/page.tsx
2import { useParams } from 'react-router-dom'
3
4export default function UserPage() {
5 // TypeScript 自动推断 id 为 string 类型
6 const { id } = useParams()
7
8 return <div>用户 ID: {id}</div>
9}
1// src/routes/blog/[category]/[slug]/page.tsx
2import { useParams } from 'react-router-dom'
3
4// 定义参数接口
5interface BlogParams {
6 category: string
7 slug: string
8}
9
10export default function BlogPostPage() {
11 const { category, slug } = useParams<BlogParams>()
12
13 return (
14 <div>
15 <h1>分类: {category}</h1>
16 <h2>文章: {slug}</h2>
17 </div>
18 )
19}
1// src/routes/search/[[...filters]]/page.tsx
2import { useParams } from 'react-router-dom'
3
4interface SearchParams {
5 filters?: string[]
6}
7
8export default function SearchPage() {
9 const params = useParams<SearchParams>()
10 const filters = params.filters || []
11
12 return (
13 <div>
14 <h1>搜索结果</h1>
15 <p>过滤器: {filters.join(', ')}</p>
16 </div>
17 )
18}
1// src/routes/user/[id]/page.tsx
2import { useLoaderData } from 'react-router-dom'
3
4// 定义数据接口
5interface User {
6 id: string
7 name: string
8 email: string
9 avatar?: string
10}
11
12interface UserLoaderData {
13 user: User
14 posts: Post[]
15}
16
17// Loader 函数
18export async function loader({ params }): Promise<UserLoaderData> {
19 const [user, posts] = await Promise.all([
20 fetchUser(params.id),
21 fetchUserPosts(params.id)
22 ])
23
24 return { user, posts }
25}
26
27// 组件中使用
28export default function UserPage() {
29 // TypeScript 自动推断数据类型
30 const { user, posts } = useLoaderData<typeof loader>()
31
32 return (
33 <div>
34 <h1>{user.name}</h1>
35 <p>{user.email}</p>
36 <div>文章数量: {posts.length}</div>
37 </div>
38 )
39}
1// src/utils/loaders.ts
2export interface PaginatedResponse<T> {
3 data: T[]
4 total: number
5 page: number
6 pageSize: number
7}
8
9export async function createPaginatedLoader<T>(
10 fetcher: (page: number, pageSize: number) => Promise<PaginatedResponse<T>>
11) {
12 return async ({ request }): Promise<PaginatedResponse<T>> => {
13 const url = new URL(request.url)
14 const page = parseInt(url.searchParams.get('page') || '1')
15 const pageSize = parseInt(url.searchParams.get('pageSize') || '10')
16
17 return fetcher(page, pageSize)
18 }
19}
20
21// 使用泛型 Loader
22export const loader = createPaginatedLoader<User>(fetchUsers)
1// src/routes/user/[id]/edit/page.tsx
2import { redirect } from 'react-router-dom'
3
4interface UpdateUserData {
5 name: string
6 email: string
7 avatar?: string
8}
9
10interface ActionResult {
11 success: boolean
12 message: string
13 errors?: Record<string, string>
14}
15
16export async function action({ request, params }): Promise<ActionResult | Response> {
17 const formData = await request.formData()
18
19 const userData: UpdateUserData = {
20 name: formData.get('name') as string,
21 email: formData.get('email') as string,
22 avatar: formData.get('avatar') as string || undefined
23 }
24
25 try {
26 await updateUser(params.id, userData)
27 return redirect(`/user/${params.id}`)
28 } catch (error) {
29 return {
30 success: false,
31 message: '更新失败',
32 errors: { general: error.message }
33 }
34 }
35}
36
37export default function EditUserPage() {
38 const actionData = useActionData<typeof action>()
39
40 return (
41 <form method="post">
42 {actionData && !actionData.success && (
43 <div className="error">{actionData.message}</div>
44 )}
45 {/* 表单字段 */}
46 </form>
47 )
48}
1// src/utils/validation.ts
2export interface ValidationResult<T> {
3 success: boolean
4 data?: T
5 errors?: Partial<Record<keyof T, string>>
6}
7
8export function validateUserForm(formData: FormData): ValidationResult<UpdateUserData> {
9 const name = formData.get('name') as string
10 const email = formData.get('email') as string
11
12 const errors: Partial<Record<keyof UpdateUserData, string>> = {}
13
14 if (!name || name.length < 2) {
15 errors.name = '姓名至少需要2个字符'
16 }
17
18 if (!email || !email.includes('@')) {
19 errors.email = '请输入有效的邮箱地址'
20 }
21
22 if (Object.keys(errors).length > 0) {
23 return { success: false, errors }
24 }
25
26 return {
27 success: true,
28 data: { name, email }
29 }
30}
1// src/routes/error.tsx
2import { useRouteError, isRouteErrorResponse } from 'react-router-dom'
3
4interface CustomError {
5 message: string
6 code?: string
7 details?: any
8}
9
10export default function ErrorBoundary() {
11 const error = useRouteError()
12
13 // 类型守卫函数
14 function isCustomError(error: any): error is CustomError {
15 return error && typeof error.message === 'string'
16 }
17
18 if (isRouteErrorResponse(error)) {
19 return (
20 <div className="error-page">
21 <h1>{error.status} {error.statusText}</h1>
22 <p>{error.data}</p>
23 </div>
24 )
25 }
26
27 if (isCustomError(error)) {
28 return (
29 <div className="error-page">
30 <h1>应用错误</h1>
31 <p>{error.message}</p>
32 {error.code && <p>错误代码: {error.code}</p>}
33 </div>
34 )
35 }
36
37 return (
38 <div className="error-page">
39 <h1>未知错误</h1>
40 <p>发生了意外错误</p>
41 </div>
42 )
43}
1// src/types/route-meta.ts
2export interface RouteMeta {
3 title?: string
4 description?: string
5 keywords?: string[]
6 requiresAuth?: boolean
7 roles?: string[]
8 layout?: 'default' | 'admin' | 'auth'
9 breadcrumbs?: BreadcrumbItem[]
10}
11
12export interface BreadcrumbItem {
13 label: string
14 path?: string
15}
16
17// 在路由中使用
18export const meta: RouteMeta = {
19 title: '用户管理',
20 description: '管理系统用户',
21 requiresAuth: true,
22 roles: ['admin'],
23 layout: 'admin',
24 breadcrumbs: [
25 { label: '首页', path: '/' },
26 { label: '管理', path: '/admin' },
27 { label: '用户管理' }
28 ]
29}
1// src/hooks/useRouteMeta.ts
2import { useMatches } from 'react-router-dom'
3import type { RouteMeta } from '@/types/route-meta'
4
5export function useRouteMeta(): RouteMeta {
6 const matches = useMatches()
7
8 // 合并所有匹配路由的元数据
9 return matches.reduce((meta, match) => {
10 const routeMeta = match.handle?.meta as RouteMeta
11 return { ...meta, ...routeMeta }
12 }, {} as RouteMeta)
13}
14
15// 在组件中使用
16export default function PageHeader() {
17 const meta = useRouteMeta()
18
19 return (
20 <header>
21 <h1>{meta.title}</h1>
22 {meta.description && <p>{meta.description}</p>}
23 {meta.breadcrumbs && (
24 <nav>
25 {meta.breadcrumbs.map((item, index) => (
26 <span key={index}>
27 {item.path ? (
28 <Link to={item.path}>{item.label}</Link>
29 ) : (
30 item.label
31 )}
32 {index < meta.breadcrumbs!.length - 1 && ' > '}
33 </span>
34 ))}
35 </nav>
36 )}
37 </header>
38 )
39}
1// src/types/route-utils.ts
2type RouteWithParams<T extends string> = T extends `${string}:${infer Param}${infer Rest}`
3 ? { [K in Param]: string } & RouteWithParams<Rest>
4 : {}
5
6type ExtractParams<T extends string> = RouteWithParams<T>
7
8// 使用示例
9type UserRouteParams = ExtractParams<'/user/:id/posts/:postId'>
10// 结果: { id: string; postId: string }
1// src/types/routes.ts
2type StaticRoutes = '/' | '/about' | '/contact'
3type UserRoutes = `/user/${string}`
4type AdminRoutes = `/admin/${string}`
5
6type AllRoutes = StaticRoutes | UserRoutes | AdminRoutes
7
8// 类型安全的导航函数
9function navigate<T extends AllRoutes>(path: T): void {
10 // 导航逻辑
11}
12
13// 使用
14navigate('/user/123') // ✅ 正确
15navigate('/invalid') // ❌ TypeScript 错误
1// .vscode/settings.json
2{
3 "typescript.preferences.includePackageJsonAutoImports": "on",
4 "typescript.suggest.autoImports": true,
5 "typescript.updateImportsOnFileMove.enabled": "always",
6 "editor.codeActionsOnSave": {
7 "source.organizeImports": true
8 }
9}
1// .eslintrc.js
2module.exports = {
3 extends: [
4 '@typescript-eslint/recommended',
5 '@typescript-eslint/recommended-requiring-type-checking'
6 ],
7 rules: {
8 '@typescript-eslint/no-unused-vars': 'error',
9 '@typescript-eslint/explicit-function-return-type': 'warn',
10 '@typescript-eslint/no-explicit-any': 'warn',
11 '@typescript-eslint/prefer-nullish-coalescing': 'error',
12 '@typescript-eslint/prefer-optional-chain': 'error'
13 }
14}
参数类型不匹配
1// ❌ 错误
2const { id } = useParams<{ id: number }>()
3
4// ✅ 正确 - 路由参数总是字符串
5const { id } = useParams<{ id: string }>()
6const numericId = parseInt(id)
Loader 数据类型错误
1// ❌ 错误
2const data = useLoaderData() as UserData
3
4// ✅ 正确
5const data = useLoaderData<typeof loader>()
可选参数处理
1// ❌ 错误
2const { category } = useParams<{ category: string }>()
3
4// ✅ 正确 - 可选参数可能为 undefined
5const { category } = useParams<{ category?: string }>()
类型文件未生成
enableGeneration
配置routesTypeFile
路径正确类型不准确