认识
一、认识
Webpack
中的 loader
本质上是一个函数, 接收文件内容源代码作为入参, 同时返回处理后的结果。
Webpack
使用 loader-runner
来依次调用配置好的 Loader
。Loader
的处理分为两个阶段: 1. Pitch
阶段, 从左到右依次执行所有 Loader
的 pitch
方法, 每个 Loader
的 pitch
可以选择提前返回结果, 若返回结果则会中断后续的 pitch
调用, 否则继续到下一个 Loader
, 如果所有 Loader
的 pitch
均未返回数据, 则会读取原始文件内容, 进入 normal
阶段; 2. Normal
阶段, Loader
的 normal
方法按照从右向左的顺序执行(即倒序执行), 在该阶段, 每个 Loader
对上一步传递的内容进行转换,例如对 ES6
语法进行 Babel
转译、对 TypeScript
代码进行编译等。此过程可能非常消耗 CPU
,尤其是当 Loader
数量多、转换逻辑复杂时(如使用 babel-loader
、eslint-loader
、ts-loader
等时,有时会多次生成 AST
进行校验与转换)。Webpack 5
内置了持久化缓存机制,可以将 Loader
处理、AST
解析等中间结果缓存起来,加快后续增量构建的速度。
二、语法
2.1 配置语法
module.exports = {
module:{
rules: [
{
test: //,
enforce: 'pre',
use: [{loader: 'xx-loader'}]
}
]
}
}
-
test
: 是一个正则表达式,我们会对应的资源文件根据test
的规则去匹配。如果匹配到,那么该文件就会交给对应的loader
去处理。 -
use
:use
表示匹配到test
中匹配对应的文件应该使用哪个loader
的规则去处理,use
可以为一个字符串,也可以为一个数组。额外注意,如果use
为一个数组时表示有多个loader
依次处理匹配的资源,按照从右往左(从下往上
) 的顺序去处理。 -
enforce
:loader
中存在一个enforce
参数标志着loader
的顺序。enforce
有两个值分别为pre
、post
:-
当我们的
rules
中的规则没有配置enforce
参数时,默认为normal loader
(默认loader
)。 -
当我们的
rules
中的规则配置enforce:'pre'
参数时,我们称之它为pre loader
(前置loader
)。 -
当我们的
rules
中的规则配置enforce:'post'
参数时,我们称之它为post loader
(后置loader
)。
-
2.2 编写语法
function loader(sourceCode, sourceMap?, data?){
console.log(this);
}
loader.pitch = function(remainingRequest, previousRequest, data){
}
module.exports = loader;
-
sourceCode
: 资源输入, 对于第一个执行的Loader
为资源文件的内容;后续执行的Loader
则为前一个Loader
的执行结果,可能是字符串,也可能是代码的AST
结构 -
sourceMap
: 可选参数,代码的sourcemap
结构 -
data
: 可选参数,其它需要在Loader
链中传递的信息,比如posthtml/posthtml-loader
就会通过这个参数传递额外的AST
对象 -
this
: 表示上下文对象, 上下文对象将在运行Loader
时以this
方式注入到Loader
函数。有如下属性:-
this.fs
:Compilation
对象的inputFileSystem
属性,我们可以通过这个对象获取更多资源文件的内容 -
this.resource
: 当前文件路径 -
this.resourceQuery
: 文件请求参数,例如import "./a?foo=bar"
的resourceQuery
值为?foo=bar
-
this.async
: 用于声明这是一个异步Loader
,开发者需要通过async
接口返回的callback
函数传递处理结果 -
this.callback
: -
this._compiler
: 用于访问webpack
的当前Compiler
对象 -
this._compilation
: 用于访问webpack
的当前Compilation
对象 -
this.getOptions(schema)
: 用于获取当前Loader
的配置对象。提取给定的loader
选项,接受一个可选的JSON schema
作为参数 -
this.cacheable(true / false)
:Webpack
默认会缓存Loader
的执行结果直到资源或资源依赖发生变化,开发者需要对此有个基本的理解,必要时可以通过this.cachable
显式声明不作缓存 -
this.emitWarning
: 添加警告 -
this.emitError
: 添加错误信息,注意这不会中断Webpack
运行 -
this.emitFile
: 用于直接写出一个产物文件,例如file-loader
依赖该接口写出Chunk
之外的产物; -
addDependency
: 将dep
文件添加为编译依赖,当dep
文件内容发生变化时,会触发当前文件的重新构建
-
-
pitch
:-
remainingRequest
: 表示剩余需要处理的loader
的绝对路径以!
分割组成的字符串。 -
previousRequest
: 表示pitch
阶段已经迭代过的loader
按照!
分割组成的字符串。 -
data
: 默认是一个空对象{}
, 在normalLoader
与pitch Loader
进行交互正是利用了第三个data
参数。
-
三、编写
3.1 通过 this.cacheable() 取消 Loader 缓存
需要注意,Loader
中执行的各种资源内容转译操作通常都是 CPU
密集型 —— 这放在 JavaScript
单线程架构下可能导致性能问题;又或者异步 Loader
会挂起后续的加载器队列直到异步 Loader
触发回调,稍微不注意就可能导致整个加载器链条的执行时间过长。
为此,Webpack
默认会缓存 Loader
的执行结果直到模块或模块所依赖的其它资源发生变化,我们也可以通过 this.cacheable
接口显式关闭缓存:
module.exports = function(source) {
this.cacheable(false);
// ...
return output;
};
3.2 Loader 通过 this.callback() 返回多个结果
简单的 Loader
可直接 return
语句返回处理结果,复杂场景还可以通过 callback
接口返回更多信息,供下游 Loader
或者 Webpack
本身使用
export default function loader(content, map) {
this.callback(null, content, map);
}
通过 this.callback(null, content, map)
语句,同时返回转译后的内容与 sourcemap
内容。callback
的完整签名如下:
this.callback(
// 异常信息,Loader 正常运行时传递 null 值即可
err: Error | null,
// 转译结果
content: string | Buffer,
// 源码的 sourcemap 信息
sourceMap?: SourceMap,
// 任意需要在 Loader 间传递的值
// 经常用来传递 ast 对象,避免重复解析
data?: any
);
3.3 Loader 通过 this.async() 返回异步结果
涉及到异步或 CPU
密集操作时,Loader
中还可以以异步形式返回处理结果
async function loader(source){
// 1. 调用 this.async() 获取异步回调函数, 此时 Webpack 会将该 Loader 标记为异步加载器, 会挂起当前执行队列直到 callback 被触发;
const callback = this.async();
try{
// 2. 编译模块
const result = await handler(source);
}catch(error){
}
// 3. 编译结束, 调用异步回调 callback 返回处理结果
callback(null,css,map);
}
3.4 Loader 通过 return promise 返回异步结果
涉及到异步或 CPU
密集操作时,Loader
中还可以以异步形式返回处理结果
function loader(source){
return Promise((resolve)=>{
setTimeout(()=>{
resolve(3000);
});
});
}
3.5 通过导出 raw 处理 Loader 二进制资源
默认情况下,资源文件会被转化为 UTF-8
字符串,然后传给 loader
。通过设置 raw
为 true
,loader
可以接收原始的 Buffer
。每一个 loader
都可以用 String
或者 Buffer
的形式传递它的处理结果。complier
将会把它们在 loader
之间相互转换。
有时候我们期望以二进制方式读入资源文件,例如在 file-loader
、image-loader
等场景中,此时只需要添加 export const raw = true
语句即可,如:
module.exports = function (content) {
assert(content instanceof Buffer);
return someSyncOperation(content);
// 返回值也可以是一个 `Buffer`
// 即使不是 "raw",loader 也没问题
};
module.exports.raw = true;
3.6 Loader 通过 this.emitFile() 直接写出文件
Loader Context
的 emitFile
接口可用于直接写出新的产物文件
function loader(source){
this.emitFile(outputPath,content,null,assetInfo);
return ……
}
借助 emitFile
接口,我们能够在 Webpack
构建主流程之外提交更多产物,这有时候是必要的,除上面提到的 file-loader
外,response-loader
、mermaid-loader
等也依赖于 emitFile
实现构建功能。
3.7 Loader 通过 this.addDependency() 添加额外依赖
Loader Context
的 addDependency
接口用于添加额外的文件依赖,当这些依赖发生变化时,也会触发重新构建
function loader(source){
this.addDependency(……);
}
3.8 Loader 函数上挂载 pitch 函数, 先于 Loader 执行
Webpack
启动后会以一种所谓链式调用的方式按 use
数组顺序从后到前调用 Loader
。链式调用有两个问题:
-
Loader
链条一旦启动之后,需要所有Loader
都执行完毕才会结束,没有中断的机会 —— 除非显式抛出异常 -
某些场景下并不需要关心资源的具体内容,但
Loader
需要在source
内容被读取出来之后才会执行
为了解决这两个问题,Webpack
在 Loader
基础上叠加了 pitch
的概念。Webpack
允许在 Loader
函数上挂载名为 pitch
的函数,运行时 pitch
会比 Loader
本身更早执行。
function loader1(sourceCode){
console.log("loader1");
return sourceCode + `\n // loader1 处理`;
}
loader1.pitch = function(remainingRequest, previousRequest, data){
}
export default loader1;
-
remainingRequest
: 当前loader
之后的资源请求字符串 -
previousRequest
: 在执行当前loader
之前经历过的loader
列表 -
data
: 与Loader
函数的data
相同,用于传递需要在Loader
传播的信息
实现上,Loader
链条执行过程分三个阶段:pitch
、解析资源、执行。pitch
阶段按配置顺序从左到右逐个执行 loader.pitch
函数(如果有的话),开发者可以在 pitch
返回任意值中断后续的链路的执行。纯函数.pitch
如果返回一个非 undefined
的值会发生熔断, loader
执行链条会被阻断, 立马掉头执行, 直接掉头执行上一个已经执行的loader
的normal
阶段并且将pitch
的返回值传递给下一个normal loader
pitcher loader
应用场景: 如果在loader
开发中你的需要依赖loader
其他loader
,但此时上一个loader
的normal
函数返回的并不是处理后的资源文件内容而是一段js
脚本, 可以将逻辑放在 pitch
中, 通过 import someThing from !!${remainingRequest}
的方式, 将脚本交给webpack
去编译执行。
四、配置
4.1 modules.rules loaderName
module.exports = {
module: {
rules: [
{
test:/\.js$/,
loader: "xx-loader"
}
]
}
}
module.exports = {
module: {
rules: [
{
test:/\.js$/,
use: ['xx-loader','xx-loader']
}
]
}
}
module.exports = {
module: {
rules: [
{
test:/\.js$/,
use: [{ loader: 'xx-loader' }]
}
]
}
}
4.2 module.rules loaderPath
const path = require('path')
// webpack.config.js
module.exports = {
...
module: {
rules: [
{
test:/\.js$/,
// .js后缀其实可以省略,后续我们会为大家说明这里如何配置loader的模块查找规则
loader: path.resolve(__dirname,'../loaders/babel-loader.js')
}
]
}
}
这里我们在loader
参数中传入一个绝对路径的形式,直接去该路径查找对应的loader
所在的js
文件。
4.3 module.rules resolveLoader.alias
const path = require('path')
// webpack.config.js
module.exports = {
...
resolveLoader: {
alias: {
'babel-loader': path.resolve(__dirname,'../loaders/babel-loader.js')
}
},
module: {
rules: [
{
test:/\.js$/,
loader: 'babel-loader'
}
]
}
}
此时,当webpack
在解析到loader
中使用babel-loader
时,查找到alias
中定义了babel-loader
的文件路径。就会按照这个路径查找到对应的loader
文件从而使用该文件进行处理。
4.4 module.rules resolveLoader.modules
const path = require('path')
// webpack.config.js
module.exports = {
...
resolveLoader: {
modules: [ path.resolve(__dirname,'../loaders/') ]
},
module: {
rules: [
{
test:/\.js$/,
loader: 'babel-loader'
}
]
}
}
4.5 import inline loader
引用资源时,通过!
分割使用loader
的方式称为行内loader
。
import Styles from 'style-loader!css-loader?modules!./styles.css';
4.6 import inline of disable normal loader
单个 !
开头, 排除所有 normal-loader
import Styles from '!style-loader!css-loader?modules!./styles.css';
4.7 import inline of disable all loader
两个 !!
开头, 排除所有 pre-loader
、normal-loader
、post-loader
import Styles from '!!style-loader!css-loader?modules!./styles.css';
4.8 import inline of disable preLoader and normalLoader
一个 -!
开头, 排除所有 pre-loader
、normal-loader
import Styles from '-!style-loader!css-loader?modules!./styles.css';
五、思考与沉淀
5.1 loader 与 plugin 的区别
Webpack
中的 loader
本质上是一个函数, 接收文件内容源代码作为入参, 同时返回处理后的结果。Loader
用于转换模块的源代码。它们在模块被加入到依赖图之前执行,主要负责对文件内容进行预处理,如将 TypeScript
转译为 JavaScript
、将 SCSS
转换为 CSS
等。Loader
按照配置的规则匹配特定文件,依次处理文件内容,支持链式调用(多个 Loader
串联执行)。Loader
的执行顺序为: Pitch
阶段, 从左到右顺序执行, Normal
阶段, 从右到左逆序执行。
function loader(sourceCode, sourceMap?, data?){
console.log(this);
}
loader.pitch = function(remainingRequest, previousRequest, data){
}
module.exports = loader;
Webpack
中的 Plugin
是一个类, 每一个插件都必须提供一个 apply
方法。apply
方法会接收一个 compiler
对象。Plugin
用于扩展 Webpack
的整体构建流程和功能, 它们可以在整个编译周期内介入, 从初始化、编译、打包到输出等各个环节发挥作用。比如在 compiler.run
阶段、compiler.compile
阶段、compiler.make
阶段、compiler.emit
阶段、compilation.optimizeAssets
等。
class MyPlugin {
constructor(options){
}
apply(compiler){
compiler.hook.compilation.tap("MyPlugin", (compilation)=>{
});
}
}