编写loader 和 plugin

发布时间 2023-11-17 17:29:55作者: 柯基与佩奇

编写一个loader

在平时自己由零搭建项目时,虽然基础配置都比较熟悉,比如配置 file-loader, url-loader, css-loader 等,配置不难,但究竟是怎么起作用的呢,如何编写一个 Webpack Loader。

loader 通常指打包的方案,即按什么方式来处理打包,打包的时候它可以拿到模块源代码,经过特定 loader 的转换后返回新的结果。
比如 sass-loader 可以把 SCSS 代码转换成 CSS 代码

保持功能单一

项目中可能会配置很多,但要记住,要保持一个 Loader 的功能单一,避免做多种功能,只需完成一种功能转换即可。
所以如 less 文件转换成 css 文件,也不是一步到位,而是 less-loader, css-loader, style-loader 几个 loader 的链式调用才能完成转换。

模块

因为 Webpack 本身是运行在 Node.js 之上的,一个 loader 其实就是一个 node 模块,这个模块导出的是一个函数,即:

module.exports = function (source) {
  // source 为 compiler 传递给 Loader 的一个文件的原内容
  // 处理...
  return source; // 需要返回处理后的内容
};

这个导出的函数的工作就是获得处理前的原内容,对原内容执行处理后,返回处理后的内容。

替换字符串的 loader

比如打包时,想要替换源文件的字符串,这时可以考虑使用 Loader,因为 loader 就是获得源文件内容然后对其进行处理,再返回。
比如 src 目录下有三个文件:

export const msg1 = "学习框架";
export const msg2 = "深入理解JS";
import { msg1 } from "./msg1";
import { msg2 } from "./msg2";

function print() {
  console.log(`输出:${msg1}, ${msg2}`);
}

print();

做的事情则是把 msg1 和 msg2 两个文件导入,然后输出两个字符串。
现在要做的事也很简单,把"框架"转为"React 框架", "JS"转为"JavaScript"。

新建 src/loaders/replaceLoader.js 文件,

module.exports = function (source) {
  const handleContent = source
    .replace("框架", "React框架")
    .replace("JS", "JavaScript");
  return handleContent;
};

source 是源文件内容
就这样,loader 写完了!!!

使用 Loader

在根目录下新建文件 webpack.config.js

const path = require("path");

module.exports = {
  mode: "production",
  entry: "./src/index.js",
  module: {
    rules: [
      {
        test: /\.js$/,
        use: "./src/loaders/replaceLoader.js",
      },
    ],
  },
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].js",
  },
};

执行 npx webpack, 查看打包结果 dist/main.js

(() => {
  "use strict";
  console.log("输出:学习React框架, 深入理解JavaScript");
})();

替换成功!

需要注意的是,use 里面填写的 loader 是去 node_modules 目录里面找的,由于是自定义的 loader,所以不能直接写 use: 'replaceLoader',但直接写路径的方式未免难看点,可以通过 webpack 来配置:

module.exports = {
  resolveLoader: {
    modules: ["node_modules", "./src/loaders"], // node_modules找不到,就去./src/loaders找
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: "replaceLoader",
      },
    ],
  },
};

获取 loader 的 options

其实就是写一个功能函数,如果 loader 可以传入参数呢

module: {
  rules: [
    {
      test: /\.js$/,
      use: {
        loader: 'replaceLoader',
        options: {
          params: 'replaceString',
        },
      },
    },
  ],
},

这个时候可以使用 this.query 来获取,通过 this.query.params 就能拿到,这里需要注意的是,this 上下文是有用的,所以这个 loader 导出函数不能是箭头函数。
但 webpack 更推荐 loader-utils 模块来获取,它提供了许多有用的工具,最常用的一种工具是获取传递给 loader 的选项。

首先要安装 npm i -D loader-utils
修改 src/loaders/replaceLoader.js

const { getOptions } = require("loader-utils");

module.exports = function (source) {
  console.log(getOptions(this)); // { params: 'replaceString' }
  console.log(this.query.params); // replaceString
  const handleContent = source
    .replace("框架", "React框架")
    .replace("JS", "JavaScript");
  return handleContent;
};

这里需要注意的是,getOptions(this)参数传入的是 this,也就是说
打印结果

{ params: 'replaceString' }
{ params: 'replaceString' }
{ params: 'replaceString' }

this.callback()

有些场景下还需要返回其他东西比如 sourceMap

module.exports = function (source) {
  // 告诉 Webpack 返回的结果
  this.callback(null, source, sourceMaps);
};

另外也不需要 return 了,所以也可使用此 API 替代 return

const { getOptions } = require("loader-utils");

module.exports = function (source) {
  const handleContent = source
    .replace("框架", "React框架")
    .replace("JS", "JavaScript");
  this.callback(null, handleContent);
};

自定义 loader 应用场景

  1. 在所有 function 外面加一层 try catch 代码块捕获错误,避免手动繁琐添加。
  2. 实现中英文替换:可以将文字用占位符如{{ title }}包裹,检测到占位符则根据环境变量替换为中英文。

常用 loader

raw-loader:加载文件原始内容(utf-8)
file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 (处理图片和字体)
url-loader:与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值时返回其 publicPath,小于阈值时返回文件 base64 形式编码 (处理图片和字体)
source-map-loader:加载额外的 Source Map 文件,以方便断点调试
svg-inline-loader:将压缩后的 SVG 内容注入代码中
image-loader:加载并且压缩图片文件
json-loader 加载 JSON 文件(默认包含)
handlebars-loader: 将 Handlebars 模版编译成函数并返回
babel-loader:把 ES6 转换成 ES5
ts-loader: 将 TypeScript 转换成 JavaScript
awesome-typescript-loader:将 TypeScript 转换成 JavaScript,性能优于 ts-loader
sass-loader:将 CSS 代码注入 JavaScript 中,通过 DOM 操作去加载 CSS
css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS
postcss-loader:扩展 CSS 语法,使用下一代 CSS,可以配合 autoprefixer 插件自动补齐 CSS3 前缀
eslint-loader:通过 ESLint 检查 JavaScript 代码
tslint-loader:通过 TSLint 检查 TypeScript 代码
mocha-loader:加载 Mocha 测试用例的代码
coverjs-loader:计算测试的覆盖率
vue-loader:加载 Vue.js 单文件组件
i18n-loader: 国际化
cache-loader: 可以在一些性能开销较大的 Loader 之前添加,目的是将结果缓存到磁盘里

编写一个plugin

按本人的理解,Webpack 插件的作用就是在 webpack 运行到某个时刻的时候,帮做一些事情。
在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

官方解释是:
插件向第三方开发者提供了 webpack 引擎中完整的能力。使用阶段式的构建回调,开发者可以引入它们自己的行为到 webpack 构建流程中。

webpack 插件的组成:

  1. 一个 JS 命名函数或一个类(可以想下平时使用插件就是 new XXXPlugin()的方式)
  2. 在插件类/函数的 (prototype) 上定义一个 apply 方法。
  3. 通过 apply 函数中传入 compiler 并插入指定的事件钩子,在钩子回调中取到 compilation 对象
  4. 通过 compilation 处理 webpack 内部特定的实例数据
  5. 如果是插件是异步的,在插件的逻辑编写完后调用 webpack 提供的 callback

写一个插件,生成一个版权的文件。

基本雏形

function CopyrightWebpackPlugin() {}

CopyrightWebpackPlugin.prototype.apply = function (compiler) {};

module.exports = CopyrightWebpackPlugin;

也可以写成类的形式:

class CopyrightWebpackPlugin {
  apply(compiler) {
    console.log(compiler);
  }
}

module.exports = CopyrightWebpackPlugin;

webpack 在启动之后,在读取配置的过程中会先执行 new CopyrightWebpackPlugin(options)操作,初始化一个 CopyrightWebpackPlugin 实例对象。在初始化 compiler 对象之后,会调用上述实例对象的 apply 方法并将 compiler 对象传入。
在 apply 方法中,通过 compiler 对象来监听 webpack 生命周期中广播出来的事件,也可以通过 compiler 对象来操作 webpack 的输出。

Compiler 和 Compilation

在插件开发中最重要的两个对象是 compiler 和 compilation 对象。

  1. compiler 对象代表了完整的 webpack 环境配置,在初始化 compiler 对象之后,通过调用插件实例的 apply 方法,作为其参数传入。这个对象在启动 webpack 时被一次性建立,并包含了 webpack 环境的所有的配置信息,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。

  2. compilation 对象会作为 plugin 内置事件回调函数的参数,一个 compilation 对象包含了当前的模块资源、编译生成资源、变化的文件以及被跟踪依赖的状态信息。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 compilation 将被创建。compilation 对象也提供了很多事件回调供插件做扩展。通过 compilation 也能读取到 compiler 对象。

编码

下面代码为生成一个版权 txt 文件,新建文件 src/plugins/copyright-webpack-plugin.js:

class CopyrightWebpackPlugin {
  apply(compiler) {
    // emit 钩子是生成资源到 output 目录之前执行,emit 是一个异步串行钩子,需要用 tapAsync 来注册
    compiler.hooks.emit.tapAsync(
      "CopyrightWebpackPlugin",
      (compilation, callback) => {
        // 回调方式注册异步钩子
        const copyrightText = "版权归 JackySummer 所有";
        // compilation存放了这次打包的所有内容
        // 所有待生成的文件都在它的 assets 属性上
        compilation.assets["copyright.txt"] = {
          // 添加copyright.txt
          source: function () {
            return copyrightText;
          },
          size: function () {
            // 文件大小
            return copyrightText.length;
          },
        };
        callback(); // 必须调用
      }
    );
  }
}

module.exports = CopyrightWebpackPlugin;

webpack 中许多对象扩展自 Tapable 类。这个类暴露 tap, tapAsync 和 tapPromise 方法,可以使用这些方法,注入自定义的构建步骤,这些步骤将在整个编译过程中不同时机触发。

使用 tapAsync 方法来访问插件时,需要调用作为最后一个参数提供的回调函数。

在 webpack.config.js

const path = require("path");
const CopyrightWebpackPlugin = require("./src/plugins/copyright-webpack-plugin");

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

执行 webpack 命令,就会看到 dist 目录下生成 copyright.txt 文件

如果在配置文件使用 plugin 时传入参数该怎么获得呢,可以在插件类添加构造函数拿到:

plugins: [
  new CopyrightWebpackPlugin({
    name: 'jacky',
  }),
],

在 copyright-webpack-plugin.js 中

class CopyrightWebpackPlugin {
  constructor(options = {}) {
    console.log("options", options); // options { name: 'jacky' }
  }
}

常见 Plugins

define-plugin:定义环境变量 (Webpack4 之后指定 mode 会自动配置)
ignore-plugin:忽略部分文件
html-webpack-plugin:简化 HTML 文件创建 (依赖于 html-loader)
web-webpack-plugin:可方便地为单页应用输出 HTML,比 html-webpack-plugin 好用
uglifyjs-webpack-plugin:不支持 ES6 压缩 (Webpack4 以前)
terser-webpack-plugin: 支持压缩 ES6 (Webpack4)
webpack-parallel-uglify-plugin: 多进程执行代码压缩,提升构建速度
mini-css-extract-plugin: 分离样式文件,CSS 提取为独立文件,支持按需加载 (替代 extract-text-webpack-plugin)
serviceworker-webpack-plugin:为网页应用增加离线缓存功能
clean-webpack-plugin: 目录清理
ModuleConcatenationPlugin: 开启 Scope Hoisting
speed-measure-webpack-plugin: 可以看到每个 Loader 和 Plugin 执行耗时 (整个打包耗时、每个 Plugin 和 Loader 耗时)
webpack-bundle-analyzer: 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块)