手把手带你开发一个实用的浏览器插件
插件开发这摊事,我最近又折腾了一遍
说实话,做前端这些年,最烦的不是写页面,也不是兼容性问题,而是搞插件。尤其是那种需要给别人用的通用组件——既要灵活,又不能太重;既得支持配置,还得避免全局污染。最近公司有个需求,要做一个可复用的弹窗插件,接入方能自定义样式、行为,还能通过 CDN 或 npm 引入。于是我又把主流方案翻出来对比了一轮:原生 JS 类封装、UMD 模块、基于 ES Module 的现代方案,还有用 Webpack 打包构建的类库形式。
结论先甩出来:现在如果是独立功能模块,我基本首选 ES Module + Rollup 打包输出多格式的方式。简单、干净、tree-shakable,而且开发体验也好。但也不是所有场景都适用,比如你得支持老浏览器,那 UMD 还是绕不过去。
谁更灵活?谁更省事?
先说说我最讨厌的:纯原生函数式写法。就是那种直接挂到 window 上,一堆 options 传进去,然后 return 一个 init 方法。看着简单,实际维护起来真要命。
function MyModal(options) {
this.options = Object.assign({}, {
title: '提示',
content: '',
onClose: null
}, options);
}
MyModal.prototype.open = function() {
// 创建 DOM
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML =
<div class="modal-header">${this.options.title}</div>
<div class="modal-body">${this.options.content}</div>
<button class="close-btn">关闭</button>
;
document.body.appendChild(modal);
// 绑定事件
const closeBtn = modal.querySelector('.close-btn');
closeBtn.addEventListener('click', () => {
document.body.removeChild(modal);
if (typeof this.options.onClose === 'function') {
this.options.onClose();
}
});
};
这种写法最大的问题是:
- 没有命名空间管理,容易冲突
- 不支持按需引入(当然你也用不上 tree-shaking)
- 调试困难,prototype 链一长串,打断点都不好打
但我以前在项目里这么干过,因为老板说“就一个小功能,别整那么复杂”。结果后来三四个地方都要改样式、加动画、控制显示逻辑……改着改着就成了意大利面条代码。
UMD:兼容时代的遗物,但现在还得会
如果你的产品要卖给传统企业客户,他们系统可能还在用 IE11,那你绕不开 UMD。这个模式的核心是:一套代码,三种引入方式都能跑——script 标签、CommonJS、AMD。
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ?
module.exports = factory() :
typeof define === 'function' && define.amd ?
define(factory) :
(global.MyModal = factory());
}(this, (function () {
// 插件主体逻辑
function MyModal(options) {
this.options = { ... };
}
MyModal.prototype.open = function() { ... };
return MyModal;
})));
这段代码看着就累,对吧?但它确实解决了跨环境的问题。我自己用 Webpack 配合 libraryTarget: "umd" 自动生成过,省了不少事。但也踩过坑:有一次 build 出来文件 global 对象挂错了,导致 CDN 引入时 MyModal 是 undefined。折腾了半天发现是 output.library 设置少了引号。
还有一个坑:UMD 包通常不会做 tree-shaking,哪怕你只用了一个方法,也得加载整个文件。对于现代应用来说,这是个硬伤。
我的心头好:ES Module + Rollup
我现在写插件,只要不强制支持 IE,一律上 ES Module。配合 Rollup 打包,可以同时输出 esm、cjs、iife 多种格式,开发爽,接入方也方便。
// src/index.js
export class Modal {
constructor(options) {
this.options = {
title: '默认标题',
...options
};
}
open() {
const el = document.createElement('div');
el.innerHTML = <div class="modal">${this.options.title}</div>;
document.body.appendChild(el);
this.element = el;
this.options.onOpen?.();
}
close() {
this.element?.remove();
this.options.onClose?.();
}
}
// 可选:导出工厂函数
export function createModal(opts) {
return new Modal(opts);
}
// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
export default [
// ESM
{
input: 'src/index.js',
output: { file: 'dist/modal.esm.js', format: 'esm' },
plugins: [resolve()]
},
// IIFE(用于 CDN)
{
input: 'src/index.js',
output: { file: 'dist/modal.min.js', format: 'iife', name: 'ModalLib' },
plugins: [resolve(), commonjs()]
}
];
这样打包完,别人可以用:
- npm 引入:
import { Modal } from 'my-modal' - CDN 直接用:
<script src="https://jztheme.com/dist/modal.min.js"></script>,然后用new ModalLib.Modal()
关键优势:
- 天然支持 tree-shaking(只要用户用的是现代打包工具)
- 代码结构清晰,class 写法更符合现代 JS 习惯
- Rollup 打出来的包比 Webpack 更轻量,没那么多 runtime 注入
这里注意我踩过一次坑:一开始用了 Babel 编译装饰器语法,结果 Rollup 没配好 plugin,导致 export 出来的东西是 default,而不是具名导出。调试了快两个小时才发现是 .babelrc 和 @babel/plugin-proposal-decorators 冲突了。后来干脆不用装饰器了,省心。
Webpack 行不行?看场景,我一般选 Rollup
有人问我为啥不用 Webpack 做类库打包。我的回答是:Webpack 是为应用设计的,不是为库设计的。它默认会注入很多运行时代码,比如 module loader、异步 chunk 加载逻辑,这些在插件里完全是累赘。
当然,如果你的插件本身就很重,依赖一大堆第三方库(比如用了 lodash、moment 等),那 Webpack 的 code splitting 和 externals 配置反而更有优势。比如你可以把 react 排除在外:
// webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
library: 'MyModal',
libraryTarget: 'umd',
filename: 'modal.umd.js'
},
externals: {
react: {
commonjs: 'react',
commonjs2: 'react',
amd: 'react',
root: 'React'
}
}
};
这种情况下 Webpack 更可控。但我自己的小工具插件,基本不用它。
我的选型逻辑
我现在判断一个插件怎么打包,主要看三个点:
- 目标用户是谁? 如果是内部项目、现代化架构,直接上 ESM;如果是卖给传统客户的套件,必须上 UMD + iife 双输出。
- 体积敏感吗? 越小越好就用 Rollup,不怕大就用 Webpack。
- 要不要支持 script 引入? 要的话一定生成 iife 格式,并设置好 global name。
比如我上周写的那个表单校验插件,只有 3KB,完全不需要兼容 IE,我就只出了 esm 和 iife 两个版本,文档里写清楚怎么用 CDN 怎么用 npm。结果接入组同事反馈说“终于不用 import 整个 utils 包了”,挺有成就感的。
但也有妥协的时候。之前给政府项目做地图插件,要求必须支持 IE9,最后还是上了 UMD + babel-polyfill + Webpack 全家桶。打包出来 80KB,我自己都想删了重写。但没办法,现实就是这样。
总结一下
插件开发没有银弹。我比较喜欢用 ES Module + Rollup 的组合,开发顺手、包体小、结构清。但在真实世界中,你往往得低头向旧环境妥协。UMD 不香,但你得会;Webpack 很重,但有时候不得不靠它处理复杂依赖。
以上是我的对比总结,有不同看法欢迎评论区交流。这个领域也没有标准答案,都是边踩坑边进化。下次再聊聊怎么给插件加 TypeScript 支持,那又是另一个血泪史了。

暂无评论