夜雨聆风学习资料网

ARTICLE · 1203616

激活与运行时:插件从"装进去"到"跑起来"的最后一公里

激活与运行时:插件从"装进去"到"跑起来"的最后一公里

装配完 ≠ 跑起来。激活策略与通信机制,决定了一个插件系统是秒开还是卡死。

核心命题:前几篇,宿主已经读懂了插件的清单(06)、把它装配进了系统(07)。但"装进去"不等于"跑起来"——一份躺在 node_modules 里的 npm 包、一个注册在 plugin.xml 里的扩展点、一个被扫进 wp-content/plugins 的 PHP 文件,在没人触发它之前,都只是静态元数据。插件系统真正的临门一脚,是激活(Activation)与运行时(Runtime):何时把它从"沉睡"唤醒?唤醒后宿主与插件怎么对话?怎么保证它干活时不卡死主线程、崩了也不拖垮全家?这一篇,我们拆开这条"最后一公里"。


引子:从"躺在清单里"到"在做事"

想象一座已经建好的物流立交桥(07 篇),货车(插件)都停进了仓库(注册表)。但仓库里的货车不会自己上路——得有调度指令,它才点火、上路、运货。

插件系统也一样。激活就是那道"点火指令",运行时就是"上路后怎么跑、怎么跟调度中心通话"。这一步的策略,直接写在启动性能和用户体验的账单上:激活太早,启动卡成狗;激活太晚,用户点一下等三秒;通信太重,主线程被拖死;隔离太弱,一个插件崩,全家陪葬。

本篇,我们横评四家代表性运行时,看它们怎么回答两个核心问题:何时醒?怎么聊?

激活策略光谱:四种"何时醒来"的设计


一、激活策略:四种"何时醒来"的设计

激活回答的是"插件代码什么时候第一次被执行"。四种范式构成一条从"用户显式触发"到"声明即永驻"的光谱。

① 显式触发(按需,最省)——代表:VS Code。 VS Code 的 activationEvents 让扩展声明"在什么事件发生时才激活"。最常见的是 onCommand:helloworld.helloWorld——用户第一次运行该命令,扩展的 activate() 才执行。编辑器启动时不会因为装了 50 个扩展就跑 50 份代码。这正是 06 篇"声明与执行分离"在运行时侧的兑现:清单里写好了"触发器",宿主按兵不动,等事件真的发生才点火。(注:自 VS Code 1.74.0 起,在 commands 里声明的命令被调用时会自动激活,无需显式 onCommand,向后兼容。)

② 事件驱动 · 瞬时(常驻→瞬时)——代表:Chrome Service Worker(MV3)。 这是近几年最戏剧性的激活变革。MV2 时代,扩展跑一个持久后台页(persistent background page)——一个常驻内存的 HTML 文档,即使扩展啥也不干也一直耗着 CPU 和内存。MV3 把它换成事件驱动的 Service Worker:Chrome 在事件来时把它拉起,空闲即终止,不占 DOM、不保全局状态。代价是开发者必须改写——状态不能放全局变量(改用 chrome.storage)、计时器不能靠 setTimeout(改用 chrome.alarms)、需要 DOM 时借道 Offscreen API(一个隐藏文档)。一句话:Chrome 用"瞬时激活"换资源与可审计性,代价是开发心智模型重构。

③ 用户启动即运行 · 显式结束——代表:Figma。 Figma 插件没有"后台常驻"概念:用户在编辑器里从插件菜单点一下,插件才在主线程沙箱里启动;干完活必须调 figma.closePlugin() 通知宿主结束,否则用户会一直看到"Running [插件名]"。运行期间用户还能随时取消,宿主自动 closePlugin()。这是一种"前台、短命、用户主权"的激活模型——没有懒加载的缓冲,但也没有僵尸进程。

④ 声明即常驻 · 可逆热重载——代表:Koishi(基于 Cordis 元框架)。 Koishi 走另一条路:插件通过 ctx.plugin() 注册,宿主立刻为它派生一个子 context并立刻执行 apply() 激活,没有"等事件"这一说。但它的杀手锏是可逆性——fork.dispose() 停用插件时,框架自动逆回该插件注册过的所有副作用(命令、监听器、中间件、定时器、子插件),连"热重载"(文件一改就卸了重装)都靠这套机制。激活在这里不是"点火一次",而是"挂在 Context 上的副作用集合",卸载即精准撤销。

小结:激活,本质是"把静态元数据变成可执行代码"的时机选择。越靠左(显式/事件驱动),启动越轻、代价是首次触发有延迟;越靠右(声明常驻),响应越快、代价是常驻开销与清理责任。


二、运行时通信机制:唤醒之后,宿主与插件怎么对话?

激活解决"何时跑",通信解决"跑起来后怎么协作"。四种主流机制,各有取舍。

① 直接 API 调用(强类型 RPC)——代表:VS Code API、IntelliJ Services。 VS Code 扩展直接 import * as vscode 调用一组显式版本化的 JS API;IntelliJ 插件通过 <extensions> 注册 Service 后,用宿主注入的 API 调能力。同一进程(或跨进程经 RPC 桥)内调用,类型安全、IDE 补全友好,但耦合紧——插件深度依赖宿主 API 面。

② 事件总线(发布-订阅)——代表:Koishi ctx.on/middleware、Eclipse 事件系统。 Koishi 的 Context 内置事件总线:插件 ctx.on('message', ...) 订阅消息、ctx.middleware(...) 插入处理链。Eclipse 同样用事件让插件解耦协作。好处是彻底解耦——发布者不知订阅者是谁;代价是调试时调用链不再是一条直路。

③ 消息传递(序列化隔离)——代表:Figma postMessage、Chrome runtime.messaging。 Figma 的"双世界"最典型:主线程沙箱(能碰文档树)和 iframe(能碰浏览器 API)互相不能直访,全靠 postMessage 序列化传消息。Chrome 扩展的各上下文(popup / worker / content script)之间也靠 runtime.sendMessage。隔离最强,但每次对话都要序列化、跨上下文,有成本。

④ 声明式贡献聚合(API 网关式)——代表:VS Code contributes、IntelliJ <extensionPoints>。 这一类不算"运行时调用",而是"启动期由宿主统一聚合插件声明的能力"——命令、菜单、配置页,宿主在启动时扫一遍清单就拼好了。它是 06 篇"清单驱动"的运行时兑现:插件不必主动调宿主,宿主按声明把能力摆上货架。

通信机制选型:四种宿主-插件对话方式


三、不阻塞主线程:异步优先是底线

大纲把"如何避免阻塞主线程"列为 08 的核心问题之一,因为它直接决定"卡不卡"。

  • •

    VS Code / IntelliJ:扩展代码跑在独立的 Extension Host(VS Code)/ 类加载器沙箱(IntelliJ),理论上不碰编辑器主线程;但重计算若不 await、不 yield,仍会卡住宿主响应——所以官方反复强调异步优先。

  • •

    Chrome Service Worker:worker 本身在后台线程,但 MV3 禁止同步阻塞 API(如 XMLHttpRequest),强制 fetch + async/await。

  • •

    Figma 主线程沙箱:这是最苛刻的——Figma 故意让插件跑在编辑器主线程(为了访问文档树的性能),所以插件代码必须又短又快,一旦长时间同步计算,整个编辑器 UI 就卡住。这是"把隔离让位于性能"的典型权衡。

  • •

    Koishi:基于 Node.js 事件循环,天然异步;单插件同步阻塞会拖慢整 bot,因此生态约定重活丢给 ctx.setTimeout / 异步任务。

一句话:异步优先不是风格,是插件系统不卡死主线程的工程底线。隔离(09 篇深聊)是把"崩"和"卡"关进笼子的另一道闸门。


四、错误传播与隔离:一个插件崩了,全家陪葬吗?

激活与运行时还决定了"崩了怎么办",这是 13 篇可观测性的伏笔,但这里先立骨架:

  • •

    VS Code:扩展在独立 Extension Host 进程,单个扩展未捕获异常不会拖垮编辑器主进程——进程级隔离天然兜底。

  • •

    Figma:用户可随时取消运行中的插件,宿主立即 closePlugin();异常被宿主捕获,不会让编辑器崩。

  • •

    Koishi:框架层用 try/catch 包裹事件分发,单插件抛错默认不中断整个 bot;配合 dispose 可逆机制,出错插件可被干净卸下。

  • •

    反面教材:老式同进程无隔离架构(典型如早期某些 CMS 插件、无沙箱的脚本加载),一个插件 Fatal Error 直接白屏全站——这正是 09 篇"隔离模型"要治的病。


五、结论/产出:激活模型对比表 + 通信机制选型指南

激活模型对比表(四种范式):

通信机制选型指南:

  • •

    要类型安全、强耦合、高性能 → 直接 API/RPC(VS Code、IntelliJ)。

  • •

    要解耦、可扩展、插件互不认识 → 事件总线(Koishi、Eclipse)。

  • •

    要强隔离、跨信任边界 → 消息传递(Figma、Chrome)。

  • •

    要启动期静态聚合能力、零运行时调用 → 声明式贡献(VS Code contributes、IntelliJ 扩展点)。


六、衔接 09:运行时划定了信任边界

本篇讲"插件跑起来",但"跑在哪、能被信任到什么程度",取决于隔离模型——09 篇深聊:VS Code 的独立进程、Figma 的 iframe 沙箱、Chrome 的多进程、Wasm 的语言级沙箱、OSGi 的类加载器。运行时决定了"插件能调用什么",隔离决定了"插件能搞坏什么"。两者合起来,才是 03 篇"契约 + 生命周期 + 治理"笼子的下半截。


附:本文关键史实与日期口径说明

  • •

    VS Code activationEvents 自动激活:自 VS Code 1.74.0(2022-11 发布)起,commands 中声明的命令被调用时自动激活扩展,仍向后兼容显式 onCommand

  • •

    Chrome MV3 Service Worker:Chrome 88(2021-01)起 MV3 可用;持久后台页被事件驱动 Service Worker 取代,background.service_worker 字段接受单字符串;chrome.alarms 替 setTimeout、Offscreen API 补 DOM;Web Store 2024 停收 V2、浏览器 2025 起停跑 V2

  • •

    Figma 主线程沙箱 + closePlugin():来源 Figma 官方《How Plugins Run》

  • •

    Koishi / Cordis 可逆插件:ctx.plugin() 注册→子 Context→apply() 激活→fork.dispose() 逆副作用→热重载(来源:Koishi 官方生命周期文档

#激活与运行时 #懒加载 #消息传递

相关学习资料