Webpack4之手动实现一个Plugin

下面手动实现一个简单的 Webpack Plugin。 先创建一个 Webpack 项目 在根目录中创建 src 文件夹,并在里面中创建一个 index.js 文件,编写下面代码

目录
  1. 搭建项目
  2. 编写 Plugin
    1. 简单实现
    2. 完善插件
    3. Node 调试

下面手动实现一个简单的 Webpack Plugin。

搭建项目#

先创建一个 Webpack 项目

mkdir make-plugin
cd make-plugin
yarn init -y
yarn add webpack webpack-cli -D

在根目录中创建 src 文件夹,并在里面中创建一个index.js文件,编写下面代码

console.log("Hello Webpack");

创建 Webpack 配置文件webpack.config.js

module.exports = {
  mode: "development",
  entry: path.resolve(__dirname, "src/index"),
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].js",
  },
};

package.json中编写打包脚本命令

"scripts": {
  "build": "webpack"
}

尝试进行编译打包

yarn build

查看 dist 文件夹里面的打包后的文件main.js,在文件倒数几行里发现下面的代码,编译成功。

eval(
  "console.log('Hello Webpack')\n\n//# sourceURL=webpack:///./src/index.js?"
);

编写 Plugin#

简单实现#

在根目录创建plugins文件夹,然后创建一个copyright-webpack-plugin.js文件,一般 Webpack 插件的名称都是叫xxx-webpack-plugin,我们现在遵守这个命名规则。

下面编写代码,先编写一个简单的类CopyrightWebpackPlugin,并且导出这个类。

class CopyrightWebpackPlugin {
  constructor() {
    console.log("插件被使用了");
  }
 
  apply(compiler) {}
}
 
module.exports = CopyrightWebpackPlugin;

Plugin 和 Loader 有点不一样,Loader 是一个普通函数,而 Plugin 却是一个类或者是构造函数。还记得我们如何使用 Plugin 的吗?一般都是调用new来使用插件的。

现在把我们编写的 Plugin 引入的配置文件中,虽然现在这个插件并没有什么用。

const CopyrightWebpackPlugin = require("./plugins/copyright-webpack-plugin");
 
module.exports = {
  // ...
  plugins: [new CopyrightWebpackPlugin()],
};

然后重新进行编译打包,你会发现在命令行中,会打印出插件被使用了的信息,说明我们编写的插件已经生效。

完善插件#

下面来完善插件的代码,先说下这个插件的功能,非常简单,就是在文件打包后,新生成一个copyright.md的文件,而文件的内容就是written by Hale

下面开始编写类的apply方法。

class CopyrightWebpackPlugin {
  // ...
  apply(compiler) {
    compiler.hooks.emit.tapAsync(
      "CopyrightWebpackPlugin",
      (compilation, cb) => {
        compilation.assets["copyright.md"] = {
          source: function () {
            return "written by Hale";
          },
          size: function () {
            return 15;
          },
        };
        cb();
      }
    );
  }
}

这个方法接受compiler作为参数,compiler是 Webpack 编译后生成的文件,里面包含了很多内容,包括配置、钩子 hooks 等。这里使用hooks钩子来增加文件。compiler的 hooks 和前端框架VueReact的生命函数的钩子类似,都是在某一个时刻执行的函数。

钩子的emit方法是在生成打包文件前执行的,我们使用这个方法在打包前创建一个copyright.md文件。

因为这个钩子是异步执行的,所以使用tapAsync方法来调用钩子,需要转入两个参数,第一个参数是本身插件的名称,第二个参数是一个函数。函数需要传入compilation参数,这个参数也包含了很多属性。因为这是一个异步的钩子,所以还需要传入函数的第二个参数cb,这是一个回调函数,在这个钩子函数结束时调用它。

compilationassets属性表示生成后的文件,我们可以在赋值前打印下这个属性

(compilation, cb) => {
  console.log(compilation.assets); // ! 打印属性
  compilation.assets["copyright.md"] = {
    // ...
  };
};

生成的内容如下,这里显示了生成的main.js文件的一些属性

{ 'main.js':
   CachedSource {
     _source: ConcatSource { children: [Array] },
     _cachedSource: undefined,
     _cachedSize: undefined,
     _cachedMaps: {},
     node: [Function],
     listMap: [Function]
   }
}

现在我们就可以添加一个文件

compilation.assets["copyright.md"] = {
  source: function () {
    return "written by Hale";
  },
  size: function () {
    return 15;
  },
};

其中source是源码文件,而size是生成文件的大小,返回 15 表示生成的文件大小是 15 byte。

插件编写完成后,重新进行编译打包,就会发现dist文化夹中新增加了一个copyright.md文件,内容是written by Hale,说明我们编写的这个插件生效了。

其实,插件可以使用多个钩子,下面使用一个同步的钩子compile,这个钩子是在 Webpack 初始化编译时执行的,执行的顺序还在emit钩子之前。因为这是一个同步执行的钩子,所以使用tap来调用,并且在第二个函数参数中也不需要再传入回调函数cb

class CopyrightWebpackPlugin {
  apply(compiler) {
    compiler.hooks.compile.tap("CopyrightWebpackPlugin", (compilation) => {
      console.log("compile hook"); // compile 钩子的打印信息
    });
 
    compiler.hooks.emit.tapAsync(
      "CopyrightWebpackPlugin",
      (compilation, cb) => {
        console.log("emit hook"); // emit 钩子的打印信息
        compilation.assets["copyright.md"] = {
          source: function () {
            return "written by Hale";
          },
          size: function () {
            return 15;
          },
        };
        cb();
      }
    );
  }
}

重新进行编译打包,编译成功。然后,我们发现在命令行中是先打印出compile hook信息,然后再打印出emit hook信息。

Node 调试#

我们在初次编写插件时,不知道插件中一些的参数是什么样子的,可以使用console.log在命令行把参数的信息打印出来查看,也可以使用 Node 的调试模式进行断点调试,来查看它的参数的属性。

下面我们使用 Node 调试模式调试代码

package.json新增debug脚本命令,这里调用node命令进行 debug 调试。

"scripts": {
  "debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js",
}

其中node node_modules/webpack/bin/webpack.jswebpack这两个命令效果是一样的。

然后加入参数,--inspect表示进入调试模式 ,而--inspect-brk参数表示在调试代码的第一行打断点。

下面执行这个命令,进入 Node 调试模式

yarn debug

命令行中会出现下面这些提示信息

$ node --inspect --inspect-brk node_modules/webpack/bin/webpack.js
Debugger listening on ws://127.0.0.1:9229/cf7f577c-b23f-417a-813e-c42299328113
For help, see: https://nodejs.org/en/docs/inspector
Debugger attached.

这时,打开chrome浏览器,在地址栏输入127.0.0.1:9229按回车,然后按下F12打开页面的控制台,发现控制面板的左上角有个绿色的 Node 的 Logo 标识,点击这个标识就可以进入到代码的调试界面。

此时,我们发现代码已经在 Webpack 模块的第一行代码中停下来,因为我们前面设置了在第一行打断点。这时我们就可以进行调试了。

从一开始就在 Webpack 模块的第一行代码中停下来并没有什么意义。因为我们还需要跳到插件的代码中去查看属性,这样需要花费不少时间进行跳转。所以,我们可以手动在插件代码中新增一个断点,然后直接跳这行代码中去调试。

怎么新增断点呢?在插件中插入一句debugger代码,这样相当于在插件的这行代码中打了一个断点。

class CopyrightWebpackPlugin {
  apply(compiler) {
    compiler.hooks.compile.tap("CopyrightWebpackPlugin", (compilation) => {
      console.log("compile hook");
    });
 
    compiler.hooks.emit.tapAsync(
      "CopyrightWebpackPlugin",
      (compilation, cb) => {
        console.log("emit hook");
        debugger; // debugger 断点
        compilation.assets["copyright.md"] = {
          source: function () {
            return "written by Hale";
          },
          size: function () {
            return 15;
          },
        };
        cb();
      }
    );
  }
}

重新启动 Node 的调试模式

yarn debug

再次打开调试页面,然后点击跳转按钮,或者按F8直接跳到插件代码中的debugger这一行上。这时,把鼠标移动到相关的代码上面就可以查看它们的属性信息了。