Claude Code × Spec Kit 实战:五步构建番茄钟待办 Web 应用
- Published on
- /35 mins read/---
Spec Kit 介绍
什么是 Spec Kit
Spec Kit 是 GitHub 开源的开发工具包(https://github.com/github/spec-kit),基于**规范驱动开发(Spec-Driven Development)**的理念,帮助开发团队更快地构建高质量软件。
它至今拿了 142,947 Stars,是 GitHub 上增长最快的开发流程类项目。从 2025-08-21 创建至今不到 15 个月,版本已经迭代到 1.2.3(2026-10-22 发布),并且从单一的 SDD 流程扩展成了包含 Bug 修复、想法评估、扩展、预设、Bundle 的完整工具箱。
核心理念:规范驱动开发
传统软件开发流程:
编写规范 → 编写代码 → 测试 → 部署
规范驱动开发则完全颠覆了这一模式:
- 规范成为核心:规范不再是临时性的文档,而是可执行的
- 自动生成代码:规范直接生成可工作的实现代码
- 规范即代码:规范本身就是开发的一部分,而不是前期准备工作
核心特点
01 提升开发效率
- 让组织专注于产品场景,而不是重复编写基础代码
- 减少编写"无差别代码"的时间投入
- 通过标准化流程提高团队协作效率
02 完整的工具生态
- Specify CLI:命令行工具(
uv tool install specify-cli),用于执行规范驱动开发流程 - AI 代理支持:集成 50+ 种 AI 助手,Claude Code 是一等公民
- 文档和最佳实践:提供完整的使用指南和模版
03 解决传统开发痛点
- 规范写完就被丢弃
- 代码与文档脱节
- 重复编写相似的基础代码
- 开发效率低下
Spec Kit 环境搭建详解
下载与安装
现在推荐用 uv 安装(不再需要手动 clone + venv):
uv tool install specify-cli需要 Python 3.11+、uv,以及一个支持的 AI 编码代理(Linux / macOS / Windows 均可)。
初始化项目
specify init focus-todo --integration claude --ai-skills
cd focus-todo三个参数值得解释:
--integration claude:装 Claude Code 集成。会生成.claude/skills/speckit-*/SKILL.md,把每个命令变成一个 skill--ai-skills:让 Claude 用 skills 模式而不是纯 prompt 模式。没有这个参数时,Claude 属于「条件斜杠代理」,命令会以 prompt 形式而非 skill 形式呈现- 项目名会自动创建对应子目录
和旧版的差别:2025 年那套是
git clone+pip3 install -e .+specify init <name>(交互式选 agent)。现在specify已发布为 PyPI 包,agent 选择改成了--integration参数,claude是合法的 integration key。
实战案例:构建番茄钟待办 Web 应用
为什么选一个最简单的案例
第一次接触规范驱动开发,很容易走偏:挑最难的那个业务来做。结果不是被规范难住,而是被环境、依赖、算力、硬件难住,最后得出"规范驱动开发不适合我"的结论。
所以这个案例刻意选了相反的一端:一个纯前端的番茄钟 + 待办清单,没有后端、没有模型、没有 GPU、没有硬件依赖,pnpm install 之后立刻就能跑起来。
选案例的标准只有三条:
- 五分钟能启动 —— 读者能真正跟完整个流程,不会卡在环境上
- 边界条件可穷举 —— 有真实的坑值得写进 spec,而不是靠规模取胜
- 结果可当场验证 —— 打开浏览器就能判断做对没做对
这不代表简单项目不需要规范。恰恰相反,番茄钟这种"看起来没有技术含量"的玩具,藏着一组非常典型的坑:页面刷新后剩余时间跳变、系统休眠后计时漂移、隐私模式下 localStorage 直接抛异常、两个标签页同时开着互相覆盖。这些坑一个都不会因为你项目小就消失,而它们恰好是规范驱动开发收益最大的地方。
项目背景
本案例用 Vite + React + TypeScript + Tailwind CSS 构建一个纯前端应用:
- 待办清单:新建、完成、编辑、删除,支持预估番茄数
- 番茄钟:专注 / 短休 / 长休三阶段,长休每 4 个番茄出现一次
- 任务联动:开始一个番茄时绑定当前任务,番茄正常结束自动累加到该任务
- 统计:今日完成番茄数与专注时长
- 持久化:localStorage,刷新不丢
- 备份:导出 / 导入 JSON,换设备可迁移
选它的原因:这是一个状态机清晰、时间逻辑可测、边界条件真实存在的小应用。所有"会出错的算术"都集中在计时逻辑里,而这恰恰是纯函数 + 单元测试最擅长覆盖的地方 —— 换句话说,Claude 写出来的第一版几乎必然是错的,规范和测试存在的意义就是把它掰回来。
Spec Kit 五大核心命令
/speckit-constitution/speckit-specify/speckit-plan/speckit-tasks/speckit-implement
语法变化:旧版是
/speckit.constitution(点号),1.x 改成了/speckit-constitution(连字符),并新增了/speckit-converge作为收敛检查环节。
进入项目环境
用 Claude Code 打开项目目录。这个案例没有现成的开源项目可以借用,但 Claude Code 原生支持 CLAUDE.md —— 我们在 T004 里专门写了一份,内容包括技术栈、目录约定、常用脚本,以及最关键的一条硬性规则:
## 硬性规则
- 所有与时间相关的计算必须放在 `src/domain/timer.ts` 的纯函数里;
组件中禁止出现 `Date.now()` 参与算术,只允许读取纯函数的返回值。这正是 Claude Code 相对普通 IDE 助手的关键差异:项目级的 CLAUDE.md 会被自动加载,配合 Spec Kit 生成的 constitution.md,形成两层约束。规则写在文件里,而不是每次在 prompt 里重复粘贴。
Spec Kit 五大核心命令详解
制定项目宪章 - /speckit-constitution
目的:建立项目的核心治理原则和开发标准。
在 Claude Code 中的执行方式:直接输入
/speckit-constitution 制定以代码质量、测试标准、时间与状态正确性、性能预算与数据可迁移性为核心的原则
输出成果:生成 constitution.md 文件。针对这个项目,我会把宪章写成这样:
# Focus-Todo Constitution
## Core Principles
### I. 代码质量(不可妥协)
- MUST 通过 ESLint + Prettier + `tsc --noEmit`,三者任一失败则禁止合并。
- MUST 开启 TypeScript `strict`,禁止使用 `any` 逃逸类型检查。
- MUST 公共组件与领域函数具备类型签名与最小示例;破坏性变更需变更记录。
### II. 测试标准与覆盖率
- MUST 测试优先(Red-Green-Refactor):先写测试、确保失败,再实现。
- MUST `src/domain/` 与 `src/storage/` 行覆盖率 ≥ 90%,全局 ≥ 80%。
- MUST CI 中执行测试并作为合并门禁。
- MUST 每个 Edge Case 至少对应一条测试用例。
### III. 时间与状态正确性(本项目特有)
- MUST 剩余时间以绝对结束时间戳计算,禁止用定时器累减。
- MUST 计时状态只能是有限状态机的合法状态,禁止出现非法中间态。
- MUST 时钟回拨、页面休眠、跨天等场景的剩余时间不得为负、不得凭空增加。
### IV. 性能预算
- MUST 在 plan.md 中声明可衡量目标(首屏可交互 < 1s;gzip < 150 kB)。
- MUST 计时每秒只更新计时组件,不得触发任务列表整体重渲染。
- MUST 提供构建产物体积检查并纳入 CI。
### V. 数据可迁移性
- MUST 持久化数据带 `schemaVersion`,读取时按版本迁移,不得直接崩溃。
- MUST 导入备份先校验再写入,校验失败必须保留原有数据。
- MUST 导出文件为人类可读 JSON,便于手工排查。
## 质量门禁与度量
- 门禁项:Lint / Typecheck 全通过;测试通过且覆盖率达标;构建产物体积不超标;
**Edge Case 清单逐条有对应测试**。
- 失败则禁止合并;豁免需在 PR 描述记录、关联后续任务并标注影响范围与期限。对比原案例:原文的宪章是通用 Web 开发视角(代码质量 / 测试 / UX / 性能)。换成这个案例后,第三条整个换掉了 —— 时间与状态正确性成为不可妥协项,因为一个番茄钟哪怕只是"多算了 3 秒",用户也会立刻失去信任。这类领域规则一旦不写进宪章,Claude 生成的代码里就不会有对应的检查。
需求规格说明 - /speckit-specify
目的:描述要构建什么,专注于 what 和 why,而非技术实现。
/speckit-specify 我要做一个番茄钟 + 待办清单 Web 应用(纯前端):待办支持新建、
完成、编辑、删除和预估番茄数;番茄钟分专注、短休、长休三个阶段;番茄结束后把
完成数累加到当前绑定的任务;统计今日番茄数与专注时长;数据存 localStorage;
支持导出/导入 JSON 备份。刷新页面不能丢状态,时间不能漂移。
输出成果:生成 001-focus-todo 目录,含 spec.md:
# Feature Specification: Focus Todo 番茄钟 + 待办
**Feature Branch**: `001-focus-todo`
**Status**: Draft
## User Scenarios & Testing
### User Story 1 - 管理任务清单 (Priority: P1)
用户新建待办、设定预估番茄数、标记完成,并在开始计时前挑选当前任务。
**Why this priority**: 没有任务清单,番茄钟只是一个计时器,这是应用的主线。
**Independent Test**: 仅实现任务的增删改查与持久化,即可独立完成"建任务—刷新—仍在"。
**Acceptance Scenarios**:
1. Given 输入框有内容,When 点击新增,Then 列表出现该任务,输入框清空且焦点保留。
2. Given 任务未完成,When 勾选完成,Then 任务置为完成态并沉底,但不从列表消失。
3. Given 存在未完成任务,When 刷新页面,Then 任务列表与完成状态保持不变。
4. Given 任务标题为空或仅空格,When 提交,Then 拒绝创建并给出提示。
### User Story 2 - 运行番茄钟并与任务联动 (Priority: P1)
用户启动一个专注番茄,倒计时结束后自动进入休息,并把完成的番茄记到当前任务上。
**Why this priority**: 这是应用存在的核心理由,也是计时正确性的主要风险点。
**Independent Test**: 仅实现计时与状态恢复,即可独立完成"开始—刷新—剩余时间不变"。
**Acceptance Scenarios**:
1. Given 处于空闲状态,When 点击开始,Then 计时器显示剩余时间并按秒更新。
2. Given 专注进行中,When 刷新页面,Then 剩余时间与刷新前一致(误差 ≤ 1 秒),
且阶段未被重置。
3. Given 专注计时结束,When 阶段切换,Then 进入休息阶段并弹出提示。
4. Given 绑定了任务,When 一个番茄正常结束,Then 该任务完成数 +1,今日统计同步更新。
5. Given 绑定的任务在计时中被删除,When 番茄结束,Then 计时正常结束但不累加到任何任务。
### User Story 3 - 查看统计并备份数据 (Priority: P2)
用户查看今日专注时长与完成数,导出一份 JSON 备份,换设备时导入恢复。
**Why this priority**: 统计是留存用户的钩子,备份是数据不丢的兜底,都不影响主线跑通。
**Independent Test**: 预置一份备份文件即可独立运行导入导出。
**Acceptance Scenarios**:
1. Given 今天已完成 3 个番茄,When 打开统计面板,Then 显示番茄数 3 与专注时长 75 分钟。
2. Given 点击导出,When 文件保存,Then 得到符合 schemaVersion 约定的 JSON 文件。
3. Given 导入文件格式非法,When 选择导入,Then 报错并保持现有数据不变。
### Edge Cases
- **页面关闭或系统休眠**:计时以绝对时间戳为准,重新打开后按真实时间计算剩余时间,
不做累加。
- **超过 12 小时未完成的番茄**:视为中断,不计入统计,状态回到空闲并提示。
- **localStorage 不可用**(隐私模式、配额满):应用必须仍可运行,降级为内存存储并
显示提示,不得白屏。
- **多标签页同时打开**:状态通过 storage 事件同步,冲突时以最后写入为准,
且不得同时跑两个番茄。
- **导入备份的 schemaVersion 高于当前应用**:拒绝导入并提示版本过新。
- **系统时钟被手动回拨**:剩余时间不得为负,也不得凭空增加。
## Requirements
### Functional Requirements
- **FR-001**: 系统 MUST 支持任务的创建、完成、编辑、删除。
- **FR-002**: 系统 MUST 将任务、计时状态、统计数据持久化到 localStorage。
- **FR-003**: 系统 MUST 支持专注 / 短休 / 长休三阶段,长休每 4 个番茄出现一次。
- **FR-004**: 计时 MUST 以绝对结束时间戳计算剩余时间,不得依赖定时器累加。
- **FR-005**: 系统 MUST 将完成的番茄累加到当前绑定任务,并更新今日统计。
- **FR-006**: 系统 MUST 支持导出 / 导入 JSON 备份,文件带 schemaVersion 字段。
- **FR-007**: localStorage 不可用时系统 MUST 降级运行并提示,不得抛出未捕获异常。
- **FR-008**: 核心操作 MUST 有键盘快捷键,交互元素可聚焦且有可见焦点样式。
### Key Entities
- **Task**: 待办项,含 id、标题、预估番茄数、已完成番茄数、状态(todo/done)、创建时间。
- **TimerState**: 计时状态,含阶段(focus/shortBreak/longBreak)、阶段结束时间戳、
绑定任务 id、是否运行中。
- **Stats**: 按日期聚合的番茄数与专注时长。
- **Backup**: 导出文件,含 schemaVersion 与 tasks / timer / stats 快照。
## Success Criteria
- **SC-001**: 计时核心纯函数(剩余时间、阶段推进、统计累加)行覆盖率 ≥ 90%。
- **SC-002**: 刷新页面后剩余时间误差 ≤ 1 秒;休眠 1 小时后恢复,误差 ≤ 1 秒。
- **SC-003**: 首屏可交互时间 < 1s,构建产物 gzip 后 < 150 kB。
- **SC-004**: 键盘可完成新增任务、开始/暂停计时、切换任务、导出备份全部核心操作。
- **SC-005**: localStorage 被禁用时应用不崩溃,且有可见提示。对比原案例:原文的 spec 里 Edge Cases 担心的是"网络中断、请求重复、取消后仍在写"。换成这个案例后,最关键的那条变成了**"刷新和休眠之后,剩余时间必须和真实时间一致"** —— 这是只有把 spec 写细才会浮出水面的需求。第一版实现几乎必然踩中这个坑(详见后文),而它之所以能被提前拦住,就是因为 FR-004 明确禁止了"定时器累减"这种实现方式。
技术实现规划 - /speckit-plan
目的:指定实现技术栈、框架和架构设计。
/speckit-plan 用 Vite + React + TypeScript + Tailwind CSS 实现,纯前端无后端;
状态用 useReducer + Context,不引入状态管理库;数据存 localStorage;
时间计算全部抽到 src/domain/ 的纯函数里。
输出成果:生成 plan.md:
# Implementation Plan: Focus Todo
## Summary
- 纯前端单页应用:Vite + React + TypeScript + Tailwind CSS,无后端、无网络请求。
- 核心设计:时间与统计计算抽成纯函数(domain/),UI 只负责渲染与派发;
持久化集中在 storage/ 层并带版本迁移。
- 测试强制:Vitest 单元测试覆盖 domain/ 与 storage/,组件测试用 Testing Library,
端到端用 Playwright 跑一条冒烟。
## Technical Context
**Language/Version**: TypeScript 5.x(strict)
**Primary Dependencies**: react、react-dom、tailwindcss、vite、vitest、
@testing-library/react、playwright
**Package Management**: pnpm(lockfile 必须提交)
**Storage**: localStorage,key `focus-todo/v1`,带 schemaVersion
**Testing**: Vitest(单元)+ Testing Library(组件)+ Playwright(端到端冒烟)
**Target Platform**: 现代浏览器(Chrome / Safari / Firefox 最近两个大版本)
**Project Type**: 单页应用
**Performance Goals**: 首屏可交互 < 1s;gzip < 150 kB;计时 tick 不重渲染任务列表
**Constraints**: 不引入状态管理库;domain 层零依赖、零副作用、100% 可单测
## Constitution Check
_GATE: Must pass before Phase 0 research. Re-check after Phase 1 design._
- Code Quality: ESLint + Prettier + `tsc --noEmit`,pre-commit 钩子。[PLANNED]
- Testing: Vitest 单元 + 组件测试;domain/ 覆盖率 ≥ 90%。[PLANNED]
- Time Correctness: 时间计算集中在 domain/timer.ts 纯函数,组件禁止参与时间算术。[PLANNED]
- Performance: 计时每秒只更新计时组件;构建体积纳入 CI 检查。[PLANNED]
- Data Portability: 备份带 schemaVersion,导入先校验后写入。[PLANNED]
Status: PASS (planned). Re-check after Phase 1 and adjust if any violations arise.
## Project Structure
### Documentation (this feature)
specs/001-focus-todo/
├── plan.md # 本文件
├── research.md # Phase 0 输出
├── data-model.md # Phase 1 输出(Task / TimerState / Stats / Backup)
├── quickstart.md # Phase 1 输出
├── contracts/ # Phase 1 输出
└── tasks.md # Phase 2 输出
### Source Code
src/
├── domain/
│ ├── types.ts # Task / TimerState / Stats / Backup 类型定义
│ ├── timer.ts # 纯函数:remainingMs、nextPhase、advance
│ ├── tasks.ts # 纯函数:创建、完成、排序、过滤
│ └── stats.ts # 纯函数:累加、按日聚合
├── storage/
│ ├── persist.ts # 读写 + 版本迁移 + 降级为内存存储
│ └── backup.ts # 导出 / 导入 + schemaVersion 校验
├── state/
│ ├── reducer.ts # 唯一状态机(useReducer)
│ └── AppContext.tsx
├── hooks/
│ ├── useTimer.ts # 每秒 tick + 阶段推进
│ └── useStorageSync.ts # 多标签页同步
├── components/
│ ├── TaskList.tsx
│ ├── TaskItem.tsx
│ ├── TimerPanel.tsx
│ ├── StatsPanel.tsx
│ └── Shortcuts.tsx
└── App.tsx
tests/
├── unit/ # domain 与 storage
├── component/ # Testing Library
└── e2e/ # Playwright 冒烟对比原案例:原文的 plan 是"前后端分离 + IndexedDB 存历史",需要考虑服务端、鉴权、流式协议。这个 plan 里最关键的一行是 domain/ 目录零依赖零副作用 —— 这不是随手加的目录,而是宪章第三条"时间与状态正确性"的落地形式。计划阶段把架构约束写死,Claude 才会把时间算术放进该放的地方,而不是顺手写在组件里。
任务分解 - /speckit-tasks
目的:将实现计划分解为详细的可执行任务。
输出成果:生成 tasks.md:
## Phase 1: Setup
- [ ] T001 初始化 Vite + React + TypeScript 项目,接入 pnpm 与 Tailwind
- [ ] T002 [P] 配置 ESLint / Prettier / `tsc --noEmit` 与 pre-commit
- [ ] T003 [P] 配置 Vitest + Testing Library + Playwright
- [ ] T004 编写 CLAUDE.md:技术栈、目录约定、常用命令、硬性规则
## Phase 2: Foundational(阻塞项)
- [ ] T005 定义领域类型 src/domain/types.ts(Task / TimerState / Stats / Backup)
- [ ] T006 [P] 实现计时纯函数 src/domain/timer.ts(remainingMs / nextPhase)
- [ ] T007 [P] 实现任务纯函数 src/domain/tasks.ts
- [ ] T008 [P] 实现统计纯函数 src/domain/stats.ts
- [ ] T009 实现持久化层 src/storage/persist.ts(版本迁移 + 降级)
- [ ] T010 实现备份导入导出 src/storage/backup.ts
- [ ] T011 实现全局 reducer 与 Context src/state/
**Checkpoint**: Foundation ready
## Phase 3: User Story 1 - 任务清单 (P1) 🎯 MVP
### US1 测试(强制)
- [ ] T012 [P] [US1] 单元测试:任务纯函数 src/domain/tasks.ts
- [ ] T013 [P] [US1] 单元测试:持久化读写与版本迁移 src/storage/persist.ts
- [ ] T014 [P] [US1] 组件测试:新增 / 完成 / 删除任务
### 实现
- [ ] T015 [US1] 实现 TaskList / TaskItem 组件
- [ ] T016 [US1] 接入 reducer 的任务动作
**Checkpoint**: US1 可独立演示
## Phase 4: User Story 2 - 番茄钟 (P1)
### US2 测试(强制)
- [ ] T017 [P] [US2] 单元测试:计时纯函数(刷新 / 休眠 / 时钟回拨)
- [ ] T018 [P] [US2] 单元测试:统计累加 src/domain/stats.ts
### 实现
- [ ] T019 [US2] 实现 TimerPanel 组件与 useTimer hook
- [ ] T020 [US2] 实现番茄与任务联动(含任务被删除的处理)
**Checkpoint**: US2 可独立演示
## Phase 5: User Story 3 - 统计与备份 (P2)
### US3 测试(强制)
- [ ] T021 [P] [US3] 单元测试:备份序列化与非法导入 src/storage/backup.ts
### 实现
- [ ] T022 [US3] 实现 StatsPanel 与导入导出入口
**Checkpoint**: US3 可独立验证
## Phase 6: Polish
- [ ] T023 [P] 实现键盘快捷键与可见焦点样式
- [ ] T024 [P] 实现多标签页 storage 同步
- [ ] T025 端到端冒烟:Playwright 跑通"建任务 → 开始番茄 → 刷新 → 剩余时间不变"
- [ ] T026 编写 quickstart.md 与 README
## Dependencies & Execution Order
- Setup(Phase 1): 无依赖,可立即开始
- Foundational(Phase 2): 依赖 Setup,阻塞所有用户故事
- User Stories(Phase 3+): 均依赖 Foundational;US1 / US2 / US3 可并行
- Polish: 依赖所有目标用户故事完成对比原案例:任务编号从 48 个减到 26 个 —— 没有后端、没有鉴权、没有流式协议,骨架天然更小。但边界条件对应的任务密度反而更高:T013、T017、T021、T024 这四个任务,全是为了让 Edge Cases 变成可执行的断言。
注意 T017 被放在 US2 的测试位置而不是实现之后:计时纯函数的测试先写,"刷新后剩余时间不变"这条断言会在实现之前就失败,Claude 必须把实现写到能通过它为止。
代码实现 - /speckit-implement
目的:根据任务列表执行具体的代码实现。
/speckit-implement
在 Claude Code 中的执行方式:Claude Code 会自动读取 tasks.md,按任务依赖顺序执行。几个实操要点:
第一,会话管理。Claude Code 的上下文窗口有限,26 个任务一次性跑完必然上下文溢出。正确做法是按 Phase 分会话:
# 会话 1:Setup + Foundational
claude --resume # 完成后新开会话
# 会话 2:US1
# 会话 3:US2
# 会话 4:US3 + Polish第二,允许 Claude 执行命令。测试和构建都需要执行权限:
claude --allowedTools "Bash(pnpm:*),Bash(pnpm run:*),Read,Edit,Write,Grep,Glob"第三,把硬规则放进 CLAUDE.md 而不是 prompt。T004 写的那条"时间计算只能出现在 domain/ 纯函数里",是 Claude 每个会话都会读到的约束。如果只在某一个会话的 prompt 里说了,换个会话就失效了。
第四,先测试再实现。宪章第二条要求 Red-Green-Refactor,在任务列表里体现为每个用户故事下面先跟一组"测试(强制)"任务。Claude 执行时会先看到测试任务,生成的实现自然对着断言写。
第一版效果呈现
按 /speckit-implement 跑完后,典型会遇到三个问题:
问题 1:刷新页面,剩余时间对不上。
Claude 第一版最自然的写法是累加:
const [remaining, setRemaining] = useState(25 * 60 * 1000)
useEffect(() => {
const id = setInterval(() => setRemaining((r) => r - 1000), 1000)
return () => clearInterval(id)
}, [])看着没问题,但电脑休眠十分钟再唤醒,页面上的剩余时间会比真实时间多出十分钟 —— 因为定时器被浏览器冻结了。
解法:FR-004 早就禁止了这种写法。让 Claude 按规范重写:
// domain/timer.ts —— 纯函数,输入状态与当前时间,输出剩余毫秒
export function remainingMs(state: TimerState, now: number): number {
return Math.max(0, state.endsAt - now)
}组件只负责渲染 remainingMs(state, Date.now()),时钟回拨时 Math.max(0, ...) 保证不会变成负数。规范不是文档,是能被测试的断言来源。
问题 2:每秒 tick,整个任务列表跟着重渲染。
任务一多就明显卡顿 —— 因为计时状态和任务状态塞在同一个 Context 里。
解法:plan 里的"计时每秒只更新计时组件"派上用场。把 Context 拆成两个:任务 Context 只在任务变化时更新,计时 Context 每秒更新一次,任务列表组件不订阅后者。
问题 3:隐私模式下 localStorage 抛异常,整个应用白屏。
Safari 隐私模式里 localStorage.setItem 会直接抛 SecurityError,Claude 第一版没有 try/catch。
解法:FR-007 规定了降级行为。让 Claude 在 storage/persist.ts 里统一兜底:
export function isStorageAvailable(): boolean {
try {
const probe = '__focus_todo_probe__'
localStorage.setItem(probe, '1')
localStorage.removeItem(probe)
return true
} catch {
return false
}
}不可用时降级为内存存储,并在顶部显示一条提示条。功能不完整,但应用不崩 —— 这正是 Edge Cases 里写清楚"不得白屏"的价值。
只改文字、不改代码
规范驱动最爽的时刻在这里。把专注时长从 25 分钟改成 50 分钟,把长休从"每 4 个番茄"改成"每 3 个":
- 改
spec.md里的FR-003与对应常量 - 让 Claude 更新配置常量与相关文案
- 重新跑一次受影响的任务
domain/ 里的纯函数结构与测试一行都不用改 —— 因为测试是拿常量做参数化的。这就是"规范即代码"的实际含义:需求变更落在文本上,而不是散落在几十个组件的字面量里。
总结与实践建议
通过 Spec Kit 的五大核心命令,我们从项目宪章的制定,到需求规格的明确、技术规划的细化、任务分解的执行,直至代码实现的自动化生成,完整地跑通了一遍规范驱动的开发流程。
这个案例展示了 Spec Kit 如何把一个模糊的想法,转化为一个边界清晰、可测试、可维护的小应用。
这个案例想说明的三件事
一、规范驱动开发的门槛不在项目难度,而在"边界条件有没有被写下来"。
番茄钟没有硬件、没有模型、没有分布式,但刷新、休眠、隐私模式、多标签页这四条边界,足以在第一版里制造 bug。写进 spec 之后,它们变成 4 条 FR 和 4 组断言 —— 规范是 bug 的清单,不是文档的仪式。
二、最小的规范单位是"可验证的约束"。
"剩余时间误差 ≤ 1 秒"能变成断言;"代码要优雅"不能。写宪章和 spec 时,凡是能用"可测量"替换"要好"的句子,都替换掉 —— 这是让 Claude 真正对齐的最低成本手段。
三、案例越简单,越能看清流程本身。
复杂业务会把注意力整个吸走,你会分不清是自己规范没写好,还是环境没配好、依赖装不上。先用一个能跑起来的小项目把流程走顺,再去接自己的业务,成功率高得多。
与原文案例的差异
- Edge Cases 的重心不同:流式 AI 对话怕的是"连接中断、请求重复、取消后仍在写",属于网络问题;番茄钟怕的是"时间漂移、存储不可用",属于状态与持久化问题。领域不同,但都需要在 spec 里写死。
- 依赖树小到可以逐个读完:这是小案例最大的好处 —— 你能确认每一行依赖都是有意加的,也更容易判断 AI 生成的代码到底在干什么。
- 验收标准当场可验:不需要 GPU、数据集和训练脚本,打开浏览器点几下就知道做对没做对。
实战建议
先写宪章,再写 spec。 小项目的宪章 30 行足够,但要有第三条这种"领域专属的不可妥协项",否则 Claude 不会在代码里加对应检查。
spec 里必须写 Edge Cases。 这是整个流程里性价比最高的一段。写不出 Edge Cases,通常说明需求本身还没想清楚。
让纯函数承担所有"会出错的算术"。 计时、金额、坐标这类计算一旦散进组件里,就只能靠人肉发现问题。抽成纯函数之后,测试可以穷举各种异常输入。
别在一次会话里跑完所有任务。 按 Phase 分会话,Setup + Foundational 一个会话,每个用户故事一个会话。
规则写进 CLAUDE.md,而不是每次在 prompt 里重复。 项目级的约束文件会自动加载,是最省事也最可靠的注入方式。
别一上来就拿最难的业务练手。 先用一个小项目把规范流程跑顺,确认自己理解了每一步在做什么,再去接真正复杂的系统。
文章大纲
- Spec Kit 介绍
- 什么是 Spec Kit
- 核心理念:规范驱动开发
- 核心特点
- Spec Kit 环境搭建详解
- 下载与安装
- 初始化项目
- 实战案例:构建番茄钟待办 Web 应用
- 为什么选一个最简单的案例
- 项目背景
- Spec Kit 五大核心命令
- 进入项目环境
- Spec Kit 五大核心命令详解
- 制定项目宪章 - /speckit-constitution
- 需求规格说明 - /speckit-specify
- 技术实现规划 - /speckit-plan
- 任务分解 - /speckit-tasks
- 代码实现 - /speckit-implement
- 第一版效果呈现
- 只改文字、不改代码
- 总结与实践建议
- 这个案例想说明的三件事
- 与原文案例的差异
- 实战建议
