OpenSpec 手把手实战:从零跑通一个完整功能
场景
假设你有一个 React 项目,用 Vite 起的,Tailwind CSS 做样式。现在要加一个暗黑模式。
需求不复杂,但涉及的东西不少:颜色变量、主题切换、用户偏好持久化、现有组件适配。用 Vibe Coding 的话,你可能和 AI 来回改三四轮。用 OpenSpec,三轮命令搞定。
下面每一步都是你打开终端和 AI 工具能实际看到的输出。跟着做就行。
第一步:装好 OpenSpec
npm install -g @fission-ai/openspec@latest
验证:
openspec --version
# 1.3.0
进到你的项目目录,初始化:
cd my-react-app
openspec init
输出:
✓ 检测到 Claude Code
✓ 已创建 openspec/specs/
✓ 已创建 .claude/skills/opsx-propose.md
✓ 已创建 .claude/skills/opsx-apply.md
✓ 已创建 .claude/skills/opsx-archive.md
✓ OpenSpec 初始化完成!
它自动检测到我装了 Claude Code,生成了对应的 skill 文件。你用 Cursor 的话会生成 .cursor/skills/,用 Copilot 会生成 .github/prompts/,逻辑一样。
看一下现在的目录结构:
my-react-app/
├── src/
│ ├── App.tsx
│ ├── main.tsx
│ └── ...
├── openspec/
│ └── specs/ ← 空的,等你写规格
├── package.json
└── ...
第二步:propose — 让 AI 写规格
在 Claude Code 里输入:
/opsx:propose add-dark-mode
等几秒,AI 开始输出:
正在创建变更:add-dark-mode...
✓ 已创建 openspec/changes/add-dark-mode/proposal.md
✓ 已创建 openspec/changes/add-dark-mode/specs/ui.md
✓ 已创建 openspec/changes/add-dark-mode/design.md
✓ 已创建 openspec/changes/add-dark-mode/tasks.md
变更 "add-dark-mode" 创建成功!
请检查生成的制品,按需调整。
看看它生成了什么
proposal.md:
# Proposal: Add Dark Mode
## Why
用户希望有暗黑模式,夜间使用更舒服。
现在大多数应用都支持,属于基本预期。
## What
- 在应用头部添加亮/暗主题切换按钮
- 为两套主题定义 CSS 自定义属性
- 将用户偏好持久化到 localStorage
- 默认跟随系统偏好(prefers-color-scheme)
## Scope
- 仅切换 UI 主题
- 不涉及数据层和 API
- 切换不加动画(保持简单)
specs/ui.md(增量规格):
# Delta for UI
## ADDED Requirements
### Requirement: Theme Toggle
系统 MUST 在应用头部提供一个主题切换按钮。
系统 MUST 在点击时切换 "light" 和 "dark" 主题。
系统 MUST 在切换按钮上直观显示当前主题。
### Requirement: Theme Persistence
系统 MUST 将用户的主题偏好持久化到 localStorage,
键名为 "theme"。
系统 MUST 在应用启动时加载已保存的偏好。
如果没有保存的偏好,系统 SHOULD 跟随系统偏好
(prefers-color-scheme)。
### Requirement: CSS Variables
系统 MUST 为应用中使用的所有颜色定义 CSS 自定义属性。
系统 MUST 为所有颜色变量提供亮色和暗色两套值。
系统 MUST 根据当前主题应用对应的变量值。
## Scenarios
#### Scenario: 切换到暗黑模式(正常流程)
- GIVEN 应用处于亮色模式
- WHEN 用户点击主题切换按钮
- THEN 系统 MUST 切换到暗黑模式
- AND 背景色 MUST 变为深色
- AND 文字色 MUST 变为浅色
#### Scenario: 切换回亮色模式(正常流程)
- GIVEN 应用处于暗黑模式
- WHEN 用户点击主题切换按钮
- THEN 系统 MUST 切换到亮色模式
- AND 亮色的原始配色 MUST 恢复
#### Scenario: 首次访问且系统为暗色偏好(边界情况)
- GIVEN 一个新用户,无保存的偏好
- AND 用户系统设置为暗色模式
- WHEN 应用加载
- THEN 系统 MUST 以暗黑模式启动
- AND 切换按钮 MUST 显示暗黑模式为当前状态
#### Scenario: 无保存偏好也无系统偏好(边界情况)
- GIVEN 一个新用户,无保存的偏好
- AND 系统没有 prefers-color-scheme 设置
- WHEN 应用加载
- THEN 系统 MUST 默认使用亮色模式
design.md:
# Design: Add Dark Mode
## 方案
用 CSS 自定义属性(变量)管理所有主题色。
用 React context(ThemeContext)管理当前主题状态。
在 document 根节点设置 `data-theme` 属性来切换变量值。
## 文件变更
- src/context/ThemeContext.tsx — 新建,管理主题状态
- src/components/Header.tsx — 添加切换按钮
- src/index.css — 定义两套主题的 CSS 变量
- src/main.tsx — 用 ThemeProvider 包裹 App
## CSS 变量
--bg-primary, --bg-secondary
--text-primary, --text-secondary
--accent, --border
tasks.md:
# Tasks: Add Dark Mode
- [ ] 1.1 创建 ThemeContext(亮/暗状态)
- [ ] 1.2 在 ThemeContext 中添加 localStorage 读写
- [ ] 1.3 在 ThemeContext 中添加系统偏好检测
- [ ] 1.4 在 index.css 中定义 CSS 变量(亮色和暗色)
- [ ] 1.5 添加 data-theme 属性切换
- [ ] 1.6 创建 ThemeToggle 组件
- [ ] 1.7 在 Header 中添加 ThemeToggle
- [ ] 1.8 更新现有组件使用 CSS 变量
- [ ] 1.9 在 main.tsx 中用 ThemeProvider 包裹 App
这一步要花时间 review
AI 生成的规格不一定全对。比如这个 design.md 里面说用 data-theme 属性切换——这个方案可以,但你想用 Tailwind 的 dark: 前缀也行。这种时候直接跟 AI 说:
design 方案改成用 Tailwind dark: 前缀,不用 CSS 变量。
它会改 design.md 和 specs 里的相关描述。
又比如你觉得 tasks 拆得太细了,1.1 到 1.3 可以合并。说一声就行:
把 1.1、1.2、1.3 合并成一个任务:创建 ThemeContext(含 localStorage 和系统偏好检测)
改到你觉得 OK 了,进下一步。这步别省——你和 AI 对齐了要做的事,后面才不返工。
第三步:apply — 让 AI 按规格写代码
/opsx:apply
输出:
正在实施变更:add-dark-mode...
✓ 1.1 创建 ThemeContext(亮/暗状态)
→ 新建 src/context/ThemeContext.tsx
✓ 1.2 添加 localStorage 读写
→ 修改 src/context/ThemeContext.tsx
✓ 1.3 添加系统偏好检测
→ 修改 src/context/ThemeContext.tsx
✓ 1.4 定义 CSS 变量(亮色和暗色)
→ 修改 src/index.css
✓ 1.5 添加 data-theme 属性切换
→ 修改 src/context/ThemeContext.tsx
✓ 1.6 创建 ThemeToggle 组件
→ 新建 src/components/ThemeToggle.tsx
✓ 1.7 在 Header 中添加 ThemeToggle
→ 修改 src/components/Header.tsx
✓ 1.8 更新现有组件使用 CSS 变量
→ 修改 src/components/Card.tsx
→ 修改 src/components/Sidebar.tsx
✓ 1.9 在 main.tsx 中包裹 ThemeProvider
→ 修改 src/main.tsx
所有任务完成!请运行测试验证。
这时候你应该跑一下项目看看效果:
npm run dev
点一下 toggle,看看亮暗切换对不对。刷新页面,看看 localStorage 持久化有没有生效。改一下系统偏好,看看默认主题对不对。
有问题怎么办? 直接跟 AI 说,改完再 apply 一次。比如:
暗黑模式下 Sidebar 背景色没变,修一下。
它会定位到 Sidebar.tsx,改完重新 apply。
第四步:archive — 归档
确认没问题了:
/opsx:archive
输出:
正在归档变更:add-dark-mode...
✓ 已将增量规格合并到 openspec/specs/
→ 新增:UI 主题相关的需求和场景
✓ 已归档变更到 openspec/changes/add-dark-mode/
完成!规格已更新为系统当前行为。
看一下 specs 目录:
openspec/
├── specs/
│ └── ui.md ← 合并进来的暗黑模式规格
└── changes/
└── add-dark-mode/ ← 已归档,留作记录
specs/ui.md 里面就是暗黑模式的完整规格——Requirement、Scenario 都有。下次再改 UI,AI 会先读这个文件,知道系统当前的状态。
进阶:第二轮迭代
第一轮做完了暗黑模式。现在产品说要加"跟随系统"这个选项——系统切暗,App 自动跟着暗。
按 Vibe Coding 的做法,你跟 AI 说"加个跟随系统的功能",它可能改 ThemeContext,可能改 ThemeToggle,也可能两个都改,改完你也不知道改全了没有。
按 OpenSpec 的做法:
/opsx:propose add-auto-theme
AI 会基于已有的 specs 生成 Delta Spec——只说这次新增了什么、改了什么:
# Delta for UI
## MODIFIED Requirements
### Requirement: Theme Persistence
系统 MUST 支持三种主题模式:"light"、"dark" 和 "auto"。
当主题为 "auto" 时,系统 MUST 跟随系统偏好。
系统 MUST 通过 prefers-color-scheme 媒体查询实时检测系统偏好变化。
(之前:仅支持 "light" 和 "dark",系统偏好作为兜底)
## ADDED Requirements
### Requirement: Auto Theme Toggle
系统 MUST 在主题切换中显示三个选项:
"Light"、"Dark" 和 "Auto"。
系统 MUST 指示当前激活的是哪个模式。
#### Scenario: 切换到自动模式(正常流程)
- GIVEN 应用处于亮色模式(手动设置)
- WHEN 用户在主题切换中选择 "Auto"
- THEN 系统 MUST 跟随系统偏好
- AND 切换按钮 MUST 显示 "Auto" 为激活状态
#### Scenario: 自动模式下系统切换(边界情况)
- GIVEN 应用处于 "auto" 模式,当前显示亮色主题
- AND 用户系统切换到暗色模式
- THEN 系统 MUST 立即切换到暗色主题
- AND 切换按钮 MUST 继续显示 "Auto" 为激活状态
注意 Delta Spec 只说了变化的部分:Theme Persistence 从"两种模式"改成"三种模式",新增了 Auto Theme Toggle 的需求。没变的(CSS 变量、颜色定义)完全不用重复。
apply 完之后 archive,增量规格再次合并到主 specs。主 specs 始终保持最新。
踩坑记录
1. propose 之后直接 apply,不 review
最容易犯的错。AI 生成的规格大概 80% 对,剩下 20% 需要你改。不改直接 apply,代码出来不对,又得回头改规格再 apply。不如一开始花两分钟看一遍。
2. Spec 写得太笼统
### Requirement: Dark Mode
系统 MUST 支持暗黑模式。
这种 Spec 等于没写。AI 会按自己的理解来——可能做一套完整的主题系统,也可能就改个背景色。Spec 越具体,AI 输出越可预测。
3. Delta Spec 忘了写 MODIFIED
加新功能的时候很容易只写 ADDED,忘了有些现有需求也被改了。比如上面的"三种模式"——如果不写 MODIFIED,AI 可能只加 auto 模式的代码,但不改原来的两选一切换逻辑,结果两个功能互相打架。
4. 一个 change 塞太多东西
一个 propose 里塞三个大功能。生成的 specs 几百行,tasks 十几条,apply 出了问题不好定位。一个 change 一件事。 暗黑模式是一个 change,OAuth 登录是另一个 change,不要合并。
5. archive 之后发现还有问题
归档了不代表不能改。开一个新的 propose 就行。比如暗黑模式已经归档了,后来发现还有 bug,开一个 fix-dark-mode-flash 的 change。Delta Spec 写清楚要改什么,apply,再 archive。
和速通版的关系
如果你还没看过速通版,这里是完整版的关键区别:
| 速通版(5 分钟) | 这篇(手把手) | |
|---|---|---|
| 目标 | 快速知道 OpenSpec 干嘛的 | 跟着做一个完整功能 |
| 输出展示 | 只列命令 | 每步的完整输出 |
| 踩坑 | 不涉及 | 专门列了 |
| 进阶 | 不涉及 | 第二轮迭代、Delta Spec |
基于 OpenSpec v1.3.0,项目迭代快,以最新版本为准。