详细介绍Webpack5中的Plugin

Plugin的作用

插件Plugin可以扩展webpack,加入自定义的构建行为,使 webpack 可以执行更广泛的任务,拥有更强的构建能力。

Plugin的工作原理

webpack就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理

webpack通过Tapable来组织这条复杂的生产线。 webpack在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。

站在代码逻辑的角度就是:webpack在编译代码过程中,会触发一系列 Tapable钩子事件,插件所做的就是找到相应的钩子,往上面挂上自己的任务,也就是注册事件,这样当webpack构建的时候,插件注册的事件就会随着钩子的触发而执行了。

Webpack内部的钩子

钩子的本质就是:事件。为了方便我们直接介入和控制编译过程,webpack 把编译过程中触发的各类关键事件封装成事件接口暴露了出来。这些接口被很形象地称做:hooks(钩子)。开发插件,离不开这些钩子。

Tapable

Tapable为webpack提供了统一的插件接口(钩子)类型定义,它是webpack的核心功能库。webpack中目前有十种hooks,在 Tapable 源码中可以看到:

// https://github.com/webpack/tapable/blob/master/lib/index.js
exports.SyncHook = require("./SyncHook");
exports.SyncBailHook = require("./SyncBailHook");
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
exports.SyncLoopHook = require("./SyncLoopHook");
exports.AsyncParallelHook = require("./AsyncParallelHook");
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook");
exports.AsyncSeriesLoopHook = require("./AsyncSeriesLoopHook");
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");
exports.HookMap = require("./HookMap");
exports.MultiHook = require("./MultiHook");

Tapable还统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为:
tap:可以注册同步钩子和异步钩子。
tapAsync:回调方式注册异步钩子。
tapPromise:Promise 方式注册异步钩子。

Plugin构建对象

Compiler

compiler对象中保存着完整的 Webpack 环境配置,每次启动 webpack 构建时它都是一个独一无二,仅仅会创建一次的对象。

这个对象会在首次启动 Webpack 时创建,我们可以通过 compiler 对象上访问到 Webapck 的主环境配置,比如 loader 、 plugin 等等配置信息。

它有以下主要属性:
compiler.options可以访问本次启动 webpack 时候所有的配置文件,包括但不限于 loaders 、 entry 、 output 、 plugin 等等完整配置信息。
compiler.inputFileSystemcompiler.outputFileSystem 可以进行文件操作,相当于 Nodejs 中 fs。
compiler.hooks 可以注册 tapable 的不同种类 Hook,从而可以在 compiler 生命周期中植入不同的逻辑。

compiler hooks 文档

Compilation

compilation 对象代表一次资源的构建,compilation 实例能够访问所有的模块和它们的依赖。

一个 compilation对象会对构建依赖图中所有模块,进行编译。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。

它有以下主要属性:
compilation.modules 可以访问所有模块,打包的每一个文件都是一个模块。
compilation.chunks chunk 即是多个 modules 组成而来的一个代码块。入口文件引入的资源组成一个 chunk,通过代码分割的模块又是另外的 chunk。
compilation.assets 可以访问本次打包生成所有文件的结果。
compilation.hooks 可以注册 tapable 的不同种类 Hook,用于在 compilation 编译模块阶段进行逻辑添加以及修改。

compilation hooks 文档

生命周期

在这里插入图片描述

开发一个插件

  1. webpack读取配置时,new TestPlugin() ,会执行插件 constructor 方法
  2. webpack创建 compiler 对象
  3. 遍历所有插件,调用插件的 apply 方法
  4. 执行剩下编译流程(出发各个hooks事件)

plugins/test-plugin.js:

class TestPlugin {
    
    
  constructor() {
    
    
    console.log("TestPlugin constructor()");
  }
  apply(compiler) {
    
    
    console.log("TestPlugin apply()");
  }
}
module.exports = TestPlugin;

插件的使用要在webpack.config.js进行配置:

const TestPlugin = require("./plugins/test-plugin")
……
plugins: [
	new TestPlugin()
]

注册 hooks

class TestPlugin {
    
    
  constructor() {
    
    
    console.log("TestPlugin constructor()");
  }
  apply(compiler) {
    
    
    console.log("TestPlugin apply()");

    // 从文档可知, compile hook 是 SyncHook, 也就是同步钩子, 只能用tap注册
    compiler.hooks.compile.tap("TestPlugin", (compilationParams) => {
    
    
      console.log("compiler.compile()");
    });

    // 从文档可知, make 是 AsyncParallelHook, 也就是异步并行钩子, 特点就是异步任务同时执行
    // 可以使用 tap、tapAsync、tapPromise 注册。
    // 如果使用tap注册的话,进行异步操作是不会等待异步操作执行完成的。
    compiler.hooks.make.tap("TestPlugin", (compilation) => {
    
    
      compilation.hooks.seal.tap("TestPlugin",()=>{
    
    
        console.log("TestPlugin seal")
      })
      setTimeout(() => {
    
    
        console.log("compiler.make() 111");
      }, 2000);
    });

    // 使用tapAsync、tapPromise注册,进行异步操作会等异步操作做完再继续往下执行
    compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {
    
    
      setTimeout(() => {
    
    
        console.log("compiler.make() 222");
        // 必须调用
        callback();
      }, 1000);
    });

    compiler.hooks.make.tapPromise("TestPlugin", (compilation) => {
    
    
      console.log("compiler.make() 333");
      // 必须返回promise
      return new Promise((resolve) => {
    
    
        resolve();
      });
    });

    // 从文档可知, emit 是 AsyncSeriesHook, 也就是异步串行钩子,特点就是异步任务顺序执行
    compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
    
    
      setTimeout(() => {
    
    
        console.log("compiler.emit() 111");
        callback();
      }, 3000);
    });

    compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
    
    
      setTimeout(() => {
    
    
        console.log("compiler.emit() 222");
        callback();
      }, 2000);
    });

    compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
    
    
      setTimeout(() => {
    
    
        console.log("compiler.emit() 333");
        callback();
      }, 1000);
    });
  }
}

module.exports = TestPlugin;

启动调试

通过调试查看 compiler 和 compilation 对象数据情况。

package.json配置指令:

{
    
    
  "name": "source",
  "version": "1.0.0",
  "scripts": {
    
    
    "debug": "node --inspect-brk ./node_modules/webpack-cli/bin/cli.js"
  },
}

运行指令:npm run debug

此时控制台输出以下内容:

PS C:\Users\86176\Desktop\source> npm run debug

> source@1.0.0 debug
> node --inspect-brk ./node_modules/webpack-cli/bin/cli.js

Debugger listening on ws://127.0.0.1:9229/629ea097-7b52-4011-93a7-02f83c75c797
For help, see: https://nodejs.org/en/docs/inspecto

打开 Chrome 浏览器,F12 打开浏览器调试控制台。此时控制台会显示一个绿色的图标:
在这里插入图片描述
点击绿色的图标进入调试模式。

在需要调试代码处用 debugger 打断点,代码就会停止运行,从而调试查看数据情况。

BannerWebpackPlugin

作用:给打包输出文件添加注释。

打包输出前添加注释:需要使用 compiler.hooks.emit 钩子, 它是打包输出前触发。compilation.assets 可以获取所有即将输出的资源文件。

// plugins/banner-webpack-plugin.js
class BannerWebpackPlugin {
    
    
  constructor(options = {
     
     }) {
    
    
    this.options = options;
  }

  apply(compiler) {
    
    
    // 需要处理文件
    const extensions = ["js", "css"];

    // emit是异步串行钩子
    compiler.hooks.emit.tapAsync("BannerWebpackPlugin", (compilation, callback) => {
    
    
      // compilation.assets包含所有即将输出的资源
      // 通过过滤只保留需要处理的文件
      const assetPaths = Object.keys(compilation.assets).filter((path) => {
    
    
        const splitted = path.split(".");
        return extensions.includes(splitted[splitted.length - 1]);
      });

      assetPaths.forEach((assetPath) => {
    
    
        const asset = compilation.assets[assetPath];

        const source = `/*
		* Author: ${
      
      this.options.author}
		*/\n${
      
      asset.source()}`;

        // 覆盖资源
        compilation.assets[assetPath] = {
    
    
          // 资源内容
          source() {
    
    
            return source;
          },
          // 资源大小
          size() {
    
    
            return source.length;
          },
        };
      });

      callback();
    });
  }
}

module.exports = BannerWebpackPlugin;

CleanWebpackPlugin

作用:在 webpack 打包输出前将上次打包内容清空,即clean: true;

如何在打包输出前执行?
需要使用 compiler.hooks.emit 钩子, 它是打包输出前触发。

如何清空上次打包内容?
获取打包输出目录:通过 compiler 对象。
通过文件操作清空内容:通过 compiler.outputFileSystem 操作文件。

// plugins/clean-webpack-plugin.js
class CleanWebpackPlugin {
    
    
  apply(compiler) {
    
    
    // 获取操作文件的对象
    const fs = compiler.outputFileSystem;
    // emit是异步串行钩子
    compiler.hooks.emit.tapAsync("CleanWebpackPlugin", (compilation, callback) => {
    
    
      // 获取输出文件目录
      const outputPath = compiler.options.output.path;
      // 删除目录所有文件
      const err = this.removeFiles(fs, outputPath);
      // 执行成功err为undefined,执行失败err就是错误原因
      callback(err);
    });
  }

  removeFiles(fs, path) {
    
    
    try {
    
    
      // 读取当前目录下所有文件
      const files = fs.readdirSync(path);

      // 遍历文件,删除
      files.forEach((file) => {
    
    
        // 获取文件完整路径
        const filePath = `${
      
      path}/${
      
      file}`;
        // 分析文件
        const fileStat = fs.statSync(filePath);
        // 判断是否是文件夹
        if (fileStat.isDirectory()) {
    
    
          // 是文件夹需要递归遍历删除下面所有文件
          this.removeFiles(fs, filePath);
        } else {
    
    
          // 不是文件夹就是文件,直接删除
          fs.unlinkSync(filePath);
        }
      });

      // 最后删除当前目录
      fs.rmdirSync(path);
    } catch (e) {
    
    
      // 将产生的错误返回出去
      return e;
    }
  }
}

module.exports = CleanWebpackPlugin;

AnalyzeWebpackPlugin

作用:分析 webpack 打包资源大小,并输出分析文件。

在哪做? compiler.hooks.emit,是在打包输出前触发,我们需要分析资源大小同时添加上分析后的 md 文件。

// plugins/analyze-webpack-plugin.js
class AnalyzeWebpackPlugin {
    
    
  apply(compiler) {
    
    
    // emit是异步串行钩子
    compiler.hooks.emit.tap("AnalyzeWebpackPlugin", (compilation) => {
    
    
      // Object.entries将对象变成二维数组。二维数组中第一项值是key,第二项值是value
      const assets = Object.entries(compilation.assets);

      let source = "# 分析打包资源大小 \n| 名称 | 大小 |\n| --- | --- |";

      assets.forEach(([filename, file]) => {
    
    
        source += `\n| ${
      
      filename} | ${
      
      file.size()} |`;
      });

      // 添加资源
      compilation.assets["analyze.md"] = {
    
    
        source() {
    
    
          return source;
        },
        size() {
    
    
          return source.length;
        },
      };
    });
  }
}

module.exports = AnalyzeWebpackPlugin;

InlineChunkWebpackPlugin

作用:webpack 打包生成的 runtime 文件太小了,额外发送请求性能不好,所以需要将其内联到 js 中,从而减少请求数量。

需要借助 html-webpack-plugin 来实现,在 html-webpack-plugin 输出 index.html 前将内联 runtime 注入进去,删除多余的 runtime 文件。

// plugins/inline-chunk-webpack-plugin.js
const HtmlWebpackPlugin = require("safe-require")("html-webpack-plugin");

class InlineChunkWebpackPlugin {
    
    
  constructor(tests) {
    
    
    this.tests = tests;
  }

  apply(compiler) {
    
    
    compiler.hooks.compilation.tap("InlineChunkWebpackPlugin", (compilation) => {
    
    
      const hooks = HtmlWebpackPlugin.getHooks(compilation);

      hooks.alterAssetTagGroups.tap("InlineChunkWebpackPlugin", (assets) => {
    
    
        assets.headTags = this.getInlineTag(assets.headTags, compilation.assets);
        assets.bodyTags = this.getInlineTag(assets.bodyTags, compilation.assets);
      });

      hooks.afterEmit.tap("InlineChunkHtmlPlugin", () => {
    
    
        Object.keys(compilation.assets).forEach((assetName) => {
    
    
          if (this.tests.some((test) => assetName.match(test))) {
    
    
            delete compilation.assets[assetName];
          }
        });
      });
    });
  }

  getInlineTag(tags, assets) {
    
    
    return tags.map((tag) => {
    
    
      if (tag.tagName !== "script") return tag;

      const scriptName = tag.attributes.src;

      if (!this.tests.some((test) => scriptName.match(test))) return tag;

      return {
    
     tagName: "script", innerHTML: assets[scriptName].source(), closeTag: true };
    });
  }
}

module.exports = InlineChunkWebpackPlugin;

参考文档

猜你喜欢

转载自blog.csdn.net/zag666/article/details/131948452
今日推荐