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
- sass-loader 将SCSS编译为CSS -> 传给下一级
- css-loader 解析CSS代码,作为一个JS模块 传给下一级
- 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 GitHub
github.com/webpack/web… - Webpack 文档
webpack.js.org/ - Webpack 中文文档
webpack.docschina.org/ - 解锁Babel核心功能:从转义语法到插件开发
jzplp.github.io/2025/babel-… - Babel 文档
babeljs.io/ - css-loader Webpack中文文档
webpack.docschina.org/loaders/css… - style-loader Webpack中文文档
webpack.docschina.org/loaders/sty… - thread-loader Webpack中文文档
webpack.docschina.org/loaders/thr… - babel-loader Webpack中文文档
webpack.docschina.org/loaders/bab… - 概念-loader Webpack中文文档
webpack.docschina.org/concepts/lo… - module.rules Webpack中文文档
webpack.docschina.org/configurati… - 编写loader Webpack中文文档
webpack.docschina.org/contribute/… - Loader Interface API Webpack中文文档
webpack.docschina.org/api/loaders…