按故事设计原型
构建一个产品的单文件HTML原型,可在多个设备上查看,并按用户故事组织,如同按设备组织一样。
顶部栏有两个选择器:
- 设备:例如 Website · Android · iPhone。同一产品,不同框架。
- 故事:“所有故事”以及每个用户故事对应的条目。选择某个故事会将共享数据重置为该故事的种子数据,将每个设备导航至故事的起始屏幕,并打开一个面板,展示该故事、优先级、状态、验收条件、演练步骤和备注。
两个选择状态都保存在URL哈希中(如 #story=S2&device=iphone),因此可以将某个故事在某个设备上的链接粘贴到评审或工单中。
工作流程
-
收集输入(仅在一次消息中询问缺失的内容):
- 产品是什么,用户是谁。
- 展示哪些设备。默认:Website + Android + iPhone。如果是现场/自助服务终端使用场景,则加上平板。
- 用户故事,可以是任何形式(列表、积压工作导出、功能文档)。
- 验收条件格式: Given/When/Then,还是纯规则?默认使用 Given/When/Then。
- 优先级(MoSCoW)和状态,如果用户有的话。否则默认为
should/draft。 - 主要数据模型(主记录 + 关键字段),或从故事中推断。
将每个故事规范化为故事格式。如果用户没有故事,则从产品描述中草拟3-6个,并在构建前确认。
-
检查故事是否符合质量检查清单。以一个简短列表的形式提出问题。不要擅自重写用户的措辞。
-
规划每个故事的屏幕。 对于每个故事,列出它在每个设备上涉及的屏幕以及所需的种子数据。只有当产品确实以这种方式工作时(例如,管理员仅在网站上),故事才会跳过某个设备。在其
devices列表中注明,并且选择该故事时,切换器会禁用其他设备。 -
将
assets/shell.html复制到用户选择的路径(默认为docs/reference/prototype.html)并填充内容。阅读references/prototype-spec.md了解视觉规则(框架、标记、字体、卡片、药丸、提示、抽屉、图标、文案语气)。 -
构建屏幕,而非故事。 屏幕是共享的;故事是入口点和种子数据。永远不要为每个故事重复创建屏幕。如果某个故事需要变体,则从种子数据中驱动。
-
验证 在浏览器中。如果有浏览器工具,则使用它。否则使用 headless Chrome:
--dump-dom "file://…/prototype.html#story=S2&device=iphone"检查渲染内容,--screenshot查看,--enable-logging=stderr捕获控制台错误。检查:- 每个故事在其列出的每个设备上是否都显示在正确的屏幕上;
- 每个验收条件是否都可演示,并且演练步骤是否可点击完成;
- 在故事中间切换设备是否保持状态;
- 没有故事的控制出现在另一个故事下,并且“所有故事”下每个故事的控制都能协同工作;
- 控制台没有错误。
-
保持同步。 当后续添加或更改故事时,在同一编辑中更新其
#story-text条目、BEHAVIOUR条目以及其列出的每个设备。
故事格式
一个故事在 shell 中存在于两个位置,均由它的 id 索引:
- 它的文本位于
#story-textJSON 块中,在故事面板中以只读方式显示; - 它的行为位于
BEHAVIOUR中,这是纯代码。
<script type="application/json" id="story-text">
{
"S1": {
"title": "保存搜索",
"story": "作为一名访客,我可以保存一个搜索,以便无需再次搜索就能收到新匹配的通知。",
"priority": "must",
"status": "draft",
"ac": [
{ "id": "S1-AC1", "text": "Given 我搜索了“橡木桌”,when 我点击保存搜索,then 它出现在“已保存”下" },
{ "id": "S1-AC2", "text": "Given 该搜索已保存,when 我再次保存它,then “已保存”仍然只显示一次" }
],
"acNext": 2,
"walkthrough": ["搜索“橡木桌”", "点击保存搜索", "打开“已保存”"],
"outOfScope": ["邮件提醒"],
"assumptions": ["访客必须登录才能保存"],
"questions": ["已保存的搜索是否有数量限制?"]
}
}
</script>
const BEHAVIOUR = {
S1: {
devices: ['web', 'android', 'iphone'], // 该故事适用的框架
start: { web: 'search', android: 'search', iphone: 'search' }, // 每个设备的屏幕
features: ['save-search'], // 该故事启用的功能
seed: () => ({ user: SIGNED_IN, saved: [] }), // 该故事的新数据
},
};
| 字段 | 规则 |
|---|---|
story | 作为一名<角色>,我可以<操作>,以便<收益>。收益是用户获得的好处,而不是对操作的重新陈述。 |
priority | must · should · could · won't(MoSCoW)。 |
status | draft · in review · agreed。 |
ac | 对象 { id, text }。ID 为 <故事>-AC<n>,从不重新编号,从不重复使用。见下方。 |
acNext | 该故事历史上分配的最高 AC 编号。新 AC 取 acNext + 1,并更新 acNext。每当添加 AC 时都要更新它。 |
walkthrough | 评审者点击的演示脚本。不是验收条件;保持简短。 |
outOfScope, assumptions, questions | 纯列表。如果为空则留空,不要编造填充内容。记录用户不确定的任何内容作为问题。 |
seed() 每次调用都返回一个新对象,因此重新选择一个故事总是会干净地重新开始。“所有故事”使用 DEFAULT_SEED 和每个设备的主屏幕。
验收条件
- 每个 AC 对应一个可测试的行为,采用用户选择的格式。Given/When/Then 为一行:
Given <上下文>, when <操作>, then <可观察的结果>。 - 精确文案: 当 AC 涉及消息时,引用消息(
then I see "Nothing open right now."),永远不要写“一条友好的消息”。 - 结果,而非样式: “它显示为‘完成’”,而不是“出现一个绿色药丸”。只有当视觉细节就是需求时,它才是一个 AC。
- 在单一设备上可测试。 不要编写关于其他设备的 AC(“出现在所有设备上”、“仅限 Web”、“手机无法更改”)。故事覆盖哪些设备属于
devices,设备间同步是后端的问题。 - ID 是永久的。
S1-AC3在编辑、移动或拆分为子故事时保留其 ID,以便可以追溯到测试和工单。新 AC 取acNext + 1。删除的 ID 永远不会再次分配,且从不重新编号。
质量检查清单
在构建每个故事之前以及每次更改时进行检查:
- 角色是否是真实的用户角色,收益是否是他们会说的话?
- 它是否独立交付价值,并且是否足够小以便在一分钟内演示?如果不是,拆分它。
- 每个 AC 是否有 ID、单一行为和可观察的结果?
- 是否涵盖了空、错误和极限情况,或者列在
questions下? - 对其他故事的依赖是否可见?(一个共享的功能键,以及在
assumptions中的提及,例如“基于 S2”。)
故事功能在故事被选中之前是关闭的
故事引入的每个交互都有一个功能键,列在该故事的 features 中。只有当选择了列出该键的故事时,can('save-search') 才为真。
- 渲染: 将故事的控制包装在
can(...)中,这样它们在其他情况下就不存在(没有按钮、没有表单、没有菜单项)。不要以禁用状态显示它们。评审者应该只看到所选故事添加的内容。 - 处理程序: 每个故事处理程序以
if (!can('save-search')) return;开始,这样即使是从过时的 DOM 或控制台触发,故事外也不会触发任何操作。 - “所有故事” 同时启用每个故事的功能:整个产品的当前状态,基于
DEFAULT_SEED。检查故事在那里是否冲突(例如,两个控件在同一位置)。 - 一个依赖于另一个故事交互的故事会列出两个键(例如,“按状态筛选”列出
['filter', 'complete'],以便可以展示勾选任务时过滤列表的变化)。 - 故事只显示的数据(例如,负责人的姓名首字母)不受门控;只有更改数据的能力受门控。
更改故事
原型仅展示故事;它从不编辑或保存故事。所有故事更改都通过 Claude 在聊天中进行:
- 新故事: 草拟故事、AC 和问题,与用户一起运行质量检查清单,然后在一个编辑中编写
#story-text条目和BEHAVIOUR条目并构建屏幕。故事永远不会只作为文本存在。如果存在,shell 会将其排除在切换器之外并在控制台中记录错误。 - 更改的故事(措辞、AC、优先级、状态、备注):编辑
#story-text。如果更改改变了故事的功能(新的 AC、不同的演练步骤),则在同一个编辑中更新屏幕和BEHAVIOUR。 - 评审反馈: 当用户传递评审者的评论时,将每个评论转化为上述更改之一,如果仍未解决,则转化为
questions下的条目。 - 始终说明您构建或更改的内容,按故事和 AC ID。
如果用户在跟踪系统(例如 Jira)中维护故事,则该系统是主副本。在此处镜像其措辞,而不仅仅在原型中编辑故事。
拆分故事
当用户要求拆分故事,或者一个故事有超过约5个AC、多个主要功能键,或者AC可以独立发布时,进行拆分。
-
选择一个拆分模式 并说明你使用的是哪一种:
- 工作流程步骤(创建 → 审核 → 发布)
- 操作(添加 / 编辑 / 删除)
- 先快乐路径,后边缘情况(空、重复、达到限制)
- 按设备或角色划分(手机捕获 vs 网站管理)
- 按数据变化(一个项目 → 多个,一种货币 → 多种)
- 先简单,后额外功能(简单排序 → 保存的排序偏好)
每个子故事仍然必须是一个完整的
作为一名… 我可以… 以便…并具有自身的价值。 -
从父故事开始编号子故事:
S3→S3.1,S3.2, … 从#story-text和BEHAVIOUR中移除父故事,并将子故事放在其位置。shell 的readHash会将旧的#story=S3链接发送到S3.1,因此共享链接继续有效。 -
划分,而非复制:
- 验收条件。 每个父故事的 AC 移动到恰好一个子故事中,并保留其原始 ID(
S3-AC2留在S3.1内部)。如果一个 AC 跨越两个子故事,则将其重写为两个新的 AC(S3.1-AC1,S3.2-AC1),并注明它们替换了哪个父故事 ID。 - 功能键。 如果交互被拆分,则拆分键(
filter→filter-status,filter-counts)。一个子故事也可能列出兄弟故事的键,如果它基于此键构建,如上述依赖规则所示。 - 设备。 每个子故事仅列出它实际涉及的设备。
- 种子。 后续子故事的
seed()从较早子故事留下的状态开始(例如,S3.2 种子一些已保存的项目),以便可以独立评审每个子故事。 - 备注。 为每个子故事提供适用于它的范围外项目、假设和问题。除非用户另有说明,否则子故事继承父故事的优先级。
- 验收条件。 每个父故事的 AC 移动到恰好一个子故事中,并保留其原始 ID(
-
更新屏幕: 重新门控任何移动到不同键的控件。然后检查每个子故事在其列出的每个设备上,并检查父故事的 id 是否重定向。
-
报告拆分 以简短的表格形式(子故事 ID、故事、优先级、设备、AC ID),以便用户可以检查每个父故事 AC 是否都已落地。如果用户在积压工作或功能文档中维护故事,则在其中镜像拆分。
规则
- 单个HTML文件,内联CSS/JS,无需构建步骤,无需框架,无需后端。直接从磁盘打开。
- 所有数据位于一个共享的内存中
state;每个设备读取和写入它。 - 渲染函数接受设备 id 并从
state重新绘制(参见 shell)。 - 使用虚构的、中性的示例数据。永远不要将其他项目中的名称、品牌或内容复制到原型中。
- 移动框架渲染相同的应用标记;Android 和 iPhone 之间的差异是框架本身(状态栏、刘海/岛、手势条),而不是不同的应用,除非用户要求平台特定的UI。