OpenSpec 手把手实战:从零跑通一个完整功能
场景
假设你有一个 React 项目,用 Vite 起的,Tailwind CSS 做样式。现在要加一个暗黑模式。
需求不复杂,但涉及的东西不少:颜色变量、主题切换、用户偏好持久化、现有组件适配。用 Vibe Coding 的话,你可能和 AI 来回改三四轮。用 OpenSpec,三轮命令搞定。
下面每一步都是你打开终端和 AI 工具能实际看到的输出。跟着做就行。
第一步:装好 OpenSpec
bash
npm install -g @fission-ai/openspec@latest
验证:
bash
openspec --version
# 1.3.0
进到你的项目目录,初始化:
bash
cd my-react-app
openspec init
输出:
bash
✓ 检测到 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/,逻辑一样。
看一下现在的目录结构:
css
my-react-app/
├── src/
│ ├── App.tsx
│ ├── main.tsx
│ └── ...
├── openspec/
│ └── specs/ ← 空的,等你写规格
├── package.json
└── ...
第二步:propose --- 让 AI 写规格
在 Claude Code 里输入:
bash
/opsx:propose add-dark-mode
等几秒,AI 开始输出:
csharp
正在创建变更: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" 创建成功!
请检查生成的制品,按需调整。
看看它生成了什么
markdown
# Proposal: Add Dark Mode
## Why
用户希望有暗黑模式,夜间使用更舒服。
现在大多数应用都支持,属于基本预期。
## What
- 在应用头部添加亮/暗主题切换按钮
- 为两套主题定义 CSS 自定义属性
- 将用户偏好持久化到 localStorage
- 默认跟随系统偏好(prefers-color-scheme)
## Scope
- 仅切换 UI 主题
- 不涉及数据层和 API
- 切换不加动画(保持简单)
specs/ui.md(增量规格):
markdown
# 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 默认使用亮色模式
markdown
# 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
markdown
# 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 说:
yaml
design 方案改成用 Tailwind dark: 前缀,不用 CSS 变量。
它会改 design.md 和 specs 里的相关描述。
又比如你觉得 tasks 拆得太细了,1.1 到 1.3 可以合并。说一声就行:
javascript
把 1.1、1.2、1.3 合并成一个任务:创建 ThemeContext(含 localStorage 和系统偏好检测)
改到你觉得 OK 了,进下一步。这步别省------你和 AI 对齐了要做的事,后面才不返工。
第三步:apply --- 让 AI 按规格写代码
bash
/opsx:apply
输出:
css
正在实施变更: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
所有任务完成!请运行测试验证。
这时候你应该跑一下项目看看效果:
bash
npm run dev
点一下 toggle,看看亮暗切换对不对。刷新页面,看看 localStorage 持久化有没有生效。改一下系统偏好,看看默认主题对不对。
有问题怎么办? 直接跟 AI 说,改完再 apply 一次。比如:
暗黑模式下 Sidebar 背景色没变,修一下。
它会定位到 Sidebar.tsx,改完重新 apply。
第四步:archive --- 归档
确认没问题了:
bash
/opsx:archive
输出:
bash
正在归档变更:add-dark-mode...
✓ 已将增量规格合并到 openspec/specs/
→ 新增:UI 主题相关的需求和场景
✓ 已归档变更到 openspec/changes/add-dark-mode/
完成!规格已更新为系统当前行为。
看一下 specs 目录:
csharp
openspec/
├── specs/
│ └── ui.md ← 合并进来的暗黑模式规格
└── changes/
└── add-dark-mode/ ← 已归档,留作记录
specs/ui.md 里面就是暗黑模式的完整规格------Requirement、Scenario 都有。下次再改 UI,AI 会先读这个文件,知道系统当前的状态。
进阶:第二轮迭代
第一轮做完了暗黑模式。现在产品说要加"跟随系统"这个选项------系统切暗,App 自动跟着暗。
按 Vibe Coding 的做法,你跟 AI 说"加个跟随系统的功能",它可能改 ThemeContext,可能改 ThemeToggle,也可能两个都改,改完你也不知道改全了没有。
按 OpenSpec 的做法:
arduino
/opsx:propose add-auto-theme
AI 会基于已有的 specs 生成 Delta Spec------只说这次新增了什么、改了什么:
markdown
# 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 写得太笼统
markdown
### 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,项目迭代快,以最新版本为准。