编码规范 coding-standards

这是一份针对React 19和TypeScript的详细编码规范文档,专门为量化金融分析工具Portfolio Buddy 2制定。它涵盖了组件结构、状态管理、TypeScript最佳实践、性能优化、错误处理、测试标准和代码组织等多个方面,旨在确保代码质量、可维护性和一致性。关键词包括:React 19, TypeScript, 编码规范, 组件设计, 状态管理, 性能优化, 错误处理, 代码审查, 前端开发, 量化金融工具。

前端开发 0 次安装 0 次浏览 更新于 2/28/2026

name: coding-standards description: “React 19 和 TypeScript 编码规范,适用于 Portfolio Buddy 2。使用场景:编写新组件、代码审查、重构或确保一致性。包含组件模式、TypeScript 规则和最佳实践。”

编码规范 - Portfolio Buddy 2

React 19 模式

组件结构

// 良好:使用 TypeScript 的函数式组件
interface MetricsTableProps {
  data: Metric[]
  onSelect: (id: string) => void
}

export function MetricsTable({ data, onSelect }: MetricsTableProps) {
  // Hooks 放在顶部
  const [selected, setSelected] = useState<Set<string>>(new Set())

  // 使用 useMemo 处理派生状态
  const sortedData = useMemo(() =>
    data.sort((a, b) => b.sharpe - a.sharpe),
    [data]
  )

  // 使用 useCallback 处理事件处理器
  const handleSelect = useCallback((id: string) => {
    setSelected(prev => new Set(prev).add(id))
    onSelect(id)
  }, [onSelect])

  // 渲染
  return <div>...</div>
}

Hooks 规则

  1. 仅在顶层使用 - 不要在条件语句或循环中使用 hooks
  2. 自定义 hooks 以 use 开头 - useMetrics, usePortfolio, useSorting
  3. 依赖数组完整 - useEffect/useMemo/useCallback 中的所有依赖项
  4. 卸载时清理 - 从 useEffect 返回清理函数

状态管理

Portfolio Buddy 2 仅使用纯 React Hooks:

  • 本地 UI 状态useState
  • 派生状态useMemo
  • 稳定的回调函数useCallback
  • DOM/值引用useRef

不使用全局状态库:

  • ❌ 不使用 TanStack Query
  • ❌ 不使用 Zustand
  • ❌ 不使用 Redux
  • ❌ 不使用 Jotai

模式:属性向下传递,自定义 hooks 用于共享逻辑

// 状态管理示例
const [files, setFiles] = useState<File[]>([])
const [dateRange, setDateRange] = useState({ start: null, end: null })

// 派生状态
const filteredData = useMemo(() =>
  filterByDateRange(files, dateRange),
  [files, dateRange]
)

// 稳定的回调
const handleUpload = useCallback((newFile: File) => {
  setFiles(prev => [...prev, newFile])
}, [])

TypeScript 标准

不使用 any 类型

// 错误
const data: any = fetchData()

// 正确
interface TradeData {
  symbol: string
  date: Date
  pnl: number
}
const data: TradeData[] = fetchData()

当前违规(技术债务):

  • usePortfolio.ts: 11 处实例(交易/指标类型)
  • useMetrics.ts: 4 处实例(排序比较)
  • dataUtils.ts: 1 处实例(Metrics 接口)
  • 总计:15 处违规待修复(原为 16 处)

严格的空值检查

// 错误
const value = data.find(x => x.id === id)
value.name // 可能为 undefined!

// 正确
const value = data.find(x => x.id === id)
if (value) {
  value.name // 类型安全
}

// 或使用可选链
const name = data.find(x => x.id === id)?.name

在明显时使用类型推断

// 冗余
const count: number = 5
const name: string = 'Portfolio Buddy'

// 更好(TypeScript 推断)
const count = 5
const name = 'Portfolio Buddy'

// 需要时显式声明
const metrics: Metric[] = [] // 空数组需要类型

组件大小限制

每个组件最多 200 行

当组件超过 200 行时:

  1. 提取子组件
  2. 将逻辑移至自定义 hooks
  3. 提取工具函数到 utils/

当前违规

⚠️ 必须重构:

  • PortfolioSection.tsx: 591 行(限制的 295%)
    • 提取 EquityChartSection
    • 提取 PortfolioStats
    • 提取 ContractControls
    • 仅保留编排逻辑

应该重构:

  • App.tsx: 351 行(限制的 175%)
    • 将各部分提取为组件
  • MetricsTable.tsx: 242 行(限制的 121%)
    • 已从 350 行改进,但仍超限

重构示例

// 之前:PortfolioSection 中的 591 行
function PortfolioSection() {
  // 合约乘数逻辑(50 行)
  // 日期过滤逻辑(40 行)
  // 图表配置(100 行)
  // 统计计算(80 行)
  // 渲染逻辑(300+ 行)
}

// 之后:拆分为专注的部分
function PortfolioSection() {
  const portfolio = usePortfolio(files, dateRange)
  const contracts = useContractMultipliers(portfolio.strategies)

  return (
    <div>
      <ContractControls {...contracts} />
      <EquityChartSection data={portfolio.equity} />
      <PortfolioStats metrics={portfolio.metrics} />
    </div>
  )
}

文件组织

实际目录结构

src/
├── components/
│   └── [AllComponents].tsx    (扁平结构,无子目录)
├── hooks/
│   ├── useContractMultipliers.ts
│   ├── useMetrics.ts
│   ├── usePortfolio.ts
│   └── useSorting.ts
├── utils/
│   └── dataUtils.ts           (指标计算、解析)
├── App.tsx
└── main.tsx

注意:没有 ui/charts/ 子目录 - 组件扁平化放在 components/

命名约定

  • 组件:帕斯卡命名法 - MetricsTable.tsx, CorrelationHeatmap.tsx
  • Hooks:驼峰命名法,以 use 开头 - useMetrics.ts, useSorting.ts
  • 工具函数:驼峰命名法 - calculateMetrics(), parseCSV()
  • 类型/接口:帕斯卡命名法 - interface Metric, type Trade

错误处理

始终处理错误

// 错误
const data = await supabase.storage.upload(file)

// 正确
const { data, error } = await supabase.storage.upload(file)
if (error) {
  console.error('上传失败:', error)
  toast.error('文件上传失败')
  return
}

使用 Try-Catch 进行解析

// 带错误处理的 CSV 解析
try {
  const parsed = parseCSV(file)
  setData(parsed.data)
  if (parsed.errors.length > 0) {
    setErrors(parsed.errors)
  }
} catch (error) {
  console.error('解析错误:', error)
  toast.error('CSV 格式无效')
}

错误边界

当前状态:未实现错误边界(技术债务)

应该添加

<ErrorBoundary fallback={<ErrorMessage />}>
  <PortfolioSection />
</ErrorBoundary>

性能

记忆化

// 昂贵的计算
const metrics = useMemo(
  () => calculateMetrics(portfolioData, riskFreeRate),
  [portfolioData, riskFreeRate]
)

// 大型数据转换
const correlationMatrix = useMemo(
  () => buildCorrelationMatrix(selectedStrategies),
  [selectedStrategies]
)

回调稳定性

// 防止子组件重新渲染
const handleSort = useCallback((column: string) => {
  setSortColumn(column)
  setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc')
}, [])

// 将稳定的回调传递给子组件
<SortableHeader onSort={handleSort} />

避免过早优化

  1. 先构建功能
  2. 如果出现问题则测量性能
  3. 基于性能分析数据进行优化
  4. 没有证据时不进行优化

Chart.js 集成

图表组件模式

import { Line } from 'react-chartjs-2'
import { Chart as ChartJS, registerables } from 'chart.js'
import zoomPlugin from 'chartjs-plugin-zoom'

// 一次性注册插件
ChartJS.register(...registerables, zoomPlugin)

function EquityChart({ data }: { data: EquityData[] }) {
  const chartData = useMemo(() => ({
    labels: data.map(d => d.date),
    datasets: [{
      label: '权益',
      data: data.map(d => d.value),
      borderColor: 'rgb(75, 192, 192)',
    }]
  }), [data])

  const options = useMemo(() => ({
    responsive: true,
    plugins: {
      zoom: { enabled: true }
    }
  }), [])

  return <Line data={chartData} options={options} />
}

图表库

  • 使用:Chart.js + react-chartjs-2
  • 不使用:Recharts(已安装但未使用,应移除)

测试标准

测试内容

  • ✅ 关键计算(夏普比率、索提诺比率、相关性)
  • ✅ 数据转换(CSV 解析、指标计算)
  • ✅ 错误状态和边界情况
  • ✅ Hook 返回值
  • ❌ UI 实现细节(className、DOM 结构)
  • ❌ 第三方库内部实现

测试结构

describe('calculateMetrics', () => {
  it('正确计算夏普比率', () => {
    const trades = mockTradeData()
    const result = calculateMetrics(trades, 0.02)
    expect(result.sharpe).toBeCloseTo(1.5, 2)
  })

  it('优雅处理空数据', () => {
    const result = calculateMetrics([], 0.02)
    expect(result.sharpe).toBe(0)
  })
})

当前状态:未实现测试(未来工作)

导入组织

导入顺序

// 1. React 和外部库
import { useState, useMemo, useCallback } from 'react'
import { Line } from 'react-chartjs-2'

// 2. 内部 hooks
import { useMetrics } from '@/hooks/useMetrics'
import { usePortfolio } from '@/hooks/usePortfolio'

// 3. 工具函数和辅助函数
import { calculateMetrics, formatCurrency } from '@/utils/dataUtils'

// 4. 类型
import type { Metric, Trade } from '@/types'

// 5. 样式(如果有)
import './styles.css'

代码注释

何时注释

// 良好:解释为什么,而不是做什么
// 通过乘以 sqrt(252) 个交易日进行年化
const sharpe = (avgReturn / stdDev) * Math.sqrt(252)

// 错误:代码作用显而易见
// 计算夏普比率
const sharpe = (avgReturn / stdDev) * Math.sqrt(252)

复杂函数的 JSDoc

/**
 * 使用下行偏差计算索提诺比率
 * @param returns - 日收益率数组
 * @param riskFreeRate - 年化无风险利率(例如,0.02 表示 2%)
 * @param targetReturn - 目标收益率阈值(默认:0)
 * @returns 年化索提诺比率
 */
function calculateSortino(
  returns: number[],
  riskFreeRate: number,
  targetReturn = 0
): number {
  // 实现
}

Git 提交消息

格式

<类型>: <主题>

<正文>

类型

  • feat: 新功能
  • fix: 错误修复
  • refactor: 代码重构
  • perf: 性能改进
  • docs: 文档
  • test: 测试添加/更改

最近提交的示例

通过年化下行偏差和修正方差计算来修复索提诺比率计算

重构投资组合计算并增强 Supabase 客户端验证;添加无风险利率输入和索提诺比率计算

增强 Supabase 数据获取中的错误处理和验证;更新 MetricsTable 和 PortfolioSection 以管理 selectedTradeLists 状态

代码审查清单

提交代码前:

  • [ ] TypeScript 严格模式通过(除非记录为技术债务,否则不使用 any
  • [ ] 组件少于 200 行(或有重构计划)
  • [ ] 错误处理到位
  • [ ] 昂贵计算使用记忆化
  • [ ] 使用 useCallback 的稳定回调
  • [ ] 正确的 TypeScript 类型(不使用 any
  • [ ] 按类别组织的导入
  • [ ] 复杂函数有 JSDoc
  • [ ] 已移除 console.logs
  • [ ] 使用 Chart.js(而非 Recharts)