Files
suanming/docs/DEVELOPMENT.md
2025-08-19 14:17:02 +08:00

27 KiB
Raw Blame History

开发指南

本文档为开发者提供详细的项目架构说明、开发流程和最佳实践。

目录

项目架构

整体架构

神机阁平台 (v2.0 重构版)
├── 前端应用 (React + TypeScript)
│   ├── 用户界面层
│   ├── 业务逻辑层 (分析与存储分离)
│   ├── 数据访问层 (API去重机制)
│   └── 工具函数层
├── 后端服务 (Node.js + Express)
│   ├── 数据库 (SQLite)
│   ├── 认证中间件 (JWT)
│   ├── 分析服务 (纯计算)
│   ├── 历史记录服务 (专门存储)
│   └── 路由层
└── 开发环境
    ├── 本地开发服务器
    ├── 热重载
    └── 调试工具

前端架构

src/
├── components/          # 组件层
│   ├── ui/             # 基础UI组件
│   │   ├── Button.tsx
│   │   ├── Card.tsx
│   │   ├── Input.tsx
│   │   └── Select.tsx
│   ├── Layout.tsx      # 布局组件
│   ├── AnalysisResultDisplay.tsx  # 分析结果显示
│   ├── CompleteBaziAnalysis.tsx   # 完整八字分析
│   ├── CompleteZiweiAnalysis.tsx  # 完整紫微分析
│   ├── CompleteYijingAnalysis.tsx # 完整易经分析
│   ├── BaziAnalysisDisplay.tsx    # 八字分析显示
│   ├── ProtectedRoute.tsx         # 路由保护
│   └── ErrorBoundary.tsx          # 错误边界
├── pages/              # 页面层
│   ├── HomePage.tsx    # 首页
│   ├── AnalysisPage.tsx # 分析页面 (重构)
│   ├── HistoryPage.tsx # 历史记录
│   ├── ProfilePage.tsx # 用户资料
│   ├── LoginPage.tsx   # 登录页面
│   ├── RegisterPage.tsx # 注册页面
│   ├── BaziDetailsPage.tsx # 八字详情
│   └── WuxingAnalysisPage.tsx # 五行分析
├── contexts/           # 状态管理层
│   └── AuthContext.tsx # 认证上下文
├── hooks/              # 自定义Hook层
│   └── use-mobile.tsx  # 移动端检测
├── lib/                # 工具函数层
│   ├── localApi.ts     # 本地API客户端 (重构)
│   └── utils.ts        # 通用工具
├── types/              # 类型定义层
│   └── index.ts        # TypeScript类型
└── data/               # 静态数据层

数据流架构 (v2.0 重构版)

分析流程:
用户交互 → 组件状态 → useMemo缓存 → 分析API → 分析服务 → 返回结果
    ↓         ↓          ↓           ↓        ↓         ↓
显示结果 ← 状态更新 ← 对象稳定化 ← 去重处理 ← 纯计算   ← 分析完成
    ↓
历史记录API → 存储服务 → SQLite数据库
    ↓           ↓         ↓
保存成功   ← 记录插入 ← 数据持久化

特点:
- 分析与存储完全分离
- API请求去重机制
- 对象引用稳定化
- 错误隔离处理

架构重构 (v2.0)

重构背景

在v1.0版本中,我们遇到了以下问题:

  1. 重复历史记录:一次分析产生多条历史记录
  2. 架构耦合:分析计算与历史存储紧密耦合
  3. React StrictMode问题开发环境下useEffect重复执行
  4. 对象引用不稳定:每次渲染创建新对象导致重复渲染

重构方案

1. 分离关注点

// 重构前:耦合架构
POST /analysis/bazi  分析 + 存储  返回 { record_id, analysis }

// 重构后:分离架构
POST /analysis/bazi  纯分析  返回 { analysis }
POST /analysis/save-history  专门存储  返回 { record_id }

2. 前端流程优化

// 重构后的分析流程
async function handleAnalysis() {
  // 第一步:获取分析结果
  const analysisResult = await localApi.analysis.bazi(birthData)
  setAnalysisResult(analysisResult.data.analysis)
  
  // 第二步:保存历史记录
  try {
    await localApi.analysis.saveHistory('bazi', analysisResult.data.analysis, birthData)
  } catch (error) {
    // 历史记录保存失败不影响分析结果显示
    console.warn('历史记录保存失败:', error)
  }
}

3. 性能优化措施

// useMemo缓存对象避免重复渲染
const memoizedBirthDate = useMemo(() => ({
  date: formData.birth_date,
  time: formData.birth_time,
  name: formData.name,
  gender: formData.gender
}), [formData.birth_date, formData.birth_time, formData.name, formData.gender])

// useEffect依赖优化
useEffect(() => {
  // 依赖具体字段而非整个对象
}, [birthDate?.date, birthDate?.time, birthDate?.name, birthDate?.gender])

// API请求去重
private pendingRequests: Map<string, Promise<any>> = new Map()

4. 错误处理改进

// 容错机制:历史记录保存失败不影响分析功能
try {
  await saveHistory()
} catch (historyError) {
  console.error('保存历史记录失败:', historyError)
  // 不抛出错误,不影响用户体验
}

重构效果

指标 重构前 重构后 改善幅度
重复记录数 3-5条/次 1条/次 减少80%+
API调用次数 多次重复 单次调用 减少60%+
组件渲染次数 频繁重渲染 按需渲染 减少40%+
代码可维护性 耦合严重 职责清晰 显著提升

技术栈详解

前端核心技术

React 18.3.1

  • 并发特性: 使用 Suspense 和 lazy loading
  • Hooks: 优先使用函数组件和 Hooks
  • 错误边界: 实现全局错误处理
// 示例:使用 Suspense 进行代码分割
import { Suspense, lazy } from 'react'

const AnalysisPage = lazy(() => import('./pages/AnalysisPage'))

function App() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <AnalysisPage />
    </Suspense>
  )
}

TypeScript

  • 严格模式: 启用所有严格类型检查
  • 类型定义: 为所有 API 响应定义类型
  • 泛型使用: 提高代码复用性
// 示例API 响应类型定义
interface BaziAnalysisResult {
  bazi: {
    year: string
    month: string
    day: string
    hour: string
  }
  wuxing: {
    wood: number
    fire: number
    earth: number
    metal: number
    water: number
  }
  analysis: {
    character: string
    career: string
    wealth: string
    health: string
    relationships: string
  }
}

Tailwind CSS

  • 实用优先: 使用原子化 CSS 类
  • 响应式设计: 移动端优先的设计方法
  • 自定义主题: 中国风配色和字体
// tailwind.config.js 自定义配置
module.exports = {
  theme: {
    extend: {
      colors: {
        'chinese-red': '#DC143C',
        'chinese-gold': '#FFD700',
        'chinese-black': '#2C2C2C'
      },
      fontFamily: {
        'chinese': ['Noto Sans SC', 'sans-serif']
      }
    }
  }
}

后端服务架构

Supabase

  • 数据库: PostgreSQL 关系型数据库
  • 认证: JWT 基础的用户认证
  • 实时: WebSocket 实时数据同步
  • Edge Functions: Deno 运行时的服务端函数
// Supabase 客户端配置
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY

export const supabase = createClient(supabaseUrl, supabaseKey, {
  auth: {
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: true
  }
})

开发环境设置

1. 环境要求

# 检查 Node.js 版本
node --version  # >= 18.0.0

# 检查 pnpm 版本
pnpm --version  # >= 8.0.0

# 检查 Git 版本
git --version   # >= 2.0.0

2. 项目初始化

# 克隆项目
git clone https://github.com/patdelphi/suanming.git
cd suanming

# 安装依赖
pnpm install

# 复制环境变量模板
cp .env.example .env.local

# 编辑环境变量
vim .env.local

3. 开发服务器

# 启动开发服务器
pnpm dev

# 启动并打开浏览器
pnpm dev --open

# 指定端口
pnpm dev --port 3000

4. 开发工具配置

VS Code 推荐扩展

// .vscode/extensions.json
{
  "recommendations": [
    "bradlc.vscode-tailwindcss",
    "esbenp.prettier-vscode",
    "dbaeumer.vscode-eslint",
    "ms-vscode.vscode-typescript-next",
    "formulahendry.auto-rename-tag",
    "christian-kohler.path-intellisense"
  ]
}

VS Code 设置

// .vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "typescript.preferences.importModuleSpecifier": "relative",
  "tailwindCSS.experimental.classRegex": [
    ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
    ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"],
    ["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
  ]
}

代码规范

ESLint 配置

// eslint.config.js
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'

export default tseslint.config(
  { ignores: ['dist'] },
  {
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
    files: ['**/*.{ts,tsx}'],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
    plugins: {
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh,
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      'react-refresh/only-export-components': [
        'warn',
        { allowConstantExport: true },
      ],
      '@typescript-eslint/no-unused-vars': 'warn',
      '@typescript-eslint/no-explicit-any': 'warn',
    },
  },
)

命名规范

// 组件命名PascalCase
const AnalysisResultDisplay: React.FC = () => {}

// Hook 命名camelCase以 use 开头
const useAuth = () => {}

// 常量命名SCREAMING_SNAKE_CASE
const API_BASE_URL = 'https://api.example.com'

// 类型命名PascalCase接口以 I 开头(可选)
interface UserProfile {
  id: string
  name: string
}

// 枚举命名PascalCase
enum AnalysisType {
  BAZI = 'bazi',
  ZIWEI = 'ziwei',
  YIJING = 'yijing'
}

文件组织规范

// 导入顺序
// 1. React 相关
import React, { useState, useEffect } from 'react'

// 2. 第三方库
import { useNavigate } from 'react-router-dom'
import { toast } from 'sonner'

// 3. 内部组件
import { Button } from '../ui/Button'
import { Card } from '../ui/Card'

// 4. 内部工具
import { supabase } from '../../lib/supabase'
import { cn } from '../../lib/utils'

// 5. 类型定义
import type { AnalysisResult } from '../../types'

组件开发

组件结构模板

// components/ExampleComponent.tsx
import React from 'react'
import { cn } from '../../lib/utils'

// 组件属性接口
interface ExampleComponentProps {
  title: string
  description?: string
  variant?: 'default' | 'primary' | 'secondary'
  className?: string
  children?: React.ReactNode
  onClick?: () => void
}

// 组件实现
const ExampleComponent: React.FC<ExampleComponentProps> = ({
  title,
  description,
  variant = 'default',
  className,
  children,
  onClick
}) => {
  return (
    <div 
      className={cn(
        'base-styles',
        {
          'variant-default': variant === 'default',
          'variant-primary': variant === 'primary',
          'variant-secondary': variant === 'secondary'
        },
        className
      )}
      onClick={onClick}
    >
      <h3>{title}</h3>
      {description && <p>{description}</p>}
      {children}
    </div>
  )
}

export default ExampleComponent

UI 组件开发

使用 class-variance-authority 创建可变样式组件:

// components/ui/Button.tsx
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '../../lib/utils'

const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
        outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
        secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
        ghost: 'hover:bg-accent hover:text-accent-foreground',
        link: 'underline-offset-4 hover:underline text-primary'
      },
      size: {
        default: 'h-10 py-2 px-4',
        sm: 'h-9 px-3 rounded-md',
        lg: 'h-11 px-8 rounded-md'
      }
    },
    defaultVariants: {
      variant: 'default',
      size: 'default'
    }
  }
)

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    return (
      <button
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = 'Button'

export { Button, buttonVariants }

状态管理

Context 模式

// contexts/AuthContext.tsx
import React, { createContext, useContext, useEffect, useState } from 'react'
import { User, Session } from '@supabase/supabase-js'
import { supabase } from '../lib/supabase'

interface AuthContextType {
  user: User | null
  session: Session | null
  loading: boolean
  signIn: (email: string, password: string) => Promise<void>
  signUp: (email: string, password: string) => Promise<void>
  signOut: () => Promise<void>
}

const AuthContext = createContext<AuthContextType | undefined>(undefined)

export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [user, setUser] = useState<User | null>(null)
  const [session, setSession] = useState<Session | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    // 获取初始会话
    supabase.auth.getSession().then(({ data: { session } }) => {
      setSession(session)
      setUser(session?.user ?? null)
      setLoading(false)
    })

    // 监听认证状态变化
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      async (event, session) => {
        setSession(session)
        setUser(session?.user ?? null)
        setLoading(false)
      }
    )

    return () => subscription.unsubscribe()
  }, [])

  const signIn = async (email: string, password: string) => {
    const { error } = await supabase.auth.signInWithPassword({ email, password })
    if (error) throw error
  }

  const signUp = async (email: string, password: string) => {
    const { error } = await supabase.auth.signUp({ email, password })
    if (error) throw error
  }

  const signOut = async () => {
    const { error } = await supabase.auth.signOut()
    if (error) throw error
  }

  const value = {
    user,
    session,
    loading,
    signIn,
    signUp,
    signOut
  }

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  )
}

export const useAuth = () => {
  const context = useContext(AuthContext)
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider')
  }
  return context
}

自定义 Hooks

// hooks/useAnalysis.ts
import { useState, useCallback } from 'react'
import { supabase } from '../lib/supabase'
import { toast } from 'sonner'

interface AnalysisParams {
  type: 'bazi' | 'ziwei' | 'yijing'
  data: any
}

export const useAnalysis = () => {
  const [loading, setLoading] = useState(false)
  const [result, setResult] = useState(null)
  const [error, setError] = useState<string | null>(null)

  const analyze = useCallback(async ({ type, data }: AnalysisParams) => {
    setLoading(true)
    setError(null)
    
    try {
      const { data: result, error } = await supabase.functions.invoke(
        `${type}-analyzer`,
        { body: data }
      )
      
      if (error) throw error
      
      setResult(result)
      toast.success('分析完成')
    } catch (err) {
      const message = err instanceof Error ? err.message : '分析失败'
      setError(message)
      toast.error(message)
    } finally {
      setLoading(false)
    }
  }, [])

  return { analyze, loading, result, error }
}

API 集成

Supabase 客户端封装

// lib/api.ts
import { supabase } from './supabase'
import type { AnalysisResult, UserProfile } from '../types'

export class ApiClient {
  // 用户相关
  static async getUserProfile(userId: string): Promise<UserProfile | null> {
    const { data, error } = await supabase
      .from('user_profiles')
      .select('*')
      .eq('id', userId)
      .single()
    
    if (error) throw error
    return data
  }

  static async updateUserProfile(userId: string, profile: Partial<UserProfile>) {
    const { data, error } = await supabase
      .from('user_profiles')
      .update(profile)
      .eq('id', userId)
      .select()
      .single()
    
    if (error) throw error
    return data
  }

  // 分析相关
  static async getAnalysisHistory(userId: string, type?: string) {
    let query = supabase
      .from('numerology_readings')
      .select('*')
      .eq('user_id', userId)
      .order('created_at', { ascending: false })
    
    if (type) {
      query = query.eq('reading_type', type)
    }
    
    const { data, error } = await query
    if (error) throw error
    return data
  }

  static async performAnalysis(type: string, data: any): Promise<AnalysisResult> {
    const { data: result, error } = await supabase.functions.invoke(
      `${type}-analyzer`,
      { body: data }
    )
    
    if (error) throw error
    return result
  }

  // 实时订阅
  static subscribeToAnalysisUpdates(userId: string, callback: (payload: any) => void) {
    return supabase
      .channel('analysis-updates')
      .on(
        'postgres_changes',
        {
          event: '*',
          schema: 'public',
          table: 'numerology_readings',
          filter: `user_id=eq.${userId}`
        },
        callback
      )
      .subscribe()
  }
}

错误处理

// lib/error-handler.ts
import { toast } from 'sonner'

export class ApiError extends Error {
  constructor(
    message: string,
    public code?: string,
    public status?: number
  ) {
    super(message)
    this.name = 'ApiError'
  }
}

export const handleApiError = (error: unknown) => {
  console.error('API Error:', error)
  
  if (error instanceof ApiError) {
    toast.error(error.message)
    return error
  }
  
  if (error instanceof Error) {
    toast.error(error.message)
    return new ApiError(error.message)
  }
  
  const fallbackError = new ApiError('未知错误')
  toast.error(fallbackError.message)
  return fallbackError
}

// 使用示例
try {
  await ApiClient.performAnalysis('bazi', data)
} catch (error) {
  handleApiError(error)
}

测试策略

单元测试

// __tests__/utils.test.ts
import { describe, it, expect } from 'vitest'
import { cn, formatDate } from '../lib/utils'

describe('Utils', () => {
  describe('cn', () => {
    it('should merge class names correctly', () => {
      expect(cn('base', 'additional')).toBe('base additional')
      expect(cn('base', { 'conditional': true })).toBe('base conditional')
      expect(cn('base', { 'conditional': false })).toBe('base')
    })
  })

  describe('formatDate', () => {
    it('should format date correctly', () => {
      const date = new Date('2024-01-01')
      expect(formatDate(date)).toBe('2024年1月1日')
    })
  })
})

组件测试

// __tests__/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import { Button } from '../components/ui/Button'

describe('Button', () => {
  it('should render correctly', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByRole('button')).toBeInTheDocument()
    expect(screen.getByText('Click me')).toBeInTheDocument()
  })

  it('should handle click events', () => {
    const handleClick = vi.fn()
    render(<Button onClick={handleClick}>Click me</Button>)
    
    fireEvent.click(screen.getByRole('button'))
    expect(handleClick).toHaveBeenCalledTimes(1)
  })

  it('should apply variant styles', () => {
    render(<Button variant="destructive">Delete</Button>)
    const button = screen.getByRole('button')
    expect(button).toHaveClass('bg-destructive')
  })
})

E2E 测试

// e2e/auth.spec.ts
import { test, expect } from '@playwright/test'

test.describe('Authentication', () => {
  test('should allow user to sign in', async ({ page }) => {
    await page.goto('/login')
    
    await page.fill('[data-testid="email"]', 'test@example.com')
    await page.fill('[data-testid="password"]', 'password123')
    await page.click('[data-testid="sign-in-button"]')
    
    await expect(page).toHaveURL('/dashboard')
    await expect(page.locator('[data-testid="user-menu"]')).toBeVisible()
  })

  test('should show error for invalid credentials', async ({ page }) => {
    await page.goto('/login')
    
    await page.fill('[data-testid="email"]', 'invalid@example.com')
    await page.fill('[data-testid="password"]', 'wrongpassword')
    await page.click('[data-testid="sign-in-button"]')
    
    await expect(page.locator('[data-testid="error-message"]')).toBeVisible()
  })
})

性能优化

代码分割

// 路由级别的代码分割
import { lazy, Suspense } from 'react'
import { Routes, Route } from 'react-router-dom'

const HomePage = lazy(() => import('./pages/HomePage'))
const AnalysisPage = lazy(() => import('./pages/AnalysisPage'))
const HistoryPage = lazy(() => import('./pages/HistoryPage'))

function App() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/analysis" element={<AnalysisPage />} />
        <Route path="/history" element={<HistoryPage />} />
      </Routes>
    </Suspense>
  )
}

组件优化

// 使用 React.memo 优化组件渲染
import React, { memo, useMemo, useCallback } from 'react'

interface ExpensiveComponentProps {
  data: any[]
  onItemClick: (id: string) => void
}

const ExpensiveComponent = memo<ExpensiveComponentProps>(({ data, onItemClick }) => {
  // 使用 useMemo 缓存计算结果
  const processedData = useMemo(() => {
    return data.map(item => ({
      ...item,
      processed: expensiveCalculation(item)
    }))
  }, [data])

  // 使用 useCallback 缓存事件处理函数
  const handleClick = useCallback((id: string) => {
    onItemClick(id)
  }, [onItemClick])

  return (
    <div>
      {processedData.map(item => (
        <div key={item.id} onClick={() => handleClick(item.id)}>
          {item.processed}
        </div>
      ))}
    </div>
  )
})

function expensiveCalculation(item: any) {
  // 模拟昂贵的计算
  return item.value * Math.random()
}

图片优化

// 图片懒加载组件
import React, { useState, useRef, useEffect } from 'react'

interface LazyImageProps {
  src: string
  alt: string
  className?: string
  placeholder?: string
}

const LazyImage: React.FC<LazyImageProps> = ({ 
  src, 
  alt, 
  className, 
  placeholder = '/placeholder.jpg' 
}) => {
  const [isLoaded, setIsLoaded] = useState(false)
  const [isInView, setIsInView] = useState(false)
  const imgRef = useRef<HTMLImageElement>(null)

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsInView(true)
          observer.disconnect()
        }
      },
      { threshold: 0.1 }
    )

    if (imgRef.current) {
      observer.observe(imgRef.current)
    }

    return () => observer.disconnect()
  }, [])

  return (
    <img
      ref={imgRef}
      src={isInView ? src : placeholder}
      alt={alt}
      className={className}
      onLoad={() => setIsLoaded(true)}
      style={{
        opacity: isLoaded ? 1 : 0.5,
        transition: 'opacity 0.3s ease'
      }}
    />
  )
}

调试技巧

React DevTools

// 在开发环境中启用 React DevTools
if (import.meta.env.DEV) {
  // 为组件添加显示名称
  Component.displayName = 'ComponentName'
  
  // 添加调试信息
  console.log('Component rendered with props:', props)
}

错误边界

// components/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react'

interface Props {
  children: ReactNode
  fallback?: ReactNode
}

interface State {
  hasError: boolean
  error?: Error
}

export class ErrorBoundary extends Component<Props, State> {
  public state: State = {
    hasError: false
  }

  public static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error }
  }

  public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('ErrorBoundary caught an error:', error, errorInfo)
    
    // 发送错误报告到监控服务
    if (import.meta.env.PROD) {
      // Sentry.captureException(error, { extra: errorInfo })
    }
  }

  public render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div className="error-boundary">
          <h2>出现了一些问题</h2>
          <p>请刷新页面重试</p>
          {import.meta.env.DEV && (
            <details>
              <summary>错误详情</summary>
              <pre>{this.state.error?.stack}</pre>
            </details>
          )}
        </div>
      )
    }

    return this.props.children
  }
}

性能监控

// lib/performance.ts
export const measurePerformance = (name: string, fn: () => void) => {
  if (import.meta.env.DEV) {
    const start = performance.now()
    fn()
    const end = performance.now()
    console.log(`${name} took ${end - start} milliseconds`)
  } else {
    fn()
  }
}

// 使用示例
measurePerformance('Data Processing', () => {
  // 执行数据处理逻辑
  processLargeDataSet(data)
})

贡献流程

1. 开发流程

# 1. 创建功能分支
git checkout -b feature/new-feature

# 2. 开发和测试
npm run dev
npm run test
npm run lint

# 3. 提交代码
git add .
git commit -m "feat: add new feature"

# 4. 推送分支
git push origin feature/new-feature

# 5. 创建 Pull Request

2. 提交信息规范

type(scope): description

[optional body]

[optional footer]

类型说明:

  • feat: 新功能
  • fix: 修复bug
  • docs: 文档更新
  • style: 代码格式调整
  • refactor: 代码重构
  • test: 测试相关
  • chore: 构建过程或辅助工具的变动

3. Code Review 检查清单

  • 代码符合项目规范
  • 包含适当的测试
  • 文档已更新
  • 性能影响已评估
  • 安全性已考虑
  • 向后兼容性已确认
  • UI/UX 符合设计规范

更多开发相关问题,请参考 FAQ 或在 GitHub Issues 中讨论。