手把手带你开发一个实用的浏览器插件
又来折腾插件架构了
最近接了个需求,要给一个内部工具系统做插件化改造。老系统是直接写死功能的,现在要让第三方能挂载自己的模块进来,类似 Chrome 扩展那种感觉。于是我又开始翻各种插件方案,对比了一圈主流做法:微前端、动态 import + 沙箱、还有直接用 iframe 嵌套。本来以为选型很快就能定,结果还是踩了不少坑。
我一开始想上微前端,毕竟听着高大上,qiankun 也挺火的。但搞了两天发现,这玩意儿在我们这种轻量级场景里有点杀鸡用牛刀。而且团队没人熟这个,后期维护成本太高。最后我退回来重新评估,才意识到:不是所有插件系统都得搞得那么复杂。
谁更灵活?谁更省事?
先说结论吧:如果是中小型项目,追求快速落地、低维护成本,我建议直接用 动态 import + 入口函数 的方式;如果插件之间隔离要求高、涉及不同技术栈共存,那可以考虑微前端;而如果你压根不想管 JS 冲突、样式污染这些问题,iframe 真的是最省心的——虽然它看起来像“过时方案”,但有时候真香。
下面一个个说,结合我自己踩过的坑。
方案一:iframe —— 老派但稳如老狗
说实话,我以前看不起 iframe,觉得它是上个时代的产物。但现在回头看,它的隔离性是真的强。每个插件就是一个独立页面,CSS、JS 完全不干扰主应用,通信走 postMessage 就行。
<iframe
src="https://jztheme.com/plugins/calendar"
style="width: 100%; height: 500px; border: none;"
></iframe>
主应用只需要把插件 URL 配置好,扔进 iframe 就完事了。插件自己负责渲染和逻辑,主应用只管容器布局和权限控制。
缺点也很明显:通信麻烦,postMessage 写起来啰嗦;跨域限制多;SEO 不友好(不过我们是后台系统无所谓);还有就是加载慢一点,毕竟是完整页面。
但我发现,只要约定好 message 的格式,比如统一用 { type, payload } 结构,封装个简单的 SDK,开发体验也能接受。
// 插件内发送消息
window.parent.postMessage({
type: 'PLUGIN_READY',
payload: { name: 'calendar' }
}, 'https://main-app.com');
// 主应用监听
window.addEventListener('message', (event) => {
if (event.origin !== 'https://main-app.com') return;
console.log('收到插件消息:', event.data);
});
总结一下:iframe 最适合那些对性能要求不高、但强调稳定和隔离的场景。比如后台系统的报表插件、审批流嵌入页之类的。别小看它,简单粗暴反而不容易出问题。
方案二:动态 import + 沙箱 —— 我目前的首选
这是我现在最喜欢的方案。插件是一个独立的 JS 文件,暴露一个 install 方法,主应用通过 import() 动态加载,然后调用它的生命周期钩子。
核心代码就这几行:
// 加载并运行插件
async function loadPlugin(url) {
try {
const module = await import(url);
if (typeof module.install === 'function') {
module.install({ registerWidget, setConfig });
}
} catch (err) {
console.error('插件加载失败:', err);
}
}
插件代码长这样:
// https://jztheme.com/plugins/weather.js
export function install({ registerWidget }) {
registerWidget({
id: 'weather',
name: '天气卡片',
component: () => import('./WeatherWidget.vue')
});
console.log('天气插件已注册');
}
你看,非常干净。主应用提供 API(比如 registerWidget),插件调用即可。JS 执行环境虽然共享,但我们可以通过规范约束来避免污染。比如禁止插件直接操作 window,全局变量必须挂到特定命名空间下。
这里注意我踩过好几次坑:某些插件用了 top.location 判断,结果在 iframe 或嵌套路由里炸了。解决办法是在插件构建时配置 webpack 的 output.globalObject = ‘window’,避免生成的代码使用 top。
另一个问题是样式冲突。我们的做法是要求插件使用 CSS Modules 或 scoped style,同时加一个前缀类名,比如 .plugin-weather-*,这样基本不会影响主应用。
这个方案最大的好处是轻量、灵活、加载快。而且你可以配合 vite/webpack 的 code split,按需加载插件资源。调试也方便,F12 就能看到源码。
方案三:微前端(qiankun)—— 功能强,但也重
我承认 qiankun 很强大,沙箱、样式隔离、资源预加载都帮你做了。但它带来的复杂度也不容忽视。
你得搭两个项目:主应用用 registerMicroApp 注册,子应用要改 entry、导出生命周期函数。稍微配置错一点,比如 publicPath 没设对,就会出现静态资源 404。
// 主应用注册
registerMicroApp({
name: 'calendar-plugin',
entry: 'https://jztheme.com/plugins/calendar',
container: '#plugin-container',
});
// 子应用导出
export async function bootstrap() {}
export async function mount() {
console.log('日历插件挂载');
}
export async function unmount() {}
而且 qiankun 的沙箱机制基于 proxy 实现,在 IE 下直接跪。如果你还要兼容老浏览器,就得引入一堆 polyfill,体积立马膨胀。
另外我发现一个问题:当多个插件同时激活时,qiankun 的内存占用会上升明显。卸载后 DOM 清理不彻底,偶尔会残留事件监听。这个问题折腾了半天才发现是某个插件用了 setInterval 没在 unmount 时清理。
所以我的结论是:微前端适合大型平台级产品,比如钉钉开放平台那种,需要支持完全独立开发、部署、技术栈自由的场景。对于我们这种几十个插件的小系统,真的没必要。
我的选型逻辑
我现在是这么分的:
- 插件少、功能简单、团队小 → 用 动态 import
- 插件来自外部合作方、安全隔离要求高 → 上 iframe
- 需要支持 React/Vue/Angular 混跑、长期演进的大平台 → 考虑 微前端
别一听“插件系统”就觉得非得上高大上的方案。很多时候,最简单的才是最可靠的。我现在宁愿花时间写文档、定规范,也不想去搞一堆 runtime 的东西。
顺便提一句,不管哪种方案,一定要做插件的版本管理和降级机制。我们之前上线一个插件导致主应用白屏,因为插件抛了个未捕获异常。后来加上 try-catch 和错误上报,才算稳住。
以上是我的对比总结,有不同看法欢迎评论区交流
这个技巧的拓展用法还有很多,比如结合 CDN 缓存策略、插件市场 UI、权限校验等。后续会继续分享这类实战经验。目前这套方案已经在线上跑了三个多月,虽然改完后仍有一两个小问题(比如热更新时旧模块没卸载干净),但无大碍。最重要的是,团队成员都能快速上手,这才是最关键的。

暂无评论