Webpack的Plugin看这一篇就够了

Webpack 插件Plugin是一个 JavaScript 对象,它可以通过 Webpack 的插件系统与编译过程进行交互。插件通过订阅特定的钩子(hooks)来执行自定义的逻辑,从而影响构建流程、修改资源、添加额外的功能等。

每个插件都需要实现一个 apply 方法,该方法接收一个 compiler 参数,代表当前的编译器对象。通过 compiler 对象,插件可以访问和操作编译过程中的各种数据和配置。

开发一个简单的 Webpack 插件

让我们从一个简单的示例开始,开发一个 Webpack 插件,它会在构建结束时输出一条提示信息。

1. 首先,创建一个名为 CustomPlugin 的文件,并编写如下代码:

class CustomPlugin {
  apply(compiler) {
    compiler.hooks.done.tap('CustomPlugin', () => {
      console.log('Build process completed!');
    });
  }
}

module.exports = CustomPlugin;

在这个示例中,我们创建了一个名为 CustomPlugin 的插件类,并实现了 apply 方法。在 apply 方法中,我们通过 compiler.hooks.done.tap 方法订阅了 done 钩子,并在构建完成时输出一条提示信息。

2. 接下来,在项目的 Webpack 配置文件中使用该插件。

假设 Webpack 配置文件为 webpack.config.js,我们可以进行如下配置:

const CustomPlugin = require('./CustomPlugin');

module.exports = {
  // ...其他配置项
  plugins: [
    new CustomPlugin()
  ]
};

通过以上配置,我们将 CustomPlugin 实例添加到了 Webpack 配置的 plugins 数组中,使其成为一个生效的插件。

常见应用场景

Webpack 插件的应用场景非常广泛,可以根据具体需求编写各种功能丰富的插件。以下是一些常见的插件应用场景:

  1. 修改资源:通过订阅相应的钩子,插件可以访问和修改编译过程中的资源,例如替换资源内容、添加额外的打包文件等。
class ModifyAssetPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync('ModifyAssetPlugin', (compilation, callback) => {
      // 获取生成的资源列表
      const assets = compilation.assets;

      // 遍历资源列表并修改内容
      for (const assetName in assets) {
        if (assetName.endsWith('.js')) {
          const asset = assets[assetName];
          const modifiedSource = modifySource(asset.source());
          asset.source = () => modifiedSource;
        }
      }

      callback();
    });
  }
}

在这个示例中,ModifyAssetPlugin 插件订阅了 emit 钩子,并在构建完成时获取生成的资源列表。然后,它遍历资源列表并对 JavaScript 文件进行修改,通过调用 asset.source() 获取原始资源内容,然后对其进行修改,最后将修改后的内容赋值回 asset.source。

  1. 优化:插件可以实现各种优化策略,例如代码压缩、文件合并、图片优化等,以提升项目的性能和加载速度。
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  // ...其他配置项
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin()
    ]
  }
};

在这个示例中,我们使用了 TerserPlugin 插件来进行代码的压缩和混淆。通过将 TerserPlugin 实例添加到 Webpack 配置的 optimization.minimizer 数组中,可以启用代码压缩功能。

  1. 注入全局变量:插件可以向打包后的代码中注入全局变量,以便在应用程序中直接访问这些变量,例如将环境配置注入到代码中。
const webpack = require('webpack');

module.exports = {
  // ...其他配置项
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production')
    })
  ]
};

在这个示例中,我们使用了 webpack.DefinePlugin 插件来注入全局变量 process.env.NODE_ENV,并将其值设置为 'production'。通过这种方式,可以在打包后的代码中直接访问该全局变量。

  1. 资源管理:通过插件,可以将构建生成的文件进行复制、移动、删除等操作,以便更好地管理构建产物。
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
  // ...其他配置项
  plugins: [
    new CopyWebpackPlugin({
      patterns: [
        { from: 'src/assets', to: 'assets' }
      ]
    })
  ]
};

在这个示例中,我们使用了 CopyWebpackPlugin 插件来复制静态资源文件。通过配置 CopyWebpackPlugin 实例的 patterns 属性,我们可以指定要复制的文件来源和目标路径。

  1. 自定义模板:插件可以根据自定义的模板生成特定类型的文件,例如生成 HTML 文件、生成样式文件等。
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  // ...其他配置项
  plugins: [
    new HtmlWebpackPlugin({
      template: 'src/index.html',
      filename: 'index.html'
    })
  ]
};

在这个示例中,我们使用了 HtmlWebpackPlugin 插件来生成 HTML 文件。通过配置 HtmlWebpackPlugin 实例的 template 属性,我们可以指定 HTML 模板文件的路径,然后通过 filename 属性指定生成的 HTML 文件的名称。

这些示例代码只是展示了常见应用场景的一部分,使用的插件也只是其中的一些示例。还有其它应用场景,像生成Source Map文件,多语言国际化,提取CSS文件,清理输出目录等。

插件的触发时机

在 Webpack 的源码中,当调用 webpack 命令或使用 Node.js API 运行 Webpack 编译时,Webpack 会创建一个 Compiler 实例。Compiler 是一个编译器对象,它负责管理整个编译过程,并在适当的时机调用插件的 apply 方法。

Webpack 在启动编译过程时,会创建 Compiler 实例,并通过调用 Compiler 构造函数来初始化。在 Compiler 的构造函数中,会调用 this.hooks 方法来创建各个钩子(hooks),每个钩子都是一个 Hook 类型的实例。

class Compiler {
  constructor() {
    // ...

    this.hooks = Object.freeze({
      /** @type {SyncHook<[]>} */
      initialize: new SyncHook([]),

      /** @type {SyncBailHook<[Compilation], boolean | undefined>} */
      shouldEmit: new SyncBailHook(["compilation"]),
      /** @type {AsyncSeriesHook<[Stats]>} */
      done: new AsyncSeriesHook(["stats"]),
      /** @type {AsyncSeriesHook<[Compiler]>} */
      run: new AsyncSeriesHook(["compiler"]),
      /** @type {AsyncSeriesHook<[Compilation]>} */
      emit: new AsyncSeriesHook(["compilation"])
      // ... 其他钩子
    })

    // ...
  }
}

Compiler 实例创建后,Webpack 会遍历安装的插件列表,并针对每个插件调用其 apply 方法。

const createCompiler = rawOptions => {
  //...
    const compiler = new Compiler(options);
  // ...
    if (Array.isArray(options.plugins)) {
        for (const plugin of options.plugins) {
            if (typeof plugin === "function") {
                plugin.call(compiler, compiler);
            } else if (plugin) {
                plugin.apply(compiler);
            }
        }
    }
    // ...
    return compiler;
};

总结起来,Webpack 在启动编译过程时创建了一个 Compiler 实例,并在适当的时机调用每个插件的 apply 方法,将 Compiler 实例传递给插件。这样,插件就可以通过订阅各个钩子来介入编译过程并执行自定义的逻辑。

插件中的钩子

  • 每个钩子都是一个 Hook 类型的实例,Hook 内部完全是基于 tapable 来实现
  • 钩子的定义通常接收两个参数:事件名和回调函数。事件名用于标识特定的钩子,而回调函数则是在触发该钩子时执行的代码。
  • 在 Webpack 中,大多数钩子的回调函数接收一个参数,通常被命名为 compilation,它表示当前的编译实例。compilation 对象包含了与当前编译相关的信息、资源和依赖关系等。

compiler 对象提供了各种钩子(hooks)来让插件在构建过程的不同阶段执行自定义逻辑。以下是一些常见的使用场景和对应的钩子:

  1. 初始化阶段(Initialization Phase):

    • entryOption:在解析入口模块之前调用,可以修改 Webpack 配置的入口选项。
    class MyPlugin {
      apply(compiler) {
        compiler.hooks.entryOption.tap('MyPlugin', (context, entry) => {
          // 修改入口配置
          // ...
        });
      }
    }
    ```
    
    
  2. 环境准备阶段(Environment Setup Phase):

    • afterEnvironment:在 Webpack 环境准备好之后调用,可以扩展环境或添加全局变量。
    class MyPlugin {
      apply(compiler) {
        compiler.hooks.afterEnvironment.tap('MyPlugin', () => {
          // 扩展环境或添加全局变量
          // ...
        });
      }
    }
    ```
    
    
  3. 编译阶段(Compilation Phase):

    • make:在创建新的编译实例之前调用,可以创建自定义的编译实例。
    class MyPlugin {
      apply(compiler) {
        compiler.hooks.make.tap('MyPlugin', (compilation) => {
          // 创建自定义的编译实例
          // ...
        });
      }
    }
    ```
    
    
  4. 编译过程阶段(Compilation Process Phase):

    • compilation:在每次创建新的编译实例时调用,可以访问和修改编译过程中的资源和依赖关系。
    class MyPlugin {
      apply(compiler) {
        compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
          // 访问和修改编译过程中的资源和依赖关系
          // ...
        });
      }
    }
    ```
    
    
  5. 构建完成阶段(Build Completion Phase):

    • done:在构建完成后调用,可以获取和处理构建结果的统计信息。
    class MyPlugin {
      apply(compiler) {
        compiler.hooks.done.tap('MyPlugin', (stats) => {
          // 获取和处理构建结果的统计信息
          // ...
        });
      }
    }
    ```
    
    
  6. 解析阶段(Resolve Phase):

    • beforeResolve:在解析模块路径之前调用,可以修改模块的解析规则或路径。
    class MyPlugin {
      apply(compiler) {
        compiler.hooks.beforeResolve.tap('MyPlugin', (resolveData) => {
          // 修改模块的解析规则或路径
          // ...
        });
      }
    }
    ```
    
    
  7. 优化阶段(Optimization Phase):

    • optimize:在优化阶段开始之前调用,可以自定义优化逻辑。
    class MyPlugin {
      apply(compiler) {
        compiler.hooks.optimize.tap('MyPlugin', (compilation) => {
          // 自定义优化逻辑
          // ...
        });
      }
    }
    ```
    
    
  8. 生成资源阶段(Asset Generation Phase):

    • emit:在生成最终资源之前调用,可以访问和修改最终生成的资源。
    class MyPlugin {
      apply(compiler) {
        compiler.hooks.emit.tap('MyPlugin', (compilation) => {
          // 访问和修改最终生成的资源
          // ...
        });
      }
    }
    ```
    
    
  9. 清理阶段(Cleanup Phase):

    • afterEmit:在生成最终资源之后调用,可以执行一些清理操作或触发其他任务。
    class MyPlugin {
      apply(compiler) {
        compiler.hooks.afterEmit.tap('MyPlugin', (compilation) => {
          // 执行清理操作或触发其他任务
          // ...
        });
      }
    }
    ```
    
    

上面演示了一些场景下钩子对构建过程的不同阶段更精细的控制和处理能力的支持。根据具体的需求和业务逻辑,可以选择适合的钩子并在插件中编写相应的逻辑代码。

通过开发自己的插件,可以根据项目需求添加自定义逻辑、优化构建过程,并实现更高效的前端开发流程。希望读到本文的都有所收获,共勉~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容