基于 Ponytail 理念 核心理念:写必要的代码,不写多余的代码
在写代码之前,停在第一层满足的地方:
1. 这个需要存在吗? → 否:跳过
2. 标准库能做吗? → 是:用它
3. 平台原生特性? → 是:用它
4. 已安装的依赖? → 是:用它
5. 一行能搞定? → 一行搞定
6. 最后才写:最小可用的代码
- 信任边界验证
- 数据丢失处理
- 安全性
- 可访问性
// ❌ 过度工程
import { useState, useEffect } from 'react'
const [count, setCount] = useState(0)
useEffect(() => {
const interval = setInterval(() => setCount(c => c + 1), 1000)
return () => clearInterval(interval)
}, [])
// ✅ Ponytail 风格
<progress value={progress} max={100} />// ❌ 引入日期库
import dayjs from 'dayjs'
const formatted = dayjs(date).format('YYYY-MM-DD')
// ✅ 标准库
const formatted = new Date(date).toISOString().split('T')[0]// ❌ 过度拆分
const getStatusColor = (status: string) => {
switch (status) {
case 'success': return 'green'
case 'failed': return 'red'
case 'pending': return 'yellow'
default: return 'gray'
}
}
// ✅ 一行搞定
const getStatusColor = (s: string) => ({ success: 'green', failed: 'red', pending: 'yellow' }[s] ?? 'gray')// ❌ 引入新依赖
import { useLocalStorage } from 'usehooks-ts'
const [value, setValue] = useLocalStorage('key', defaultValue)
// ✅ 用已有的
// packages/web/src/hooks/useLocalStorage.ts 已有,直接用
import { useLocalStorage } from '@/hooks/useLocalStorage'// ❌ 过度组件
<Button onClick={handleClick} variant="primary">
提交
</Button>
// ✅ Ponytail 风格
<button className="btn-primary" onClick={handleClick}>
提交
</button>拆分条件(满足任一):
- 超过 50 行
- 有独立的测试用例
- 被复用 3 次以上
- 有独立的业务逻辑
不拆分:
- 只是为了"看起来干净"
- 没有复用的单页组件
- 过度抽象的配置对象
// ❌ 过度使用 useState/useReducer
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [data, setData] = useState<Data | null>(null)
// ✅ 用 TanStack Query,内置 loading/error/data
const { data, isLoading, error } = useQuery({
queryKey: ['sources'],
queryFn: fetchSources,
})// ❌ REST 过度设计
GET /api/v1/sources/{id}/config/tags
POST /api/v1/sources/{id}/config/tags/batch
DELETE /api/v1/sources/{id}/config/tags/{tagId}
// ✅ Ponytail 风格
GET /api/v1/sources/:id/tags
POST /api/v1/sources/:id/tags
DELETE /api/v1/sources/:id/tags/:tagId// ❌ 过度包装
{
"success": true,
"data": { ... },
"meta": { ... },
"links": { ... },
"error": null
}
// ✅ 只在需要时包装
// 单条数据直接返回
{ "id": "123", "name": "..." }
// 列表才加 meta
{ "data": [...], "meta": { "total": 100 } }// ❌ 过度嵌套
src/features/sources/components/forms/inputs/NameInput.tsx
// ✅ Ponytail 风格
src/components/features/NameInput.tsx
- 单个文件不超过 200 行
- 超过 200 行才拆分
- 拆分时按功能分,不按类型分
// ❌ 按类型分
components/
├── Button.tsx
├── Input.tsx
├── Modal.tsx
// ✅ Ponytail 风格:按功能分
components/features/
├── SourceCard.tsx // 包含 Button, Input 等
├── SourceForm.tsx // 包含表单逻辑必须测试:
- 业务逻辑
- API 路由
- 数据转换
- 错误处理
不测试:
- React 组件(E2E 覆盖)
- 简单的 getter/setter
- 第三方库封装
// ❌ 测试过度
test('SourceForm 渲染正确的 Input 数量', () => {
render(<SourceForm />)
expect(screen.getAllByRole('textbox')).toHaveLength(5)
})
// ✅ Ponytail 风格
test('SourceForm 提交正确数据', async () => {
const onSubmit = vi.fn()
render(<SourceForm onSubmit={onSubmit} />)
await userEvent.click(screen.getByRole('button', { name: /submit/i }))
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
name: expect.any(String)
}))
})# ❌ 冗长
git commit -m "feat: add functionality to handle user data processing and validation"
# ✅ Ponytail 风格
git commit -m "feat(sources): add validation"| 类型 | 用途 |
|---|---|
feat |
新功能 |
fix |
Bug 修复 |
chore |
杂项(依赖更新等) |
docs |
文档 |
refactor |
重构 |
test |
测试 |
- 每行不超过 72 字符
- 不写 "and"
- 动词开头
# 添加前问:
1. 标准库能做吗?
2. 已有的依赖能做吗?
3. 这个真的需要吗?| 问题 | 答案 | 动作 |
|---|---|---|
| 标准库能做吗? | 是 | 不添加 |
| 已安装的依赖能做吗? | 是 | 不添加 |
| 没有这个库功能能用吗? | 否 | 确认后添加 |
| 功能会被复用吗? | 是 | 添加 |
- 这行代码需要吗?
- 标准库能做吗?
- 平台原生特性有吗?
- 一行能搞定吗?
- 超过 50 行了吗?(考虑拆分)
- 输入验证了吗?
- 错误处理了吗?
- 敏感信息泄露了吗?
- 有 N+1 查询吗?
- 有不必要的重渲染吗?
- 有内存泄漏风险吗?
// 不要:创建工厂类来创建简单的对象
class SourceFactory {
static create(config: SourceConfig): Source {
return new Source(config)
}
}
// 不要:为简单的对象创建接口
interface ISource {
id: string
name: string
type: SourceType
}
// ✅ 直接写
const source: Source = { id, name, type }// 不要:抽象出 BaseService
class BaseService<T> {
async findAll(): Promise<T[]>
async findById(id: string): Promise<T | null>
async create(data: Partial<T>): Promise<T>
async update(id: string, data: Partial<T>): Promise<T>
async delete(id: string): Promise<void>
}
// ✅ Ponytail:直接写需要的方法
async function getSources(): Promise<Source[]> { ... }
async function createSource(data: SourceInput): Promise<Source> { ... }// 不要:给所有东西都加类型
type SourceDTO = {
readonly id: string
readonly name: string
readonly type: SourceType
readonly createdAt: Date
readonly updatedAt: Date
readonly config: Readonly<SourceConfig>
}
// ✅ Ponytail:类型推断够用时不用显式标注
const source = { id, name, type } // TypeScript 推断