Webpack中的插件流程是怎么实现的?

一、前言

最近在学习许多框架的底层插件化实现,如ice.jsumi.js,在这些框架中,我们都可以在介绍中看到支持可插拔灵活的插件系统,那插件化的本质是怎么实现的?

此时不如回归本质,在我们学习Webpack的时候,都遇到过面试题------Webpack插件是怎么实现的?当我们思考插件化的实现,不如回到Webpack,从源码看看在前端工程化领域看看插件化是怎么设计的

二、插件化

插件化底层涉及两个核心概念。

  1. 钩子(生命周期),在每个生命周期阶段都可以触发很多插件,比如webpack开始构建、生成html文件、完成构建等等。
  2. 插件(每个钩子阶段做的事情),比如在生成html文件后再二次修改处理一些文件,这是插件做的事情。

那钩子和插件如何结合起来呢?

使用发布订阅模式

这可以让钩子插件充分的解耦。

一个钩子可以触发多个插件(运行时代码);

一个插件可以在多个钩子阶段处理(日志,在每个钩子打日志);

webpack底层依赖tapable库,这个库就是提供增强版本的发布订阅模式

webpack在初始化开始构建之前,会发布所有的生命周期钩子、初始化所有的插件

并且将钩子插件关联起来。之后,webpack在主流程中定期的触发钩子就行。

就像这样:

ts 复制代码
const Compiler = require("./Compiler");
const WebpackOptionsApply = require("./WebpackOptionsApply");

const webpack = (options, callback) => {
  // 1. 创建 Compiler 实例
  const compiler = new Compiler(options.context, options);

  // 2. 把所有用户配置的 plugins 应用到 compiler 上
  if (options.plugins && Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      plugin.apply(compiler);  // ← 注意这里!逐个执行插件的 apply
    }
  }

  // 3. 执行 Webpack 自带的一些内部插件
  new WebpackOptionsApply().process(options, compiler);

  if (callback) {
    compiler.run(callback); // 执行构建流程(会触发 hooks)
  }

  return compiler;
};

这就是插件化在webpack中的原理。

下面我们从webpack源码来看看它是如何实现的。

三、源码入手

我们从webpack源码开始,看下插件化是如何实现的。

webpack主流程代码在webpack/lib/webpack.js中。

在这里会做初始化的事情,比如Compiler初始化、所有插件初始化。

  1. 这里首先初始化了Compiler实例,后续的所有生命周期钩子都会在里面初始化。
  2. 然后读取了所有的插件,执行了插件函数,并将Compiler实例作为参数传递,这一步成功将Compilerthis指向到了所有插件,让插件的函数绑定在了钩子上。

而这些Webpack插件我们都知道它的写法类型是固定的。

  1. 是个class
  2. 必须有apply函数。

看到这里是不是秒懂了?apply函数就是让我们写的pluginwebpack建立关系。绑定在生命周期钩子上。

这里给一个Plugin代码示例:

ts 复制代码
class MyPlugin {
  constructor(options) {
    this.options = options;
  }

  apply(compiler) {
    // 订阅 Compiler 阶段
    compiler.hooks.run.tap('MyPlugin', (compilerInstance) => {
      console.log('[MyPlugin] 开始运行,options:', this.options);
    });

    // 订阅 Compilation 阶段
    compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
      compilation.hooks.processAssets.tap(
        {
          name: 'MyPlugin',
          stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS
        },
        (assets) => {
          console.log('[MyPlugin] 当前构建的资源:', Object.keys(assets));
          assets['extra.txt'] = {
            source: () => '这是 MyPlugin 生成的文件',
            size: () => Buffer.byteLength('这是 MyPlugin 生成的文件')
          };
        }
      );
    });

    // 订阅 done 阶段
    compiler.hooks.done.tap('MyPlugin', (stats) => {
      console.log('[MyPlugin] 构建完成,耗时:', stats.endTime - stats.startTime, 'ms');
    });
  }
}

使用这个插件也很简单。

webpack.config.js中这样配置下就行:

ts 复制代码
// webpack.config.js
const path = require('path');
const MyPlugin = require('./plugins/MyPlugin');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true
  },
  plugins: [
    new MyPlugin({ customOption: 'abc' })
  ]
};

回到webpack源码。

Compiler 本身并不关心插件注册,它只是提供 lifecycle hooks(事件源)。

然后走到webpack/lib/compiler.js中,在类初始化中基于tapable创建了webpack的所有生命周期。

OK,到这里插件化的两大要点已经结束。插件生命周期Hook都已经读完了,接下来Webpack会做啥?

执行构建 -> 在构建中不断地触发各种Hook进而触发来自不同插件的逻辑。

这不就是插件化的原理么?

Webpack中体现。

一句话概括。

Webpack从源码到构建出产物这件事情注入很多的阶段,并在每个阶段触发指定的插件。

Tapable是怎么实现的?我们同样去看下源码。

找到tapable/lib/Hook.js,这是所有Hook的子类,我们看下子类怎么监听的就行。

这是个抽象类,本质也是基于发布订阅模式的实现。

通过tap来保存订阅列表。

通过call来触发订阅回调。

当在webpack中订阅的时候

ts 复制代码
compiler.hooks.emit.tapAsync('MyPlugin', (compilation, cb) => { ... });

实际上就是走到了这里:

ts 复制代码
tapAsync() → _tap("async", options, fn)

然后保存到this.taps中。

而当webpack发布事件的时候:

ts 复制代码
compiler.hooks.emit.callAsync(compilation, done);

tapable会遍历taps按注册顺序依次触发回调,最终实现webpack插件的代码逻辑。

这就是Webpack插件化的原理,也是市面上很多应用框架插件化的底层原理。

我们在umiice代码库中都可以搜到tapable,而使用姿势和Webpack是差不多的。

四、结尾

插件化是前端应用框架的基石,也是设计模式精妙的应用体验。

看到这里,相信你对Webpack原理更深了一步,同时也更加熟悉许多插件的设计理念了。

如果文章对你有帮助,欢迎在评论区探讨。

相关推荐
残冬醉离殇4 小时前
原来dom树就是AST!!!
前端
~无忧花开~4 小时前
掌握Axios:前端HTTP请求全攻略
开发语言·前端·学习·js
gfdgd xi4 小时前
GXDE For deepin 25:deepin25 能用上 GXDE 了!
linux·运维·python·ubuntu·架构·bug·deepin
橙某人5 小时前
Vue3 + Pinia 移动端Web应用:页面缓存策略解决方案💡
前端·javascript·vue.js
小Pawn爷5 小时前
构建Django的Web镜像
前端·python·docker·django
Sailing5 小时前
🚀🚀 从前端到AI Agent开发者,只差这一篇入门指南
前端·后端·ai编程
草帽lufei5 小时前
轻松上手WSL安装与使用
linux·前端·操作系统
TimelessHaze5 小时前
🚀 一文吃透 React 性能优化三剑客:useCallback、useMemo 与 React.memo
前端·javascript·react.js
长存祈月心5 小时前
Rust 迭代器适配器
java·服务器·前端