手把手带你开发一个实用的浏览器插件

南宫焕焕 前端 阅读 2,080
赞 7 收藏
二维码
手机扫码查看
反馈

插件开发这摊事,我最近又折腾了一遍

说实话,做前端这些年,最烦的不是写页面,也不是兼容性问题,而是搞插件。尤其是那种需要给别人用的通用组件——既要灵活,又不能太重;既得支持配置,还得避免全局污染。最近公司有个需求,要做一个可复用的弹窗插件,接入方能自定义样式、行为,还能通过 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 更可控。但我自己的小工具插件,基本不用它。

我的选型逻辑

我现在判断一个插件怎么打包,主要看三个点:

  1. 目标用户是谁? 如果是内部项目、现代化架构,直接上 ESM;如果是卖给传统客户的套件,必须上 UMD + iife 双输出。
  2. 体积敏感吗? 越小越好就用 Rollup,不怕大就用 Webpack。
  3. 要不要支持 script 引入? 要的话一定生成 iife 格式,并设置好 global name。

比如我上周写的那个表单校验插件,只有 3KB,完全不需要兼容 IE,我就只出了 esm 和 iife 两个版本,文档里写清楚怎么用 CDN 怎么用 npm。结果接入组同事反馈说“终于不用 import 整个 utils 包了”,挺有成就感的。

但也有妥协的时候。之前给政府项目做地图插件,要求必须支持 IE9,最后还是上了 UMD + babel-polyfill + Webpack 全家桶。打包出来 80KB,我自己都想删了重写。但没办法,现实就是这样。

总结一下

插件开发没有银弹。我比较喜欢用 ES Module + Rollup 的组合,开发顺手、包体小、结构清。但在真实世界中,你往往得低头向旧环境妥协。UMD 不香,但你得会;Webpack 很重,但有时候不得不靠它处理复杂依赖。

以上是我的对比总结,有不同看法欢迎评论区交流。这个领域也没有标准答案,都是边踩坑边进化。下次再聊聊怎么给插件加 TypeScript 支持,那又是另一个血泪史了。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论