卷 VI · 造CH 24深度 24/24

你该开多大的门

最后一章不写代码。我们回到第 1 章那个问题 —— 你要不要开这道门,开多大。这一章给你一张能直接填的决策表、四档现成的配置、以及一份从零开始的落地顺序。读完你手上应该有一个具体的答案,而不是一堆概念。

决策表四档配置落地顺序收尾

第一个问题:你真的需要插件系统吗

第 1 章讲过,这里做成一张能直接判的表。三条全中才继续往下走:

条件为什么它是必要的中了吗
① 需求在长尾上
不是一个功能一万人要,而是一万个功能各有几人要
否则你自己做完就行了,插件系统是纯亏
② 变化速率你追不上
那部分东西的变化频率 ≫ 你的发版周期
否则改源码或加配置项更划算
③ 有人愿意替你写没有社区,你造的只是一套给自己添麻烦的加载器

只中一两条:先做配置项,或者做好模块划分。把「插件系统」这个想法记下来,等三条都中了再回来。

第 ③ 条尤其容易被跳过。很多团队花几个月造了一套精美的插件系统,然后发现没人写插件 —— 因为他们从来没验证过有人想写。验证方法很土但有效:先手动帮三个真实用户做他们想要的扩展。如果这个过程中你发现「他们其实自己能写,只要我开个口」,那就是绿灯;如果你发现「他们只是想要这个功能,不在乎谁写」,那你需要的是功能,不是插件系统。

第二个问题:你的处境更像谁

这是全书最重要的一个判断。四个问题,答案决定了剩下的一切:

问题如果答案是 A如果答案是 B
① 插件作者是谁? 我自己 / 我团队 / 严格审核过的合作方 任何人
② 用户数量级? 内部工具 / 几千个愿意折腾的深度用户 几十万以上普通用户
③ 应用里有什么数据? 不敏感 / 用户自己的本地内容 凭据、隐私、钱、企业数据
④ 一个插件卡死应用,后果? 用户重启一下,抱怨两句 商店一星、上新闻、丢客户

数一下你有几个 B。

B 的个数你的档位参照系统
0 个档位一:只要注册表
1 个档位二:清单 + 权限 + 错误边界Obsidian、Mihon
2–3 个档位三:运行时隔离VS Code
4 个档位四:全套Chrome
四档配置

档位一:只要注册表

适用:插件只由你和你团队写。这是绝大多数项目的正确答案。

要做明确不做
扩展点注册表(第 4 章,30 行)清单 —— 直接用代码注册就行
可弃置对象(第 13 章,8 行)权限 —— 都是自己人
调用插件时 try/catch(第 6 章)隔离 —— 出事了改代码就行
版本兼容 —— 一起编译的

大约 50 行,一下午做完。如果有人建议你「顺便把插件系统做完整点」,请指出:完整插件系统的复杂度全部来自「作者是陌生人」这一个前提,而你没有这个前提。

档位二:清单 + 权限 + 错误边界

适用:有第三方插件,但用户是愿意承担风险的深度用户,且数据不算特别敏感。

这就是本书第 23 章那台宿主。在档位一基础上加:

  • 清单 + 严格校验(第 7、8 章)—— 未知即错误,错误信息写给作者看;
  • 版本范围检查(第 8、19 章);
  • 按能力域分组的 ctx + 权限闸门(第 5、17 章);
  • 懒激活(第 21 章)—— 清单里能声明的都声明;
  • 签名 / 来源记录(第 22 章)—— 至少记下来,更新时比对;
  • 所有 API 异步化(第 12、18 章)—— 这一条现在做,成本为零
  • 诚实的安全文档(第 14、22 章)—— 别说「沙箱」。
◆ 档位二最重要的一条

把所有插件 API 设计成返回 Promise。

现在做,成本几乎为零(同进程时 async 函数就是立刻 resolve)。不做的话,将来你想升到档位三时,要让所有插件作者重写代码 —— 而他们中的大部分不会重写。

这是全书性价比最高的一条建议:一个今天几乎不花钱的决定,买下了明天升级的可能性。

档位三:运行时隔离

适用:插件来自不认识的人,且「插件卡死应用」或「插件读到敏感数据」的后果你承受不起。

在档位二基础上:

  • 插件挪进 Worker / 独立进程(第 16 章选型表);
  • RPC 桥:超时、崩溃恢复、错误跨边界传递、粗粒度 API(第 18 章);
  • UI 改成声明式贡献点(第 5、18 章)—— 这是隔离逼出来的,也是最大的收益;
  • 耗时统计与归因(第 15、21 章);
  • 远程封禁通道(第 22 章)。

这一档的成本是真金白银的:RPC 基础设施要几千行,UI 贡献点体系要重新设计,插件 API 全部异步。但你买到的是「插件永远搞不垮主应用」这句能对用户说的话 —— 第 15 章说过,这是一条产品上的分界线,不是一个优化。

档位四:全套

适用:任何人可上传 + 几十万普通用户 + 敏感数据。

在档位三基础上再加:权限逐项申请 + 用户可见的授权界面(第 17 章)、商店与人工审核(第 22 章)、自动扫描、灰度发布、可复现构建、以及一个持续的安全团队

如果你在读这一段并觉得「我可能需要这一档」—— 先重新数一遍上面那四个 B。这一档的持续成本(不是一次性成本)通常需要一个专职团队,而且它的很多机制(独立世界、进程沙箱)需要你控制运行时本身。绝大多数产品应该停在档位二或三,然后把力气花在生态运营上。

落地顺序

如果从零开始,按这个顺序做

不管你的目标是哪一档,都从上往下走 —— 每一步都能单独上线,每一步都有独立的价值。

#做什么为什么是这个顺序章节
1先硬编码三个功能不看到三个,你抽出来的不是「这一类」的形状04
2抽出扩展点注册表身份 / 契约 / 多重性 / 顺序,四件事一次定清楚04
3设计 ctx,按能力域分组现在分组不花钱,将来加权限时省一次大重构05
4所有 API 异步化唯一一件「越晚做越贵」的事12 · 18
5可弃置对象 + 自动登记让正确的写法成为最省事的写法13
6清单 + 严格校验它同时解决安全、性能、分发三件事07 · 08
7懒激活清单做好之后,这一步只有十几行21
8权限闸门ctx 已经分好组了,这一步只是加一行 if17
9耗时统计它会告诉你下一步该优化什么15 · 21
10(如需)挪进 Worker因为第 4 步做了,这一步比想象中容易16 · 18

注意这个顺序里的一条暗线:第 3、4、5 步几乎不花钱,但它们让第 8、10 步从「大重构」变成「加几行」。插件系统的所有痛苦,几乎都来自「第一天没做那几件便宜的事」。

五个最常见的错,和它们的代价

这些是真实发生过、而且反复发生的。按「事后有多难修」排序:

错误为什么会犯事后修的代价
① 同步 API
插件 API 全是同步的
同进程时同步最自然,也最好写 最贵。要上隔离就得让所有插件作者重写代码 —— 而大部分人不会重写,生态死一半
② 交出内部对象
为了省事把 app / db 整个传给插件
「先跑起来再说」 很贵。你的每个内部字段都成了公开 API,从此不敢重构
③ 太早抽象
产品还没定型就设计了一套灵活的插件架构
觉得「架构要先行」 中等。猜错的扩展点没人用,真正需要扩展的地方没开口,只能重来
④ 沉默失败
插件出错就默默跳过、往不存在的扩展点注册也不报错
怕报错吓到用户 中等。但它持续消耗你和插件作者的时间 —— 因为出错的人和能看到错误的人不是同一个人
⑤ 号称有沙箱
用 Proxy 包一层就在文档里写「安全沙箱」
真心以为那是沙箱 代价由用户承担,而且是在出事那天一次性到账
◆ 注意①和⑤的共同点

它们都是「今天几乎不花钱,但决定了你三年后有没有选择」的事。

把 API 写成异步,今天多敲六个字母;三年后它是你能不能加隔离的分水岭。

把安全模型说准,今天多写三行文档;出事那天它是「用户知情地承担了风险」和「我们骗了用户」的区别。

插件系统的大部分痛苦,都不是因为某天做了个艰难的错误决定,而是因为很多天里没做那几件便宜的对的事。

五个问题,最后一次

第 3 章给过这张表,现在你应该能填了。把它填出来,就是你的设计文档第一页:

① 发现:插件放在哪?靠什么文件被认出来?
   ______________________________________________

② 加载:用什么把它变成活对象?能卸载吗?
   ______________________________________________

③ 契约:开哪几个扩展点?ctx 里有什么?谁能声明、谁必须写代码?
   ______________________________________________

④ 隔离:插件死循环,我的程序会怎样?我能接受吗?
   ______________________________________________   ← 唯一事后改不了的

⑤ 演化:下次改 API,怎么让老插件不集体死掉?
   ______________________________________________

全书回顾:十件值得带走的事

  1. 插件系统解决的是「速率差」 —— 你的程序变得慢,世界变得快。不是审美问题,是物理问题。(01)
  2. 五个系统不是五种技术,是同一笔交易的五个报价。看懂一个插件系统,先看它把价钱定在哪。(02)
  3. 先有扩展点,才有插件。第一课在宿主这边,不在插件那边。(04)
  4. 能声明的别用回调。「宿主不执行代码能知道多少」决定了懒加载、安全决策和生态透明度的上限。(05 · 07)
  5. 清单是申报,闸门才是拦截。两者缺一不可。(07 · 17)
  6. 在同一个运行时里造沙箱,是一场你必须次次全对、对方只要对一次的比赛。而最危险的状态是以为自己有沙箱。(14)
  7. 你不能用一个被占着的线程,去检查那个占着它的东西。「给插件加超时」在同进程里是幻觉。(15)
  8. 有效的隔离都是白名单式的 —— 判断标准是「漏掉一个的后果」是全线失守还是少个功能。(16)
  9. 一个好的约束,衡量标准不是「少给了什么」,而是「因此不用再操心什么」。VS Code 不给 DOM,买到了五样东西。(18)
  10. 换代时先死的是那批老老实实写版本范围的人。所以宿主不能只靠 semver。(19)

最后

◆ 回到那句主线

插件系统是一笔关于信任的交易:你把程序的一部分权力,交给一段你没写过、没审过、随时会变的代码。

二十四章讲的每一个机制 —— 扩展点、清单、类加载器、可弃置对象、独立世界、权限闸、semver、签名 —— 都是在给这笔交易定价。

而这笔交易值得做的理由,第 1 章就说完了:你写不完所有代码。一千个漫画站、几百种语言的支持、每个人自己那套工作流 —— 那些东西你永远做不完,但世界上有一千个人愿意各做一点。

门开得对,你就得到了一个你一个人永远造不出来的东西。

现在去开你自己那道门吧。记得先想清楚第 ④ 行 —— 那是唯一一个事后改不了的。

◆ 想继续往下走

读源码的顺序(从易到难):

① Mihon 的 ExtensionLoader.kt —— 一个文件,完整的加载流程,本书第 11 章拆过。读完你能自己写一个 Android 插件加载器。

② VS Code 的 extensionHostProcess.tsextHost*.ts —— 工业级 RPC 桥长什么样。重点看那套代理对象是怎么生成的。

③ Chrome 扩展文档的「独立世界」和 MV3 迁移指南 —— 不是源码,但那份迁移指南本身就是一份「如何破坏兼容性」的教科书(正面和反面都有)。

④ IntelliJ Platform SDK 里关于扩展点和动态插件的部分 —— 一个二十年历史的插件系统积累的全部复杂度,读起来很累,但每一处复杂都有它的理由。

如果只读一个:读 Mihon 那个文件。三百行,五个问题它全答了。

这本书的一句话

你写不完所有代码,所以你要开一道门;门后面的每一个机制,都是在给「让它能做多少事」和「它能害你多深」定价 —— 而这个价,只有你自己知道该定多少。