mirror of
https://github.com/patdelphi/suanming.git
synced 2026-02-27 21:23:12 +08:00
24 KiB
24 KiB
开发指南
本文档为开发者提供详细的项目架构说明、开发流程和最佳实践。
目录
项目架构
整体架构
三算命平台
├── 前端应用 (React + TypeScript)
│ ├── 用户界面层
│ ├── 业务逻辑层
│ ├── 数据访问层
│ └── 工具函数层
├── 后端服务 (Supabase)
│ ├── 数据库 (PostgreSQL)
│ ├── 认证服务
│ ├── Edge Functions
│ └── 实时订阅
└── 部署平台 (Vercel/Netlify)
├── CDN 加速
├── 自动部署
└── 环境管理
前端架构
src/
├── components/ # 组件层
│ ├── ui/ # 基础UI组件
│ │ ├── Button.tsx
│ │ ├── Card.tsx
│ │ ├── Input.tsx
│ │ └── Select.tsx
│ ├── Layout.tsx # 布局组件
│ ├── AnalysisResultDisplay.tsx # 业务组件
│ └── ErrorBoundary.tsx # 错误边界
├── pages/ # 页面层
│ ├── HomePage.tsx # 首页
│ ├── AnalysisPage.tsx # 分析页面
│ ├── HistoryPage.tsx # 历史记录
│ └── ProfilePage.tsx # 用户资料
├── contexts/ # 状态管理层
│ └── AuthContext.tsx # 认证上下文
├── hooks/ # 自定义Hook层
│ └── use-mobile.tsx # 移动端检测
├── lib/ # 工具函数层
│ ├── supabase.ts # Supabase客户端
│ └── utils.ts # 通用工具
├── types/ # 类型定义层
│ └── index.ts # TypeScript类型
└── data/ # 静态数据层
数据流架构
用户交互 → 组件状态 → Context/Hook → API调用 → Supabase → 数据库
↓ ↓ ↓ ↓ ↓ ↓
UI更新 ← 状态更新 ← 数据处理 ← 响应处理 ← Edge Function ← 查询结果
技术栈详解
前端核心技术
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: 修复bugdocs: 文档更新style: 代码格式调整refactor: 代码重构test: 测试相关chore: 构建过程或辅助工具的变动
3. Code Review 检查清单
- 代码符合项目规范
- 包含适当的测试
- 文档已更新
- 性能影响已评估
- 安全性已考虑
- 向后兼容性已确认
- UI/UX 符合设计规范
更多开发相关问题,请参考 FAQ 或在 GitHub Issues 中讨论。