跳到主要内容

认识

2024年04月06日
柏拉文
越努力,越幸运

一、认识


Webpack 中的 loader 本质上是一个函数, 接收文件内容源代码作为入参, 同时返回处理后的结果。

Webpackloader-runner 核心逻辑为: 在 Webpack 进入构建阶段后,首先接受待处理的资源文件路径, 会通过 IO 接口读取文件内容,之后调用 LoaderRunner 并将文件内容以 source 参数形式传递到 Loader 数组,source 数据在 Loader 数组内首先经过pitch阶段读取资源文件内容再经过normal阶段处理资源文件内容,最终以标准 JavaScript 代码提交给 Webpack 主流程,以此实现内容翻译功能。

二、语法


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有两个值分别为prepost:

    1. 当我们的rules中的规则没有配置enforce参数时,默认为normal loader(默认loader)。

    2. 当我们的rules中的规则配置enforce:'pre'参数时,我们称之它为pre loader(前置loader)。

    3. 当我们的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: 默认是一个空对象{}, 在normalLoaderpitch 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。通过设置 rawtrueloader 可以接收原始的 Buffer。每一个 loader 都可以用 String 或者 Buffer 的形式传递它的处理结果。complier 将会把它们在 loader 之间相互转换。

有时候我们期望以二进制方式读入资源文件,例如在 file-loaderimage-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 ContextemitFile 接口可用于直接写出新的产物文件

function loader(source){
this.emitFile(outputPath,content,null,assetInfo);
return ……
}

借助 emitFile 接口,我们能够在 Webpack 构建主流程之外提交更多产物,这有时候是必要的,除上面提到的 file-loader 外,response-loadermermaid-loader 等也依赖于 emitFile 实现构建功能。

3.7 Loader 通过 this.addDependency() 添加额外依赖

Loader ContextaddDependency 接口用于添加额外的文件依赖,当这些依赖发生变化时,也会触发重新构建

function loader(source){
this.addDependency(……);
}

3.8 Loader 函数上挂载 pitch 函数, 先于 Loader 执行

Webpack 启动后会以一种所谓链式调用的方式按 use 数组顺序从后到前调用 Loader。链式调用有两个问题:

  1. Loader 链条一旦启动之后,需要所有 Loader 都执行完毕才会结束,没有中断的机会 —— 除非显式抛出异常

  2. 某些场景下并不需要关心资源的具体内容,但 Loader 需要在 source 内容被读取出来之后才会执行

为了解决这两个问题,WebpackLoader 基础上叠加了 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 执行链条会被阻断, 立马掉头执行, 直接掉头执行上一个已经执行的loadernormal阶段并且将pitch的返回值传递给下一个normal loader

pitcher loader 应用场景: 如果在loader开发中你的需要依赖loader其他loader,但此时上一个loadernormal函数返回的并不是处理后的资源文件内容而是一段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-loadernormal-loaderpost-loader

import Styles from '!!style-loader!css-loader?modules!./styles.css';

4.8 import inline of disable preLoader and normalLoader

一个 -! 开头, 排除所有 pre-loadernormal-loader

import Styles from '-!style-loader!css-loader?modules!./styles.css';

五、思考与沉淀


5.1 loader 与 plugin 的区别

loader 本质上是一个函数, 在 Webpack 进入构建阶段后,首先接受待处理的资源文件路径, 会通过 IO 接口读取文件内容,之后调用 LoaderRunner 并将文件内容以 source 参数形式传递到 Loader 数组,source 数据在 Loader 数组内首先经过pitch阶段读取资源文件内容再经过normal阶段处理资源文件内容,最终以标准 JavaScript 代码提交给 Webpack 主流程,以此实现内容翻译功能。

Webpack 中的 Plugin 是一个类, 每一个插件都必须提供一个 apply 方法。apply 方法会接收一个 compiler 对象。 编写 Plugin 就是通过 tapable 发布订阅的能力操作 compiler 对象从而影响打包结果。其过程为: 每一个 Pluginapply 方法通过 tapableWebpack 编译准备阶段订阅对应的事件, 当编译执行到一定阶段时发布对应事件, 执行对应事件函数, 从而达到在编译阶段的不同生命周期内去触发对应的 Plugin

因此, pluginloader的区别在于loader只在编译module时执行,而plugin可能在webpack工作流程的各个阶段执行。

参考资料


多角度解析Webpack5之Loader核心原理

你不知道的「pitch loader」应用场景