编码规范Skill coding-standards

这是一份针对 Portfolio Buddy 2 项目的 React 19 和 TypeScript 编码规范文档。它详细定义了前端组件开发的最佳实践,包括组件结构、状态管理、TypeScript 类型安全、性能优化、错误处理、文件组织、测试标准和代码审查流程。适用于前端开发团队进行代码编写、审查和重构,确保项目代码质量和一致性。关键词:React 19, TypeScript, 前端开发, 编码规范, 组件设计, 状态管理, 性能优化, 代码审查。

前端开发 0 次安装 2 次浏览 更新于 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)