把功能塞进本体,还是把边界交出去
把功能塞进本体,还是把边界交出去
一个人做软件,最危险的时刻不是没人用,是「功能请求的增长速度快过你的时间」。
我在这件事上摔过一次,后来才想明白:解法不是写得快点,是把该交出去的东西交出去。
一、杂物间
做 X-Note 的前几个版本,我的做法非常直觉:有人要什么,我就往本体里塞什么。
于是主程序慢慢变成了一间杂物间:双向链接、关系图谱、标签面板、版本历史 diff、导入导出、云同步、多窗口、公式、流程图……每个功能都合理,加在一起就出问题——
- 任何一个功能出 bug,用户第一反应是「这个软件坏了」,而不是「那个插件坏了」
- 每次发版都要把所有功能重新测一遍,因为它们共享同一份状态
- 想改一个功能的交互,得先确认它没有牵扯到别处
这不是代码问题,是耦合的责任问题:我允许功能进入本体,就等于承诺了对它的长期维护。
二、同一个问题,我给了两个相反答案
有意思的是,我这段时间同时在写两个笔记应用,而它们对「扩展」的态度几乎相反。
X-Note 走的是生态优先:插件放在独立仓库 X-Note-Plugins,通过 index.json 索引 + jsDelivr 分发 zip,主程序动态加载目录。现在里面有六个:AI 写作助手(润色 / 翻译 / 摘要 / 生成标题标签)、GitHub Gist 备份、Gitee Gist 备份、全库待办看板、批量查找替换、链接健康检查。
每个插件有自己的版本号和发布节奏,主程序不必跟着发一次版。这一点直接把我从「加功能就得整体回归」里解出来了。
X-Notepad 走的是边界优先,做得更狠:
- 插件只接受 纯 WASM,用 WIT 描述接口(Component Model),v1 的核心模块留了兼容层
plugin.json里必须声明要用哪些 host 能力,安装前先给你看清单- 插件包用 Ed25519 签名,内容摘要对不上直接拒装
为什么不做 .js 插件?因为在 WebView 里跑的 .js 能拿到 invoke,那等于把整个后端交给它——沙箱就是一层纸。既然决定开这个口子,就得同时给边界,否则「可扩展」会变成「不可控」。
三、交出去之后,我才发现自己真正的工作是什么
插件化带来的变化,不是「少写了功能」,而是职责变了。
以前我是唯一作者:写功能、修 bug、决定交互。现在我先要回答几个更靠前的问题:
| 以前 | 现在 |
|---|---|
| 这个功能怎么实现 | 这个能力该以什么粒度暴露 |
| 加在哪一层 | 接口要不要版本化,老插件还能不能跑 |
| 怎么调 bug | 权限声明怎么写,越权怎么拒 |
| 什么时候发版 | 主程序和插件怎么各自发版还互不破坏 |
最难的是最后一条。主程序只要改一次 ABI,所有插件都得跟着改——所以我不得不提前想清楚「什么算稳定接口」。这件事写功能的时候根本不会想。
还有一个不好受的部分:定接口比写功能慢得多。写一个「生成标题」的功能,一天就够;设计一个让所有插件都能安全请求模型的接口,要考虑超时、并发、失败重试、用户额度、密钥从哪来、日志怎么记。前者产出一段能用的代码,后者产出一个别人能用的约束。
四、插件不是免费的
我也不想把插件化说成万灵药,它有几个明确的代价:
- 用户多一步。要装、要更新、要授权。多数人不会为了一点增益去做这三件事。
- 问题分散。插件出错时,用户找的是你,不是插件作者。
- 接口一旦公开就是承诺。破坏性变更会被骂,不破坏就得背兼容层。
- 插件质量不可控。你得接受有人写出让主程序卡死的东西。
所以我的取舍是:只有「一个人不可能全写完」的领域才开插件口。备份、AI 写作、批量操作属于这一类;编辑器内核、文件读写不属于——那部分必须自己扛住。
五、心态的变化
早期我把「功能多」当成竞争力,看到别人的软件功能表更长会焦虑。现在我知道那份功能表背后是一堆没人维护的承诺。
插件化给我的真正收获不是数量,是分寸感:知道哪些该自己做扎实,哪些该交给别人做、自己只负责规则。
一个人做软件的天花板,从来不是写代码的速度,而是你能同时维护多少个东西。插件化是把这块天花板往上顶一格——代价是你得先学会说「这个我不做」。
现在回头看,X-Note 那间杂物间是必要的:不塞满一次,就不会知道塞满之后有多难收拾。而 X-Notepad 里那些签名、权限、ABI 版本协商,全是从那次收拾里学来的。
评论