prototype 精读:先有问题才有形状,一次原型只回答一个问题
prototype 是『一次性代码,用来回答一个问题』,问题决定产物长什么样:状态/逻辑问题产出单个可分享的 HTML,UI 问题产出同一路由下几个激进不同的变体。本文讲清两条分支各自的规格、它为什么把答案和证据分开保存(原型进 prototype/<name> 分支、工单上留指针)、它和 handoff 的往返配合,以及它刻意摒弃的七件事——包括『用完就删』和对整个 app 做原型。
(本文读的是 mattpocock/skills 快照 5beb35b,2026-10-19。引用原文都能按文中路径回仓库核对。这是「逐个技能精读」系列的第十五篇,聚焦 prototype 一个技能。)
它先问的是「要回答哪个问题」
skills/engineering/prototype/SKILL.md 开头一句就把定义钉死:
A prototype is throwaway code that answers a question. The question decides the shape.
紧接着是「选分支」——这一步决定整个原型长什么样:
并附一条警告:The two branches produce very different artifacts, so getting this wrong wastes the whole prototype. 选不出来时的兜底是「按周围代码猜——后端模块走逻辑、页面组件走 UI——并在原型顶部写明这个假设」。
分支 A:单个可分享的 HTML
状态/逻辑问题,产出一个单文件、可双击打开、可邮件转发的 HTML。LOGIC.md 规定的形态很有辨识度:
- 顶部先写清问题(在可见的 intro 里,不是注释里)——
A logic prototype that answers the wrong question is pure waste. - 一块「当前状态」面板,用带标签的字段渲染,不是原始 JSON dump,每次点击后重渲染,必要时点出「刚才变了什么」
- 自由按钮:每个 action 一个按钮,任何顺序都能点
- 分场景的引导式走查:一个场景一个 tab,每个 tab 下面是「按顺序按的按钮」,每按一下走一步;开始一个走查会重置到已知初始状态,保证每次跑得一样
- 场景要挑纸上难推的那几个:happy path、刁钻的边界情况,以及一次「本来不该允许」的尝试
最关键的一条约束在「隔离逻辑」那一步:
Put the actual logic (the bit that is answering the question) in a single
<script>block written as a small, pure module that could be lifted out and dropped into the real codebase later. The page around it is throwaway; this module isn’t.
而且 Keep it pure: no DOM, no document, no button handlers reaching inside it. 页面调用它,不能反向流。这样问题一旦被回答,验证过的 reducer / 状态机 / 函数集就能直接搬进真实模块。HTML 本身则要求 no framework, no bundler, no server, everything inline,所有标签用领域语言,好让不懂代码的 PM、设计师、领域专家自己驱动。
分支 B:一个路由下几个激进不同的变体
UI 问题,产出「同一路由上几个结构上不同的版本」,通过 ?variant= 搜索参数和一个浮动底栏切换。UI.md 里有两个关键取舍:
一、优先挂在真实页面上。 子形态 A(在已有页面里换渲染)优先于子形态 B(新建一个路由)。理由很锋利:A throwaway route on its own is a vacuum: every variant looks fine in isolation. 换子形态 B 之前先自检:真没有可嵌入的现成页面吗?
二、默认 3 个,上限 5 个。 More than 5 stops being radically different and starts being noise. 变体必须结构不同——布局、信息层级、主操作都不同,不是换颜色:Three slightly-tweaked card grids isn't a UI prototype, it's wallpaper.
切换条本身也定了规格:左右箭头(循环、写回 URL 参数,让变体可分享、刷新后不变)、键盘 ←/→(输入框聚焦时不拦截)、视觉上明显不属于被评估的设计,并且生产构建里隐藏(gate on process.env.NODE_ENV !== 'production')。
它和「一般技能」的差别:主动收窄,而不是主动铺开
一般「生成 UI」的技能倾向于多给:更多方案、更多代码、更多打磨。prototype 一路往反方向收:
- 一个问题,一个产物。
- 状态放内存,不持久化——
Persistence is the thing the prototype is _checking_, not something it should depend on. - 不要测试、不要超出「能跑」的错误处理、不要抽象。
- 不要框架 / 打包器 / 服务。
这套收窄的合理性来自问题的性质:原型要的是快和可被非开发者驱动,不是可维护。一旦你开始加固它(加测试、接真数据库、为「以后可能」做泛化),文档说得很干脆:you have stopped prototyping.
它是「一手来源」,不是一次性产物
原型做完,留下两样东西,去两个地方:
- 答案(结论 + 它回答的问题)写进永久的地方:commit message、ADR、实现工单。主分支只保留被验证的决定。
- 原型本身是「答案所依据的可运行证据」,不删,也不进主分支——
It doesn't belong in main either, because nobody maintains it and it goes stale fast.它被提交到prototype/<name>这条从主分支切出去、永不合并的一次性分支上,并在实现工单上留一个上下文指针指过去。
文档专门回答了这个转身:
Wait, isn’t the prototype supposed to be deleted? Not any more. … The strongest objection to that was not about speed. It was who picks up the work next session, and what do they have to work from? A prose summary of a prototype loses the thing that made it convincing.
同一篇文档还记着另一次转身:逻辑分支以前产出的是终端 app,现在换成单个可分享 HTML。理由是终端 app 只有「克隆了仓库、装了运行时」的人能驱动,恰好把最需要其意见的人(设计师、PM、领域专家)排除在外;换成单文件后,「不读代码的人也能驱动它」被直接列成了一条验收判据。
它和 handoff 的配合
原型天然住在一个独立目录里,会产生大量你不想留在提问线程里的上下文。所以文档给的用法是:别在原线程里跑它。 ——Use handoff to move the work into that session and to bring the answer back. 你在设计对话里撞上一个只有跑代码才能回答的问题,又不想把攒起来的线索花在试错上:hand off 给一个 prototype 会话,拿回答案,再从原对话引用它。The work crosses over twice, your original conversation stays live, and you re-explain nothing.
这就把 handoff 讲到的「分叉」用成了一个具体场景:prototype 是一次「派出去跑、再带答案回来」的作业,而 handoff 是它的运输方式。 原型跑完留下的那条 prototype/<name> 分支和工单上的指针,又正好是下一次交接能直接引用的路径——两条技能在同一个「上下文边界之外的工作靠什么传过去」的问题上接了头。
原文证据
三条判据最见它的边界:
- Someone who doesn’t read code can drive the logic demo.
- It is answered in one sitting. If you’re still building it a day later, the question was too big; split it.
- When it’s over, main contains the decision and none of the prototype, and the implementation issue points at the branch that still holds it.
它有意识摒弃了什么
一、摒弃了「一次性用完就删」。 改成「答案进主线,原型进 prototype/<name> 分支」——留证据,不留维护负担。
二、摒弃了持久化。 状态放内存,除非问题本身就是关于持久化。
三、摒弃了测试、错误处理、抽象、打磨。 它们是别的技能的活;把原型加固成产品,是明确的失败态。
四、摒弃了框架 / 打包器 / 服务。 就为了「双击就能开、能邮件传」。
五、摒弃了没有相邻归属的新路由。 UI 变体默认挂在已有页面上,因为空页面会掩盖设计问题。
六、摒弃了「把整个 app 原型化」。 文档把这个问题切掉:它不是「一个问题」,没有自然停止点,会漂移成生产应用,然后按原型规则写出的代码(无测试、无错误处理)进了用户面前。要卖 demo 就明说这是 demo。
七、摒弃了模型自动往这里路由。 文档记着一个真实命名问题:agent 不知道流程时,会把 prototype 读成「工单有了之后的显然下一步」,于是该实现的时候推荐它。Reach for a prototype only when a specific design question is unresolved and talking won't resolve it.
可迁移结论
- 先写下一句话的问题。 它决定了分支、产物形态,也决定了何时算完。
- 把「可搬走的那块」和「用完就扔的那块」在文件里分开。 纯模块能被抬进真实代码,外壳不能。
- 给评审者一条零安装的驱动路径。 单个 HTML 文件把设计师 / PM / 领域专家从「要装环境」里解放出来。
- 变体要结构不同,数量要封顶。 3 个起步、5 个封顶;再多就是从「激进不同」掉进「噪声」。
- 答案和证据分开保存。 决定进主线,原型进一次性分支并在工单上留指针——这一条同时解决了「谁来接手」和「主线别脏」两个问题。
prototype 把这个系列里几乎所有技能共有的一个判断推到了极端:在动手之前,先把「我在回答什么问题」写下来。 它连产物都按问题分岔,连「什么时候算完」都交给问题——而那份被留下来的原型,是这条原则唯一的物证。
讨论
用 GitHub 账号留言;评论保存在公开仓库chengshu-blog-discussions的 Discussions 里。也可通过 RSS 订阅后续文章。