Webpack如何实现万物皆可import?loader的使用/配置/手写实践

作者:漂流瓶jz日期:2026/6/4

Webpack是前端历史上具有统治地位的打包工具,应用非常广泛。虽然现在逐渐被性能更强的工具替代,但是依然有很多工程使用。loader是Webpack中的一种重要的外部插入配置工具,负责对源代码进行转换。Webpack本身只能理解JavaScript和JSON文件,其它类型的文件不能处理。正是使用各种loader,Webpack才有了将各种格式的资源和代码识别和引入的能力。当然,loader的能力也并不仅限于此。

loader使用示例

为了了解loader的作用和使用方式,我们举例一些现有的知名loader。

创建Webpack工程

方便后续演示,首先需要新创建一个使用Webpack构建的最简单工程。执行命令行:

1npm init -y
2npm install -D webpack webpack-cli
3

修改package.json,去掉"type": "commonjs",scripts中增加"build": "webpack"指令。创建src/index.js,内容创建一个div元素并放到body中。

1function genEle(test, className) {
2  const div = document.createElement("div");
3  div.className = className;
4  div.textContent = test;
5  document.body.appendChild(div);
6}
7genEle("jzplp1");
8

然后是index.html,为HTML入口,注意因为要打包,所以引用的js是dist目录中的,并不是src。

1<html>
2  <head>
3    <title>jzplp Webpack loader</title>
4  </head>
5  <body>
6    <script src="./dist/main.js"></script>
7  </body>
8</html>
9

然后是webpack.config.js,配置打包相关路径:

1const path = require("path");
2
3module.exports = {
4  mode: "production",
5  entry: "./src/index.js",
6  output: {
7    filename: "main.js",
8    path: path.resolve(__dirname, "dist"),
9  },
10};
11

然后执行npm run build进行打包,可以在dist目录中看到打包结果,以及在浏览器中看到执行效果:

CSS文件loader

CSS文件在Webpack中默认是不支持的,如果需要引入CSS文件,则需要loader的帮助。这里我们先试一下不使用loader的效果。首先新增一个src/index.css文件:

1.abc {
2  color: red;
3}
4

然后修改src/index.js,引入CSS文件:

1import "./index.css";
2
3function genEle(test, className) {
4  const div = document.createElement("div");
5  div.className = className;
6  div.textContent = test;
7  document.body.appendChild(div);
8}
9genEle("jzplp1", "abc");
10

这时候我们再次打包,会报错模块解析错误,提示我们应该找个合适的loader来适配这个文件类型:

与CSS相关的最常用loader有两个,顺序为 css-loader -> style-loader。

  • css-loader 负责解析CSS代码,使其作为一个模块
  • style-loader 创建style样式,将CSS代码插入到HTML中

如果只引入css-loader,打包后可以看到JS文件中有CSS代码,但是却没有生效。这里我们引入两个试试:

1const path = require("path");
2
3module.exports = {
4  mode: "production",
5  entry: "./src/index.js",
6  output: {
7    filename: "main.js",
8    path: path.resolve(__dirname, "dist"),
9  },
10  module: {
11    rules: [
12      {
13        test: /\.css$/,
14        use: [
15          "style-loader",
16          "css-loader",
17        ],
18      },
19    ],
20  },
21};
22

注意看在配置中有个test,我们传入了正则,表示.css后缀的文件名会匹配到这个规则。匹配到这个规则的文件,就使用这两个loader处理。然后再打包,发现生成文件中有引入CSS代码,而且浏览器中可以看到效果:

xml-loader

对于引入的XML文件,如果不使用xml-loader,也会像前面CSS文件一样提示模块解析报错。这里我们修改配置,匹配到xml后缀名的文件。

1const path = require("path");
2
3module.exports = {
4  mode: "production",
5  entry: "./src/index.js",
6  output: {
7    filename: "main.js",
8    path: path.resolve(__dirname, "dist"),
9  },
10  module: {
11    rules: [
12      {
13        test: /\.css$/,
14        use: ["style-loader", "css-loader"],
15      },
16      {
17        test: /\.xml$/,
18        use: "xml-loader",
19      },
20    ],
21  },
22};
23

然后我们创建src/index.xml文件,内容如下:

1<?xml version="1.0" encoding="UTF-8"?>
2<note>
3  <to>jzplp1</to>
4  <from>jzplp2</from>
5</note>
6

然后在src/index.js中引入文件,并输出结果:

1import "./index.css";
2import dataXml from './index.xml';
3
4console.log(dataXml);
5
6function genEle(test, className) {
7  const div = document.createElement("div");
8  div.className = className;
9  div.textContent = test;
10  document.body.appendChild(div);
11}
12genEle("jzplp1", "abc");
13

然后进行构建,查看生成文件和浏览器的Console效果。可以看到xml文件被解析为数据直接放到生成代码中了,浏览器可以正常输出结果。

babel-loader

Babel是一个知名的代码编译工具,它的主要作用是将新版本的ECMAScript代码转换为兼容的旧版本JavaScript代码,以便新代码可以正常运行在旧浏览器环境中。要将Babel引入到Webpack打包流程中,同样需要babel-loader的帮助。首先需要安装依赖babel-loader和@babel/core,以及@babel/preset-env。然后修改配置:

1const path = require("path");
2
3module.exports = {
4  mode: "production",
5  entry: "./src/index.js",
6  output: {
7    filename: "main.js",
8    path: path.resolve(__dirname, "dist"),
9  },
10  module: {
11    rules: [
12      {
13        test: /\.js$/,
14        exclude: /node_modules/,
15        use: {
16          loader: "babel-loader",
17          options: {
18            presets: ["@babel/preset-env"],
19          },
20        },
21      },
22    ],
23  },
24};
25

在配置中我们识别了以js作为后缀名的文件,排除了node_modules中的代码,并且设置了loader选项,提供了Babel预设。注意js文件即使我们不引入babel-loader,Webpack也是可以解析的。在使用babel-loader后,Webpack打包js文件前,需要先经过Babel处理。下面对比一下使用和不使用babel-loader时,打包后的文件内容:

1// 不使用 babel-loader
2!(function () {
3  const e = document.createElement("div");
4  ((e.className = "abc"),
5    (e.textContent = "jzplp1"),
6    document.body.appendChild(e));
7})();
8// 使用 babel-loader
9(() => {
10  var e;
11  (((e = document.createElement("div")).className = "abc"),
12    (e.textContent = "jzplp1"),
13    document.body.appendChild(e));
14})();
15

通过对比可以看到,使用babel-loader后,代码被Babel编译了,明显区别在于const这个ES6语法不存在了,转为了var这个兼容语法。

thread-loader

thread-loader并不是用来引入某种类型文件的,而是利用多进程同时执行的技术,优化其它loader的执行时间的。thread-loader只需要放置在其它loader之前,它会创建多个进程,将后续的loader执行代码放到独立的进程中执行,从而优化时间性能。它适合用在比较耗时的操作中,例如babel-loader。我们修改配置:

1const path = require("path");
2
3module.exports = {
4  mode: "production",
5  entry: "./src/index.js",
6  output: {
7    filename: "main.js",
8    path: path.resolve(__dirname, "dist"),
9  },
10  module: {
11    rules: [
12      {
13        test: /\.js$/,
14        exclude: /node_modules/,
15        use: ["thread-loader", "babel-loader"],
16      },
17    ],
18  },
19};
20

多进程是否有效,直接打包是看不出来的,因此我们增加一个Babel插件,在执行时打印PID和文件名。创建babel.config.js:

1module.exports = {
2  presets: ["@babel/preset-env"],
3  plugins: [
4    function plugin1(babel) {
5      return {
6        visitor: {
7          Program(path, state) {
8            const line = `PID=${process.pid}  ${state.file.opts.filename}\n`;
9            console.log(line);
10          },
11        },
12      };
13    },
14  ],
15};
16

然后再创建两个JavaScript文件,在入口文件src/index.js中都引入:

1import './index2';
2import './index3';
3
4function genEle(test, className) {
5  const div = document.createElement("div");
6  div.className = className;
7  div.textContent = test;
8  document.body.appendChild(div);
9}
10genEle("jzplp1", "abc");
11

然后在使用和不使用thread-loader时分别打包,打包时观察console输出,可以发现在不使用时,三个文件的PID都一样,说明是在一个进程中执行的。而使用了thread-loader之后,PID出现不一致的情况,说明babel编译不同文件被分散到了不同进程处理。

1// 不使用thread-loader
2PID=15988  E:\testProj\webpack-loader\use-loader\src\index.js
3PID=15988  E:\testProj\webpack-loader\use-loader\src\index2.js
4PID=15988  E:\testProj\webpack-loader\use-loader\src\index3.js
5
6// 使用thread-loader
7PID=25820  E:\testProj\webpack-loader\use-loader\src\index.js
8PID=25820  E:\testProj\webpack-loader\use-loader\src\index2.js
9PID=7808  E:\testProj\webpack-loader\use-loader\src\index3.js
10

loader配置方式

通过前面对于几个loader的介绍,我们对loader的作用已经有了简单的了解。这里我们再描述一下,loader在Webpack中是如何配置的。

基础配置

loader主要的配置方式是通过module.rules进行配置。但这个配置项并不是专供loader使用的,而是负责Webpack模块的规则配置。parser和generator(解析器和生成器)也是用rules等选项进行配置。这里我们主要介绍和loader相关的配置项,这一节会提到这些配置:

  • Rule.test 匹配模块规则
  • Rule.use 应用于模块的loader配置
  • Rule.loader 应用模块的单个loader配置
  • Rule.include 引入符合条件的模块
  • Rule.exclude 排除符合条件的模块
  • Rule.issuer 匹配模块请求者的路径

这里列举几个简单的配置实例,部分示例是前面提到过的:

1module.exports = {
2  module: {
3    rules: [
4      // 单个loader写法,使用use
5      {
6        test: /\.xml$/,
7        use: "xml-loader",
8      },
9      // 单个loader写法
10      {
11        test: /\.xml$/,
12        loader: "xml-loader",
13      },
14      // 多个loader写法
15      {
16        test: /\.css$/,
17        use: [ "style-loader", "css-loader" ],
18      },
19      // 排除模块
20      {
21        test: /\.js$/,
22        exclude: /node_modules/,
23        use: "babel-loader",
24      },
25    ],
26  },
27};
28

loader还可以接受参数,此时需要修改为对象写法。

1module.exports = {
2  module: {
3    rules: [
4      // loader接受参数
5      {
6        test: /\.css$/i,
7        use: [
8          "style-loader",
9          { loader: "css-loader", options: { modules: true } },
10        ],
11      },
12      // 单个loader时的简化写法
13      {
14        test: /\.css$/i,
15        loader: 'css-loader',
16        options: {
17          modules: true,
18        },
19      },
20    ],
21  },
22};
23

上面几个路径相关的匹配参数test, include, exclude等,匹配的都是我们要引入的被loader处理的模块路径。例如我们在src/index.js中引入src/index.xml,那么匹配的路径就是src/index.xml。而issuer这个参数,匹配的却是引入者的路径,例如src/index.js。这里举个配置的例子:

1// 生效,成功编译
2{
3  test: /\.xml$/,
4  issuer: /index\.js/,
5  loader: "xml-loader",
6},
7// 失效,编译失败
8{
9  test: /\.xml$/,
10  issuer: /123\.js/,
11  loader: "xml-loader",
12},
13

顺序和优先级

前面的配置中我们看到,同一个模块可以接收多个loader,这些loader依次对模块代码进行处理。最常见的是loader配置为数组,这时候是从后往前执行。例如我们希望编译SCSS文件,则配置和执行顺序如下:

1{
2  test: /\.scss$/i,
3  use: ["style-loader", "css-loader", "sass-loader"],
4},
5
  1. sass-loader 将SCSS编译为CSS -> 传给下一级
  2. css-loader 解析CSS代码,作为一个JS模块 传给下一级
  3. style-loader 创建style样式,将CSS代码插入到HTML中

可以看到,每个loader实际上只做一件事情。除了顺序之外,使用Rule.enforce还可以配置优先级,pre表示高优先级,未设置表示普通优先级,post表示低优先级。优先级越高的越早执行。例如我们希望共享编译CSS和SCSS的配置,可以利用优先级这样写:

1{
2  test: /\.scss$/i,
3  enforce: "pre",
4  use: ["sass-loader"],
5},
6{
7  test: /\.(scss|css)$/i,
8  use: ["style-loader", "css-loader"],
9},
10

这样,对于CSS文件,仅仅执行css-loader, style-loader两个。对于SCSS文件,在前面多执行了一个sass-loader。这样两种文件都能得到妥善处理。

函数形式

use属性支持函数形式,可以自定义启用loader的逻辑:

1{
2  test: /\.xml$/,
3  use: (info) => {
4    console.log(info);
5    return ["xml-loader"];
6  },
7},
8/*
9{
10  resource: 'E:\\testProj\\webpack-loader\\use-loader\\src\\index.xml',
11  realResource: 'E:\\testProj\\webpack-loader\\use-loader\\src\\index.xml',
12  phase: 'evaluation',
13  dependency: 'esm',
14  descriptionData: {
15    name: 'webpack-loader',
16    scripts: { build: 'webpack' },
17    devDependencies: { ... },
18    ...
19  },
20  issuer: 'E:\\testProj\\webpack-loader\\use-loader\\src\\index.js',
21  ...
22}
23*/
24

返回值与直接配置use参数一致。info参数可以拿到部分项目和模块信息,这里列举部分属性的含义:

  • resource 被加载的模块路径
  • issuer 引入者的路径
  • descriptionData package.json中的信息

oneOf和嵌套rules

使用oneOf和rules属性,可以实现嵌套rule。首先使用OneOf可以配置多个规则,只有第一个匹配到的规则生效:

1{
2  test: /\.(scss|css)$/i,
3  oneOf: [
4    {
5      test: /\.scss$/i,
6      use: ["style-loader", "css-loader", "sass-loader"],
7    },
8    {
9      test: /\.css$/i,
10      use: ["style-loader", "css-loader"],
11    },
12  ],
13}
14

上面的父规则同时匹配SCSS和CSS文件,但是子规则却区分了两条,不同的文件匹配不同的规则。下面再来举例一个嵌套rule的例子:

1{
2  test: /\.scss$/i,
3  rules: [
4    {
5      test: /\.m\.scss$/i,
6      use: [
7        "style-loader",
8        {
9          loader: "css-loader",
10          options: {
11            modules: true,
12          },
13        },
14        "sass-loader",
15      ],
16    },
17    {
18      test: /\.c\.css$/i,
19      use: ["style-loader", "css-loader", "sass-loader"],
20    },
21  ],
22},
23

上面的例子中,父rule识别了SCSS文件,子rule又根据不同文件名采取不同的处理方式。通过oneOf和嵌套rules的能力,可以更好的组织Rule配置。

内联方式

除了Webpack配置文件中配置外,loader还可以配置在引入模块的路径处,这样可以对部分特性模块使用特殊配置。我们先删除webpack.config.js中module相关的所有内容,然后修改引入模块的路径:

1// 单个loader
2import data from 'xml-loader!./index.xml';
3// 多个loader
4import 'style-loader!css-loader!./index.css';
5// loader配置
6import { abc } from "style-loader!css-loader?modules=local!sass-loader!./index.scss";
7// 另一种loader配置
8import { abc } from 'style-loader!css-loader?{"modules": true}!sass-loader!./index.scss';
9

可以看到,将loader写在路径前面,通过感叹号分隔,即可以内联方式使用loader。多个loader时执行顺序依然是从后到前,和rules中一致。同时内联方式也支持配置,可以使用类似url查询参数的格式,例如?a=xx&b=xx。又或者是一个JSON数据,例如?{"modules": true}。

当同时配置了Webpack配置文件和内联方式时,它们两个必定会冲突。内联模式也提供了一些配置可以禁用配置文件,在最前面增加符号即可:

1// !  禁用普通loader
2import data from '!xml-loader!./index.xml';
3// !! 禁用所有loader
4import data from '!!xml-loader!./index.xml';
5// -! 禁用pre和普通loader
6import data from '-!xml-loader!./index.xml';
7

自定义loader

新建loader

loader实际上就是一个处理代码的函数。首先我们新建一个最简单的loader,接收js后缀名的文件,但是只输出,不处理。新建loader/abc.js,内容就是loader的本体:

1module.exports = function abcLoader(src) {
2  console.log(src);
3  return src;
4}
5

然后在webpack.config.js中配置这个loader:

1module: {
2  rules: [
3    {
4      test: /\.js$/,
5      use: path.resolve('loader/abc.js'),
6    },
7  ],
8},
9

然后Console和打包后输出的结果如下。可以看到,loader函数接受的src实际上就是代码字符串本身。

1// Console输出
2function genEle(test, className) {
3  const div = document.createElement("div");
4  div.className = className;
5  div.textContent = test;
6  document.body.appendChild(div);
7}
8genEle("jzplp1", "abc");
9
10// 打包后文件内容
11!(function (e, t) {
12  const n = document.createElement("div");
13  ((n.className = t), (n.textContent = "jzplp1"), document.body.appendChild(n));
14})(0, "abc");
15

如果我们不返回src本身,而是对src进行修改,或者返回其它内容,这时候webpack处理的代码也会变化。

1module.exports = function abcLoader(src) {
2  return 'const str = "hello, jzplp!"; console.log(str)';
3}
4

使用上面的loader,无论我们js文件中的代码是什么,输出都是一致的。例如用前面一样的js源码进行尝试,输出如下:

1console.log("hello, jzplp!");
2

了解了这些,我们可以试着做一个处理xml的loader。首先需要安装fast-xml-parser用来处理xml文件。

1const { XMLParser } = require("fast-xml-parser");
2
3module.exports = function abcLoader(src) {
4  const parser = new XMLParser();
5  const data = parser.parse(src);
6  return [`export default ${JSON.stringify(data)}`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.export.md);
7};
8

通过loader代码可以看到,我们接收xml字符串,然后处理成JSON数据,再返回一个JavaScript模块,导出JSON数据,这样就完成了xml的接入。再修改webpack.config.js配置:

1module: {
2  rules: [
3    {
4      test: /\.xml$/,
5      use: path.resolve('loader/abc.js'),
6    },
7  ],
8},
9

然后使用前面的xml数据,修改index.js内容。通过打包后输出代码可以看到,xml中的数据内容已经被合并进了输出JavaScript中。

1// index.js
2import data from './index.xml'; 
3console.log(data);
4
5// 打包后输出代码
6(() => {
7  "use strict";
8  console.log({ "?xml": "", note: { to: "jzplp1", from: "jzplp2" } });
9})();
10

参数和异步loader

Webpack提供了很多loader相关的API,在loader函数内部以this.xxx的方式执行。从本节开始,我们逐步介绍一些。首先是loader可以接受参数,使用this.getOptions读取。

1const { XMLParser } = require("fast-xml-parser");
2
3module.exports = function abcLoader(src) {
4  const params = this.getOptions();
5  console.log(params);
6  const parser = new XMLParser();
7  const data = parser.parse(src);
8  return [`export default ${JSON.stringify(data)}`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.export.md);
9};
10

然后修改Webpack配置,传入loader配置,最后输出结果如下:

1{
2  test: /\.xml$/,
3  use: {
4    loader: path.resolve("loader/abc.js"),
5    options: {
6      abc: "qwe",
7      qaz: 123,
8    },
9  },
10},
11
12/* 输出结果 
13{ abc: 'qwe', qaz: 123 }
14*/
15

除了return返回处理后的数据之外,还可以使用this.callback返回结果,它可以接收更多的参数。

1const { XMLParser } = require("fast-xml-parser");
2
3module.exports = function abcLoader(src, map, meta) {
4  const parser = new XMLParser();
5  const data = parser.parse(src);
6  this.callback(null, [`export default ${JSON.stringify(data)}`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.export.md), map, meta)
7};
8
9// 参数说明
10this.callback(
11  err: Error | null, // Error表示抛出异常,停止编译
12  content: string | Buffer, // 返回的代码
13  sourceMap?: SourceMap, // SourceMap数据
14  meta?: any // loader间可以传递的额外数据,Webpack本身并不使用
15);
16

loader也可以异步返回结果,这时候就必须使用callback了,而且还需要从this.async中获取,执行下面loader,Webpack会异步暂停10秒。

1const { XMLParser } = require("fast-xml-parser");
2
3module.exports = function abcLoader(src, map, meta) {
4  const callback = this.async();
5  setTimeout(() => {
6    const parser = new XMLParser();
7    const data = parser.parse(src);
8    callback(null, [`export default ${JSON.stringify(data)}`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.export.md), map, meta);
9  }, 10000);
10};
11

处理raw数据

设置loader函数的raw属性为true,则接受的是Buffer形式的源码,方便我们处理二进制数据。这里我们尝试写一个将各种类型图片转为Base64的Data URI形式的链接的loader。

1const { fileTypeFromBuffer } = require("file-type");
2
3async function handleImg(buffer) {
4  // 从buffer中找到图片类型
5  const type = await fileTypeFromBuffer(buffer);
6  // 拼合图片数据
7  return [`data:${type.mime};base64,${buffer.toString("base64")}`](https://xplanc.org/primers/document/zh/09.Lua/91.%E5%86%85%E7%BD%AE%E5%87%BD%E6%95%B0/EX.type.md);
8}
9
10module.exports = function imgLoader(buffer) {
11  const callback = this.async();
12  handleImg(buffer).then((data, err) => {
13    if (err) {
14      callback(err);
15      return;
16    }
17    callback(null, [`export default '${data}'`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.export.md));
18  });
19};
20
21module.exports.raw = true;
22

我们创建的loader可以接收多种图片格式,这里在Webpack中进行配置:

1{
2  test: /\.(jpg|png|gif)$/,
3  use: path.resolve("loader/img.js"),
4},
5

然后在源码中引入图片,并将其插入到浏览器中,这里尝试了jpg和png类型的图片:

1import jpg from './1.jpg';
2import png from './2.png';
3
4function genEle(test) {
5  const img = document.createElement("img");
6  img.src = test;
7  document.body.appendChild(img);
8}
9genEle(jpg);
10genEle(png);
11

打包后查看生成代码,发现图片已经被转成了Base64的Data URI形式,在浏览器中打开,图片可以正常展示。

1// 生成代码
2(() => {
3  "use strict";
4  function A(A) {
5    const Q = document.createElement("img");
6    ((Q.src = A), document.body.appendChild(Q));
7  }
8  (A(
9    "data:image/jpeg;base64,/9j/4AAQSkZJRgA...", // 太长省略
10  ),
11    A(
12      "data:image/png;base64,iVBORw0KGgoAAAA....", // 太长省略
13    ));
14})();
15

执行顺序

前面我们介绍过loader的执行顺序,是从右往左依次执行,左边的loader获取的是右边loader的执行结果。这里我们将前面处理图片的loader拆成两个,实践一下执行顺序。

1// loader/base64.js
2const { fileTypeFromBuffer } = require("file-type");
3async function handleBuffer(buffer) {
4  const type = await fileTypeFromBuffer(buffer);
5  return {
6    data: buffer.toString("base64"),
7    mime: type.mime,
8  };
9}
10module.exports = function base64Loader(buffer) {
11  console.log('base64Loader');
12  const callback = this.async();
13  handleBuffer(buffer).then((data, err) => {
14    if (err) {
15      callback(err);
16      return;
17    }
18    callback(null, data.data, null, { mime: data.mime });
19  });
20};
21module.exports.raw = true;
22
23// loader/imgurl.js
24module.exports = function imgUrlLoader(data, map, meta) {
25  console.log('imgUrlLoader');
26  return [`export default 'data:${meta?.mime};base64,${data}'`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.export.md);
27};
28

可以看到,第一个base64Loader将Buffer数据转为base64数据,第二个imgUrlLoader接收base64格式的字符串数据,返回Base64的Data URI形式的链接。中间还使用了meta作为loader之间数据传输的手段。然后修改Webpack配置,观察打包结果。

1{
2  test: /\.(jpg|png|gif)$/,
3  use: [
4    path.resolve("loader/imgurl.js"),
5    path.resolve("loader/base64.js"),
6  ],
7},
8
9/* Console输出结果
10base64Loader
11imgUrlLoader
12base64Loader
13imgUrlLoader
14*/
15

可以看到,loader的执行顺序确实是从右向左,且因为有两个图片,所以每个loader被调用过两次。loader的函数还可以设置一个pitch方法,这个方法是在本次所有loader执行前被调用,而且是从左向右调用,与正式loader函数的调用顺序相反。

1// loader/base64.js
2const { fileTypeFromBuffer } = require("file-type");
3async function handleBuffer(buffer) {
4  const type = await fileTypeFromBuffer(buffer);
5  return {
6    data: buffer.toString("base64"),
7    mime: type.mime,
8  };
9}
10module.exports = function base64Loader(buffer) {
11  console.log("base64Loader");
12  const callback = this.async();
13  handleBuffer(buffer).then((data, err) => {
14    if (err) {
15      callback(err);
16      return;
17    }
18    callback(null, data.data, null, { mime: data.mime });
19  });
20};
21module.exports.raw = true;
22module.exports.pitch = function pitch(
23  remainingRequest,
24  precedingRequest,
25  data,
26) {
27  console.log("pitch base64Loader");
28  console.log(remainingRequest, "|", precedingRequest, "|", data);
29};
30
31
32// loader/imgurl.js
33module.exports = function imgUrlLoader(data, map, meta) {
34  console.log("imgUrlLoader");
35  return [`export default 'data:${meta?.mime};base64,${data}'`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.export.md);
36};
37module.exports.pitch = function pitch(
38  remainingRequest,
39  precedingRequest,
40  data,
41) {
42  console.log("pitch imgUrlLoader");
43  console.log(remainingRequest, "|", precedingRequest, "|", data);
44};
45
46/* Console输出结果
47pitch imgUrlLoader
48E:\testProj\webpack-loader\use-loader\loader\base64.js!E:\testProj\webpack-loader\use-loader\src\1.jpg |  | {}
49pitch base64Loader
50E:\testProj\webpack-loader\use-loader\src\1.jpg | E:\testProj\webpack-loader\use-loader\loader\imgurl.js | {}
51pitch imgUrlLoader
52E:\testProj\webpack-loader\use-loader\loader\base64.js!E:\testProj\webpack-loader\use-loader\src\2.png |  | {}
53pitch base64Loader
54E:\testProj\webpack-loader\use-loader\src\2.png | E:\testProj\webpack-loader\use-loader\loader\imgurl.js | {}
55base64Loader
56imgUrlLoader
57base64Loader
58imgUrlLoader
59*/
60

可以看到,pitch方法调用更早,且顺序与正式loader函数的调用顺序相反。上面还输出了pitch函数拿到的参数,这里列举下:

  • remainingRequest 表示调用完这个loader之后,还需要调用的loader,以!作为分隔符,包含引入文件本身路径
  • precedingRequest 表示调用这个loader之前调用的loader,以!作为分隔符,包含引入文件本身路径
  • data 在pitch函数和loader函数中传递数据,下面举个例子:
1module.exports = function imgUrlLoader(data, map, meta) {
2  console.log(this.data);
3  // 其它代码
4};
5
6module.exports.pitch = function pitch() {
7  data.value = 'hello, jzplp';
8};
9
10/* Console输出结果
11{ value: 'hello, jzplp' }
12*/
13

如果在pitch中返回数据,则可以中断loader的执行流程,这个数据直接作为loader输出的代码,且后面的loader不会被执行,但注意它“之前”的loader还是会被执行的。这里我们先修改后执行的imgUrlLoader试一下:

1module.exports = function imgUrlLoader(data, map, meta) {
2  console.log("imgUrlLoader");
3  return [`export default 'data:${meta?.mime};base64,${data}'`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.export.md);
4};
5module.exports.pitch = function pitch() {
6  console.log("pitch imgUrlLoader");
7  return 'export default "hello, jzplp"';
8};
9
10/* Console输出结果
11pitch imgUrlLoader
12pitch imgUrlLoader
13*/
14
15// 生成代码
16(() => {
17  "use strict";
18  function e(e) {
19    const l = document.createElement("img");
20    ((l.src = e), document.body.appendChild(l));
21  }
22  (e("hello, jzplp"), e("hello, jzplp"));
23})();
24

可以看到,这里仅执行了imgUrlLoader的pitch方法,任何一个loader函数都没被执行到。我们再换成base64Loader试一下:

1const { fileTypeFromBuffer } = require("file-type");
2async function handleBuffer(buffer) {
3  const type = await fileTypeFromBuffer(buffer);
4  return {
5    data: buffer.toString("base64"),
6    mime: type.mime,
7  };
8}
9module.exports = function base64Loader(buffer) {
10  console.log("base64Loader");
11  const callback = this.async();
12  handleBuffer(buffer).then((data, err) => {
13    if (err) {
14      callback(err);
15      return;
16    }
17    callback(null, data.data, null, { mime: data.mime });
18  });
19};
20module.exports.raw = true;
21module.exports.pitch = function pitch() {
22  console.log("pitch base64Loader");
23  return 'export default "hello, jzplp"';
24};
25
26/* Console输出结果
27pitch imgUrlLoader
28pitch base64Loader
29imgUrlLoader
30pitch imgUrlLoader
31pitch base64Loader
32imgUrlLoader
33*/
34
35// 生成代码
36(() => {
37  "use strict";
38  function e(e) {
39    const t = document.createElement("img");
40    ((t.src = e), document.body.appendChild(t));
41  }
42  (e('data:undefined;base64,export default "hello, jzplp"'),
43    e('data:undefined;base64,export default "hello, jzplp"'));
44})();
45

通过执行顺序和生成代码可以明显看到,base64Loader在后面,所以它在pitch返回之后,依然执行了前面的imgUrlLoader,且pitch返回的数据直接作为了前面loader的输入数据被处理。

参考


Webpack如何实现万物皆可import?loader的使用/配置/手写实践》 是转载文章,点击查看原文


相关推荐


Android 离线优先架构实践:网络只是本地数据库的同步触发器
潜龙勿用之化骨龙2026/5/29

本文基于对 Learn-Kotlin-Coroutines 的工程化重构,记录从「请求式架构」走向「响应式单一数据源(SSOT)」的完整思路与实现方案。 引言:被网络绑架的 UI 过去很多 Android 项目的数据流都是这样的: 请求网络 → 拿到数据 → 更新 UI 这种模式在网络稳定时看起来没有问题。但一旦网络变慢、接口超时、页面频繁切换,问题就会迅速暴露: 页面白屏等待 数据状态不一致 本地缓存形同虚设 根本原因在于:UI 的命运被网络状态决定了。 现代 Android 架构正


ODA运维实战:Oracle 19c YJXT PDB表空间在线扩容全过程_20260503
sysrootsa2026/5/6

一、操作时间 2026 年 5 月 3 日 09:30 二、操作环境 平台:Oracle Database Appliance,ODA 数据库版本:Oracle Database 19c,19.25.0.0.0 主机:teierp1 实例:erpcdb1 CDB:ERPCDB PDB:YJXT 存储:ASM +DATA 操作方式:在线扩容 本次操作为 Oracle 表空间在线扩容,不涉及业务数据修改,不需要停库。 三、登录数据库并进入


AI!一种新的AI项目架构思想与尝试(怎么让AI更有效的开发)
无我Code2026/4/27

前言 在2026的今天,AI极大的提升了项目的开发效率,程序员在当下已经不是考虑AI行不行,而是应该如何积极的拥抱AI,虽然AI不是万能的银弹,但是在2026的今天,AI已经可以帮助我们快速的开发一款小而美的应用,或者解决一些简单的功能开发,提升我们的开发进度,减少我们敲击键盘的次数。那么在AI盛行的当下,我们的项目架构自然需要针对性的向AI方向进行调整,让AI能够更容易的理解项目,提升代码准确率已经是当下最需要解决的问题。 项目架构设计 在近期使用AI的过程中,我使用AI搭建了一个python


《 SwiftUI 进阶第8章:表单与设置界面》
90后晨仔2026/4/18

8.1 Form 组件 核心概念 Form 是 SwiftUI 中用于创建表单界面的专用组件,它提供了: 自动的分组和分隔线 自适应的布局 与系统设置一致的外观 支持多种表单控件 基本使用 import SwiftUI struct ContentView: View { var body: some View { NavigationStack { Form { Section {


OpenClaw(龙虾)最强开源对手!Github 40K Star了,又一个爆火的Agent..
AI袋鼠帝2026/4/10

大家好,我是袋鼠帝。 最近几天,不管是国内的开发者社群,还是国外的X,又有一个开源项目的热度简直高得离谱。 根据开源项目飙升榜的数据,它在一个月内的增长率达到了惊人的百分之1237。 仅仅过了两个月时间,它的标星数量就已经突破了40k大关。 在技术社区里,很多人甚至直接把它称为OpenClaw的第一个真正竞争对手。 这个爆火的开源项目,叫做 Hermes Agent,地址 github.com/NousResearc… 是由 Nous Research 团队倾力打造的开源Agent。 今


在 Debian 上部署 ELK 7.17 完整指南
itmanll2026/4/2

Elasticsearch 7.17 是 7.x 系列的最终维护版本,目前仍有大量生产环境集群运行此版本。本指南将详细介绍如何在 Debian 12/13 上完整部署 ELK 7.17 栈(Elasticsearch、Logstash、Kibana)。 环境要求 Debian 12(bookworm)或 Debian 13(trixie)至少 4GB 内存(默认堆内存占用约 2.4GB)开放端口:9200(Elasticsearch HTTP)、9300(Elasticsearch 传输)


【35天从0开始备战蓝桥杯 -- Day5】
小年糕是糕手2026/3/24

🫧个人主页:小年糕是糕手 💫个人专栏:《C++》《Linux》《数据结构》《C语言》 🎨你不能左右天气,但你可以改变心情;你不能改变过去,但你可以决定未来! 目录 一、输入输出 1.1、单组测试用例 1°计算 (a+b)/c 的值 2°与 7 无关的数 1.2、多组测试用例 情况一 1°多组输入a+b II 2°斐波那契数列 3°制糊串 情况二 1°多组输入a+b 2°数字三角形 3°定位查找 情况三 1°字符统计 拓展函数 2°多组数据


OpenClaw“小龙虾”深度解析:60天碾压Linux的AI智能体,从原理到搞定本地部署【Windows系统 + 接入飞书】
燃于AC之乐2026/3/16

👇点击进入作者专栏: 《算法画解》 ✅ 《linux系统编程》✅ 《C++》 ✅ OpenClaw“小龙虾”深度解析:60天碾压Linux的AI智能体,从原理到搞定本地部署【Windows系统 + 接入飞书】 引言:2026年最火爆的开源AI智能体一、OpenClaw是什么?——从“对话”到“动手”的质变1.1 核心定位:长了手脚的大模型1.2 核心能力:一键控制电脑 二、技术原理:AI如何真正“动手干活”?2.1 架构设计2.2 工作流程2.3 技术关键点:上下文窗口压


dt/dd表格解析、URL拼接缺失、ON DUPLICATE字段覆盖、CSS选择器定位——俄罗斯轮胎展爬虫四大技术难关攻克纪实
进击的雷神2026/3/8

一、引言 在俄罗斯展会网站采集中,莫斯科轮胎及橡胶展览会(Expocentr)的网站采用了典型的表格布局和dt/dd结构存储展商信息。本文以该展会参展商信息采集项目为例,深入剖析在开发过程中遇到的四大技术难题,以及我们如何通过创新的技术方案逐一攻克这些难关。 二、技术难点全景图 #mermaid-svg-T86ttY9To1T8fJpx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill


三月,我只想做好这四件事
修己xj2026/2/28

今天是二月的最后一天,也是春节后上班第一周的收官之日。 在我们中国人的观念里,只有过完春节,新的一年才算真正开始。往年的这个时候,我总会兴致勃勃地立下一堆flag,制定满满当当的年度计划。虽然年终盘点时发现大部分都没实现,但来年依旧乐此不疲——仿佛只要把计划写得够漂亮,生活就会自动变好。 但今年不一样了。 家里添了一位新成员,我的身份悄然发生了改变。抱着怀里这个两个月大的小家伙,我突然不想再立那些宏大的flag,也不想做那些看似充实却往往落空的计划了。 一年很长,长到要数着日历过365个日夜;

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读