Skip to content

插件能安装却不能保存:宿主接口与版本兼容 ​

开发桌面应用的插件(扩展、主题),常会遇到这样的情况:

插件安装成功了,设置页也能打开,点了保存,界面提示成功——但设置其实没有生效,重启后一切照旧。

这里面至少有三件不同的事:包管理器接受了依赖范围、宿主提供了对应的能力、宿主接受并持久化了修改。任何一件没做到,“保存”都不算真正完成。

本文取材于一个桌面主题扩展在 2026 年 9—10 月的接口迁移,核对日期为 2026-10-23。只讨论通用的插件工程,不复制项目的配置、素材或宿主的内部实现。

读完这篇你会

  • 分清插件版本、开发依赖版本和宿主兼容范围
  • 把“调用结束”“写入被接受”“当前值已确认”分开验证
  • 理解分层配置下“恢复默认值”的正确含义
  • 避开 semver 预发布版本范围的误读,并核对实际加载的产物

1. 原理:插头、电压和开关 ​

把插件接入宿主,就像把一台电器接上电:

  • 插头能插进插座(包管理器检查依赖范围通过、插件能安装)——不代表电器能工作;
  • 电压匹配(宿主真的提供了插件需要的能力和接口)——不代表电器已经在运行;
  • 按下开关、灯真的亮了(宿主接受了这次修改,并且持久化保存)——这才算真正完成。

很多“装得上、存不住”的问题,都是只验证了第一步,就以为全部完成了。

2. 先把三种版本分开 ​

版本表达什么不能代替什么
插件版本这次插件发布的身份宿主的版本
开发依赖版本编译时使用的类型定义和工具用户实际的运行环境
宿主兼容范围声明允许和哪些宿主版本协作真实的兼容验证

peerDependencies 用来表达和宿主或其他包的兼容关系,不能把“安装检查通过”当成运行验收。npm package.json

3. 坑一:只放宽了兼容范围,没有迁移接口 ​

现象:宿主升级后插件报“不兼容”,于是把 peerDependencies 的范围放宽,安装通过了,但设置页显示为空,或者保存没有任何效果。

原因:宿主的新版本换了一套设置接口。只改兼容声明,并不会自动完成接口的迁移。

修复:近期的修复把开发依赖对齐到新的宿主 SDK,同时放宽了原先过窄的兼容声明;实际代码也一起调整了注入的服务、配置的命名空间和宿主侧的注册方式。

这次迁移从旧的“设置绑定”接口,转向宿主提供的“配置表单”能力:客户端按命名空间获取表单,订阅权威的快照,再根据当前的状态判断能否编辑和保存。整条链要作为一个整体核对:

text
宿主注册配置定义
       ↓
客户端取得对应命名空间
       ↓
加载状态与可写能力
       ↓
草稿 + 基线版本 → 提交修改
       ↓
宿主接受结果 + 权威快照 → 确认保存

只替换一个方法名,可能留下另一端没有注册、命名空间不一致、或者连接只有只读权限的问题。当前的界面分别显示“加载中”“连接能力不足”“保存结果未确认”和“冲突”,而不是一律显示空白的设置。

4. 坑二:Promise 完成了,就显示“保存成功” ​

现象:调用保存方法后 Promise 正常结束,界面显示“已保存”,实际上宿主拒绝了这次修改。

原因:Promise 完成,只代表调用结束了,不代表宿主接受了修改,更不代表当前的值就是你提交的值。

修复:近期的修复会显式检查修改操作的“接受结果”,然后再次读取宿主的快照,确认它处于可以确认的状态、并且值和提交的目标一致,才更新页面的基线。

通用的教学伪代码:

js
const accepted = await form.write(changes, baseRevision);
if (!accepted) throw new Error('WRITE_REJECTED');

const current = form.snapshot();
if (!current.authoritative || !sameValues(current.value, submitted)) {
  throw new Error('WRITE_UNCONFIRMED');
}

这里的 API 名字只是示意。重点是把“调用结束”“写入被接受”“当前值已确认”分开,而不是建议读者复制一个宿主专用的函数。

当前的设置页还区分了“草稿”和“已保存的基线”。其他窗口更新了设置时:本地没有修改,就直接同步;本地有未保存的草稿,就保留草稿并提示冲突,是否重新加载由用户明确选择。这和 编辑会话与本地草稿 中“确认的是哪一份快照”是同一个道理。

5. 坑三:恢复默认值后,以后再也跟不上默认值的变化 ​

现象:用户点了“恢复默认”,看起来一切正常。后来宿主更新了某项的默认值,这个用户的设置却一直停在旧的默认值上。

原因:在分层配置里,用户的值会覆盖宿主或插件的默认值。如果“恢复默认”的实现是“把一组默认常量写进用户设置”,那么用户设置里就多了一份和默认值一样的覆盖值。将来默认值变了,这个覆盖值依然生效,用户被旧值锁住了。

修复:当前的重置操作是删除用户的覆盖键,再确认这些键确实已经不存在了,而不是只比较界面上显示的值是否等于默认值。

这是配置语义的问题:只有宿主确实采用“继承与覆盖”的模型时,删除覆盖才有这个含义,不能推广到所有设置系统。

6. 坑四:以为放宽了范围,就兼容所有预发布版本 ​

现象:兼容范围写的是 >=1.4.0-rc.1 <2.0.0,在宿主的 1.5.0-rc.1 测试版上却提示不兼容。

原因:默认的 node-semver 匹配规则会限制预发布版本:范围里显式包含了某个版本号的 rc,并不表示后续小版本的所有 rc 都会匹配。node-semver 预发布规则

下面是独立的合成示例,统一使用范围 >=1.4.0-rc.1 <2.0.0,结果已在本地用 node-semver 核对过(没有加载目标宿主):

被判断的版本默认的匹配结果
1.4.0-rc.2匹配
1.5.0-rc.1不匹配
1.5.0匹配

规则可以理解为:预发布版本只有在和范围中某个预发布版本的主、次、修订号完全相同时才会被考虑。所以,源码里“放宽了范围”和“覆盖了所有桌面预发布运行时”不能画等号。建议用实际发布过的目标版本逐个测试;如果宿主使用了特殊的匹配参数,也要明确记录,不能默认它开启了预发布兼容选项。

7. 坑五:改了源码,用户加载的还是旧代码 ​

现象:仓库里的代码已经修好了,用户安装新版本后问题依旧。

原因:当前的扩展把宿主入口和客户端入口分别构建,并把部分宿主共享的依赖设为 external;包的入口指向生成后的文件。只改了源代码、没有更新实际的入口产物,就会出现“仓库看起来修好了,用户加载的还是旧代码”。

修复:发布前核对源代码、类型检查、构建结果、导出路径和包内容是否一致,并确认运行时由谁提供 external 依赖。能在开发目录里运行,不等于安装包里包含了全部必要的文件。可以用 npm pack 生成安装包,解开后检查里面实际包含的文件。

这里总结的是现有的构建模式,本次没有实际执行这个扩展的安装或桌面兼容矩阵,也没有重新分发它的素材。

排查思路 ​

  1. 装得上、存不住:按“插头 → 电压 → 开关”的顺序检查:依赖范围、宿主是否提供了对应的能力和命名空间、写入是否被接受、快照里的值是否和提交的一致。
  2. 只在某个宿主版本出问题:用 semver 工具核对这个版本是否真的在兼容范围内,特别注意预发布版本。
  3. 修复后问题依旧:检查用户实际加载的入口文件,确认它是新构建的产物。
  4. 默认值不跟随更新:检查用户配置层里是否残留了覆盖键。

建议验证矩阵 ​

  • [ ] 最低支持版本、常用的稳定版本和目标预发布版本分别加载测试
  • [ ] 宿主和客户端能力缺失时,给出可以理解的“不可用”状态
  • [ ] 修改被拒绝或返回不一致的快照时,不显示保存成功
  • [ ] 两个窗口同时编辑,失败的一方仍然保留草稿
  • [ ] 重置后用户覆盖层消失,而不只是看起来值相同
  • [ ] 实际打包产物中的入口、依赖和源码版本一致

延伸阅读 ​

为复用而记录,为理解而整理。