日常开发,我该掌握哪些webpack loader知识
前言
大家在使用webpack的时候,是不是经常接触webpack loader,每次copy一下loader配置或者从一些脚手架生成了自带的完整的webpack配置,导致很多时候我们就停留在了解或者知道这个东西,往往没有系统的去学习或者总结过webpack相关的内容,本篇聚焦于webpack loader,帮助大家系统的学习webpack loader相关的知识点,学完之后让你对webpack loader有一个相对完整的认知
在本篇文章中,我们将深入探讨webpack loader相关的知识,从为什么要设计loader到怎么具体在项目中使用loader,帮助大家掌握日常开发中的webpack loader相关知识
webpack是一个非常流行的JavaScript模块打包工具,使用它可以将多个JavaScript模块打包成一个或多个bundle文件。webpack loader与webpack plugin不同的点在于,webpack plugin是聚焦webpack构建全流程的增强,而webpack loader则是聚焦构建流程中的资源转化这个点上,在我的看来,loader也可以看成是一种plugin,只不过与webpack的plugin有区别;但是在rollup上我们就能看到其实就一个plugin概念是可以做所有事情的
本文将从以下几个方面介绍webpack loader:
webpack loader是什么?帮助我们了解webpack为什么设计loader机制webpack loader常用配置?帮助我们快速记住一些常用的loader配置webpack loader执行顺序?帮助我们在使用loader组合,避免因为顺序出现错误- 项目内如何使用
loader?帮助我们快速写出符合项目需求的webpack loader配置 - 项目中常用的
loader组合推荐:帮助我们积累日常loader组合使用方式 - 编写自己的
webpack loader: 带着大家手写一个完整的loader
本篇重点在于实践,也就是在于怎么用webpack loader,下一篇原理篇,主要讲webpack loader执行原理,及一些常用loader的原理
loader是什么
loader 本质上是导出函数的 JavaScript 模块,下面我们来具体看看为什么设计loader及为什么说loader本质上就是一个函数
为什么设计loader
webpack本质是是一个模块打包器,将js、css、image等资源 合并打包成一个bundle,一图描述webpack的作用,如下图所示

那么webpack是怎么做到将所有的资源都打包合并到一起的呢?大致原理就是从entry入口模块开始,然后将entry模块解析成ast,然后在遍历ast,获取entry模块的依赖模块,如果有模块依赖则继续重复上述步骤,直到所有的依赖模块都包含进来,如下图所示

但是这里有一个问题,就是webpack默认只能处理js、json这两种静态资源,比如css、image直接由webpack处理就会报错,会中断构建流程,那么webpack为什么不内置支持css、image这些静态资源的解析呢?原因还是webpack不想做这么多的事情,另外无法穷举所有需要被处理的资源类型
所以webpack 设计了loader,loader的作用就是将非js资源转换成webpack能够处理的资源,比如将css经过css-loader之后转换成module.exports = ".wrap: {color: red}",将image转换成module.exports = base64等,经过loader转化之后,这些资源就能够被webpack处理
怎么定义loader
在我个人看来,我认为loader只有两种类型,一种是normal loader,另一种就是picth loader,前者是为普通资源转化成webpack能够处理的资源准备的,后者是在前者的基础上拓展出来的新能力,是对前者的增强;至于inline loader、pre loader、post loader、raw loader、同步 loader、异步loader 都是在这两种loader的基础上衍生出来的
比如在loader转化的时候,有一些比较耗时或者异步操作,又不想阻塞js代码执行,这时候就衍生出
- 同步
loader - 异步
loader
比如normal loader接受的内容默认是字符串类型,我现在想接受的内容是buffer类型,这时候通过设置一个raw属性,来决定loader接受的内容,这时候就衍生出
raw loader
比如normal loader默认执行顺序是从右至左,从下至上,这时候我写在左边,或者写在上边,想改变默认的执行顺序,那么可以通过enforce: pre | post属性,改变loader的执行顺序,这时候就衍生出
pre loaderpost loader
比如我们在处理某个资源的时候,需要准确指定的loader或者跳过loader,这时候就衍生出
inline loader
虽然上面有哪些多种loader的叫法,但是只要我们只要记住,我们在定义一个loader的时候,只有两种形式
// normal loader
module.exports = function normalLoader() {}
// pitch loader
module.exports.pitch = function pitchLoader() {}
下面围绕着这两种loader继续讲解
normal loader
同步 normal loader
// 直接return 返回结果
module.exports = function (content, map, meta) {
return someSyncOperation(content);
};
module.exports = function (content, map, meta) {
// 直接调用this.callback返回内容
return this.callback(null, someSyncOperation(content), map, meta);
};
如果需要返回sourcemap or meta则需要通过this.callback这种方式返回结果
异步 normal loader
loader最初的设计就只是同步loader,但是javascript引擎是单线程,为了尽可能的提升webpack的构建性能,支持异步loader,通过异步非阻塞的方式来转换资源
module.exports = function (content, map, meta) {
var callback = this.async();
someAsyncOperation(content, function (err, result) {
if (err) return callback(err);
callback(null, result, map, meta);
});
};
raw loader
loader默认接受到的content是string,而对于一些图片场景,则需要接受buffer的形式,所以loader通过raw参数来控制content接受的数据类型
module.exports = function (content) {
assert(content instanceof Buffer);
return someSyncOperation(content);
// return value can be a `Buffer` too
// This is also allowed if loader is not "raw"
};
module.exports.raw = true;
pitch loader
为什么设计pitch loader
为什么要设计pitch loader,难道normal loader不能满足业务诉求吗?
我们先回看下loader设计的初衷,是将webpack不能处理的资源比如css等资源转换成webpack能够处理的资源,更多的是一种资源形态的转换,比如css => js,es6+ => es5,ts => js,不会对原始的代码做过多的新增能力的操作,但是有一些场景,我们可能需要在每个loader转换的时候新增一些功能性或者适配性的代码,以满足业务诉求,那么这时候有两种思路:
- 直接在现有的
loader里面新增功能性or适配性代码 - 采用一种新的形式来对现有的代码做增常能力,且不与当前的
loader进行功能耦合
作者最后采用了第二种思路,新增一种pitch loader的概念,类似与dom事件模型中的捕获,先执行pitch loader,然后在执行normal loader,且如果picth loader有返回非undefined的值,则直接中断后续loader的执行,这样就方便对原始模块一些增强功能,而同时不需要改动到原始模块代码
看下webpack作者对于设计picth loader的动机的回答Documentation for pitch vs normal loader isn't very clear
You can/should use it if you don't want to modify the source code, but just insert a module in front for the module. i. e. the bundle-loader uses this. 如果你想不修改原代码,且又想在当前模块之前插入一些模块或者内容,那么你就可以使用pitch loader
我们再来看下作者提到的bundle-loader代码实现
var loaderUtils = require("loader-utils");
module.exports = function() {};
module.exports.pitch = function(remainingRequest) {
...
var result = [
"module.exports = function(cb) {\n",
" require.ensure([], function(require) {\n",
" cb(require(", loaderUtils.stringifyRequest(this, "!!" + remainingRequest), "));\n",
" }" + chunkNameParam + ");\n",
"}"];
return result.join("");
}
picth loader相对normal loader
- 在导出默认方法上不同,
picth loader需要明确导出pitch方法 - 在传入的参数不同,
normal loader传入了内容,pitch loader传入的是loader链路
bundle-loader定义了picth 方法并且在picth 方法内返回了非undefiend值,那么来看下具体的执行顺序,及最终的返回结果
原始代码
import bundle from './file.bundle.js';
bundle((file) => {
file.add();
});
将./file.bundle.js这个js文件,变成懒加载的文件,并且在./file.bundle.js加载完之后,可以直接使用./file.bundle.js暴露出来的属性or方法
bundle-loader是这样做的
// 经过bundle-loader处理之后就变成了,./file.bundle.js 会包裹一层如下代码
module.exports = function (cb) {
require.ensure([], function(require){
cb(require(loaderUtils.stringifyRequest(this, "!!" + remainingRequest)))
}
}
// 相当于 bundle这个函数变成了
function (cb) {
require.ensure([], function(require){
cb(require(loaderUtils.stringifyRequest(this, "!!" + remainingRequest)))
}
}
// 加上"!!" 确保剩下的loader可以正常处理,且禁用rules中匹配到的所有loader
而require(loaderUtils.stringifyRequest(this, "!!" + remainingRequest))
所以整体逻辑就是,在匹配到./file.bundle.js 这个js文件之后,bundle-loader的picth会直接阻断,并返回一层包裹之后的代码,而由于包裹的代码内继续包含了require('!!生效loader!./file.bundle.js'),webpack在第一次通过loader处理完./file.bundle.js之后,然后对内容解析成ast,然后遍历ast,发现reuqire了./file.bundle.js,则直接使用匹配到的行内loader进行处理,最终./file.bundle.js 文件变成了懒加载
用图表示,如下所示

整体看下来,在不改变原有'./file.bundle.js'模块内部代码的条件下,改变了'./file.bundle.js'模块的加载方式
当然除了这个作用之外,在我看来还有对现有loader的功能增强,我们继续往下看
同步 picth loader
// 直接return 返回结果
module.exports.picth = function (remainingRequest) {
return `require(", loaderUtils.stringifyRequest(this, "!!" + remainingRequest), "))`;
};
module.exports.picth = function (remainingRequest) {
// 直接调用this.callback返回内容
return this.callback(null, `require(", loaderUtils.stringifyRequest(this, "!!" + remainingRequest), "))`);
};
异步 picth loader
module.exports.picth = function (remainingRequest) {
var callback = this.async();
someAsyncOperation(content, function (err, result) {
if (err) return callback(err);
callback(null, `自定义操作`);
});
};
pitch loader vs normal loader
现在对pitch loader已经有了更深入的了解,那我们应该知道pitch loader与normal loader针对的场景不一样,normal loader充当的是一个资源转换的角色,不对输入的源代码做一些额外的功能,而picth loader更多的是充当一个对源代码添加一些额外功能的角色或者对现有normal loader功能的增强,比如将模块替换成懒加载模块,比如将css资源通过style标签掺入html中
到这里我们知道,loader本质上是导出函数的 JavaScript 模块,无论是normal loader还是pitch loader
loader常用配置
了解了loader的定义形式之后,我们看看loader常用的配置,以webpack提供的类型说明来讲
匹配规则配置
Rule.test
作用:匹配资源,一般用于粗略过滤
取值:string | RegExp | ((value: string) => boolean)
比如匹配所有的css文件,从入口出发找到的所有css相关文件都会匹配到该条rule
{
module: {
rules: [
{
test: /\.css$/,
},
{
test: function (resource) {
return resource.indexOf('.css') > -1
},
},
]
}
}
Rule.exclude && Rule.include
作用:对test匹配规则的补充,精确匹配
取值:string | RegExp | ((value: string) => boolean)
比如不处理node_modules下的js文件
{
module: {
rules: [
{
test: /\.[tj]sx?$/i,
exclude: /node_modules/,
},
{
test: /\.[tj]sx?$/i,
exclude: function (resource) {
return resource.indexOf('node_modules') > -1
},
},
]
}
}
比如不处理node_modules下的js,但是不包括react-abc、react-bbb
{
module: {
rules: [
{
test: /\.[tj]sx?$/i,
exclude: /node_modules\/(?!react-aaa|react-bbb)/,
}
]
}
}
比如只处理src下的css文件
{
module: {
rules: [
{
test: /\.css$/i,
include: /xxx\/src/,
},
{
test: /\.css$/i,
include: function (resource) {
return resource.indexOf(path.join(__dirname, 'src')) > -1
},
},
]
}
}
Rule.resourceQuery
作用:对test匹配规则的补充,精确匹配
取值:string | RegExp | ((value: string) => boolean)
比如只处理url上包含addAttr的css文件
{
module: {
rules: [
{
test: /\.css$/i,
resourceQuery: /addAttr/,
}
]
}
}
loader配置
Rule.use
作用:设置loader,设置该条rule作用的loader
取值:string | RuleSetUseItem[] | ((data: any) => RuleSetUseItem[])
比如设置处理.css的loader
{
module: {
rules: [
{
test: /\.css$/i,
use: 'css-loader' // 字符串形式
},
{
test: /\.css$/i,
use: ['css-loader'] // 数组形式
},
{
test: /\.css$/i,
use: { // 对象形式
loader: 'css-loader',
options: {}
}
},
{
test: /\.css$/i,
use: [{ // 数组对象形式,建议这种方式,因为在webpack里面最终都会转化成这种方式
loader: 'css-loader',
options: {}
}]
},
]
}
}
Rule.oneOf
作用:设置loader,设置该条rule可以根据不同的条件用不同的loader
取值:RuleSetRule[]
比如我想对css文件做区分处理
{
module: {
rules: [
{
test: /\.css$/,
oneOf: [
{
resourceQuery: /no-postcss/, // foo.css?no-postcss
use: ['css-loader']
},
{
use: ['css-loader', 'postcss-loader']
}
]
}
]
}
}
其它配置
Rule.type
作用:设置资产的处理方式
取值:string
比如在webpack5里面处理.png等这样的静态资源
module: {
rules: [
{
test: /\.png/,
type: 'asset/resource',
parser: {
dataUrlCondition: {
maxSize: 4 * 1024 // 4kb
}
},
generator: {
filename: 'static/[hash][ext][query]'
}
}
]
},
Rule.enforce
作用:控制loader的执行顺序
取值:"pre" | "post"
{
module: {
rules: [
{
test: /\.css$/,
enforce: 'pre',
use: [
{
loader: 'css-loader',
},
]
}
]
}
}
loader 执行顺序
在了解loader的执行顺序之前,一定要弄清楚为什么loader需要有执行顺序?
比如我们需要将less资源转换成webpack能够识别的资源,我们可能需要将less先转换成css、然后在对css进行相应的处理最后转换成能识别的资源,如果我们在一个loader里面把事情都做完,当然最好,那么就不需要与其它loader配合使用,那也就不存在执行顺序的问题,但问题是,我们不仅只考虑less的场景,比如还有sass、styules等等,所以webpck loader遵循单一职责原则,一个loader只做一件事情,然后整个工作由不同的loader协助完成,即降低了单个loader的复杂度,也提升了loader的可维护性
既然一件工作需要多个loader配合使用,那么loader之间的执行顺序就不能有问题,如果顺序颠倒,则会导致最终生成的资源不符合预期,或者在执行的过程中报错,所以保证loader的执行顺序很重要
loader的执行顺序只需要牢记3点即可:
- 相同优先级
normal loader的执行顺序是从右到走,从下到上 - 不同优先级
normal loader的执行顺序是pre > normal > inline > post pitch loader与normal loader的执行顺序完全相反
执行顺序例子
都包含normal loader与pitch loader,且pitch loader无返回非undefined值
use: ['a-loader', 'b-loader', 'c-loader'],
|- a-loader `pitch`
|- b-loader `pitch`
|- c-loader `pitch`
|- requested module is picked up as a dependency
|- c-loader normal execution
|- b-loader normal execution
|- a-loader normal execution
具体流程如下图所示

都包含normal loader与pitch loader,且pitch loader无返回非undefined值,且pitch b-loader返回非undefined值
use: ['a-loader', 'b-loader', 'c-loader'],
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
if (someCondition()) {
return (
'module.exports = {}'
);
}
};
|- a-loader `pitch`
|- b-loader `pitch` returns a module
|- a-loader normal execution
具体流程如下图所示

有pre 及 post loader,且pitch loader无返回非undefined值
use: ['a-loader'],
enforce: 'pre',
use: ['b-loader'],
use: ['c-loader'],
enforce: 'post',
|- c-loader `pitch`
|- b-loader `pitch`
|- a-loader `pitch`
|- requested module is picked up as a dependency
|- a-loader normal execution
|- b-loader normal execution
|- c-loader normal execution
具体流程如下图所示
更多顺序相关的知识,只要自己写一个demo跑一下就知道了
项目内如何使用loader
好了我们已经知道了loader的执行顺序,那么看下项目中我们可以怎么组合使用loader
集中式
module.exports = {
...
module: {
rules: [
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
],
},
}
OR
module.exports = {
...
module: {
rules: [
{
test: /\.less$/,
use: [{
loader: 'style-loader'
}, {
loader: 'css-loader'
}, {
loader: 'less-loader'
}]
},
],
},
}
处理同一类资源的loader,放在一条rule里面
分布式
module.exports = {
...
module: {
rules: [
{
test: /\.(css|less)$/,
use: 'style-loader',
},
{
test: /\.(css|less)$/,
use: 'css-loader',
},
{
test: /\.less$/,
use: 'less-loader',
},
],
},
}
处理同一类资源的loader,放在多条rule里面
内联式
webpack允许在引入模块的时候直接指定loader,这样指定loader的方式称之为inline loader,具体如下所示
import common from 'loader-a!loader-b!loader-c?type=abc!./common.js'
每个loader之前同!隔开,允许携带query参数,最后的模块也使用!隔开
loader的执行顺序与rules中定义的loader执行顺序保持一致
当通过inline loader方式引入的模块,同时命中rules中的loader时,默认场景下二者都会执行,且normal loader执行顺序是 normal > inline,当然允许通过增加前缀来控制rules中的loader是否执行
- 使用
!前缀,禁用rules中匹配到的normal loader
import common from '!loader-a!loader-b!loader-c?type=abc!./common.js'
- 使用
!!前缀,禁用rules中匹配到的所有loader
import common from '!!loader-a!loader-b!loader-c?type=abc!./common.js'
- 使用
-!前缀,禁用rules中匹配到的所有pre loader及normal loader
import common from '-!loader-a!loader-b!loader-c?type=abc!./common.js'
其实集中式、分布式或者内联式,并无哪种更好哪种不好,主要还是看项目,及最终的需求
项目中常用的loader组合推荐
处理js && ts
推荐直接使用babel-loader处理ts,而不是使用ts-loader处理ts,之所以推荐babel-loader处理快之外(因为ts-loader相对于babel-loader多了一层类型检查),我们可以在vscode中使用typescript类型检查,同时也可以通过git hook在代码提交的时候进行类型检查,所以推荐直接使用babel-loader即可
module.exports = {
...
module: {
rules: [
{
test: /\.[tj]sx?$/i,
use: [
{
loader: 'babel-loader'
}
],
},
],
},
}
不推荐直接使用eslint-loader
原因:开发体验不好,很容易中断构建流程,推荐使用vscode编辑器在开发的过程中进行eslint检查,或者通过husky+lint-statge的方式进行eslint检查
module.exports = {
...
module: {
rules: [
{
test: /\.[tj]sx?$/i,
use: [
{
loader: 'eslint-loader'
}
],
},
],
},
}
对于大项目,排查之后,定位到是babel-loader耗时比较长的问题,我们可以使用多进程、swc、esbuild等方式优化,如下所示
推荐1:thread-loader + babel-loader组合
module.exports = {
...
module: {
rules: [
{
test: /\.[tj]sx?$/i,
use: [
{
loader: 'thread-loader',
},
{
loader: 'babel-loader'
}
],
},
],
},
}
推荐2: 使用swc-loader替换babel-loader
module: {
rules: [
{
test: /\.[tj]sx?$/i,
exclude: /(node_modules)/,
use: {
// `.swcrc` can be used to configure swc
- loader: "babel-loader",
+ loader: "swc-loader",
+ options: {
jsc: {
parser: {
syntax: "typescript",
tsx: true,
decorators: true,
},
transform: {
legacyDecorator: true,
},
externalHelpers: true, // 注意这里设置true时,需要在项目下安装@swc/helpers
target: 'es5',
},
env: {
targets: "last 3 major versions, > 0.1%", // 根据项目设置
mode: "usage",
coreJs: "3" // 根据项目选择
},
isModule: 'unknown'
}
}
}
]
}
推荐3: 使用esbuild-loader替换babel-loader
module: {
rules: [
- {
- test: /\.js$/,
- use: 'babel-loader'
- },
+ {
+ test: /\.[tj]sx?$/i,
+ loader: 'esbuild-loader',
+ options: {
+ // JavaScript version to compile to
+ target: 'es2015',
+ loader: 'tsx',
+ }
+ },
...
],
},
更详细的swc-loader、esbuild-loader使用方式可以参考swc与esbuild-将你的构建提速翻倍
处理样式
对于样式的处理,主要涉及到
css-loader:将css资源转化成webpack能够处理的资源style-loader:用于开发环境将css-loader处理后的样式通过style等方式插入到htmlmini-css-extract-plugin.loader: 用于生成环境构建时将css-loader处理后的样式抽离到单独的文件中postcss-loader: 用于处理css,将css添加前缀,支持css新语法等less-loader: 将less转化为css-loader能够处理样式sass-loader: 将sass转化为css-loader能够处理样式
在我们知道了处理样式相关的loader之后,那么我们一般的使用组合如下所示
sass-loader > postcss-loader > css-loader > (mini-css-extract-plugin.loader or style-loader)postcss-loader > css-loader > (mini-css-extract-plugin.loader or style-loader)css-loader > (mini-css-extract-plugin.loader or style-loader)
一般项目配置如下所示
module.exports = {
...
module: {
rules: [
{
test: /\.(css|less|s[a|c]ss)(\?.*)?$/i,
use: [
{
loader: process.env.NODE_ENV === 'development' ? 'style-loader' : miniCssExtractPlugin.loader
},
{
loader: 'css-loader'
},
{
loader: 'postcss-loader'
},
{
loader: 'less-loader'
},
],
},
],
},
}
或者这样写
module.exports = {
...
module: {
rules: [
{
test: /\.(css|less|s[a|c]ss)(\?.*)?$/i,
use: [
{
loader: process.env.NODE_ENV === 'development' ? 'style-loader' : miniCssExtractPlugin.loader
},
],
},
{
test: /\.css$/i,
use: [
{
loader: 'css-loader'
},
{
loader: 'postcss-loader'
},
],
},
{
test: /\.less$/i,
use: [
{
loader: 'css-loader'
},
{
loader: 'postcss-loader'
},
{
loader: 'less-loader'
},
],
},
{
test: /\.s[a|c]ss$/i,
use: [
{
loader: 'css-loader'
},
{
loader: 'postcss-loader'
},
{
loader: 'sass-loader'
},
],
},
],
},
}
你要你清楚了每个loader的作用及loader的执行顺序,那么自己看着组合就行
处理图片
webpack5已经内置了图片的处理能力,所以我们一般只需要image-webpack-loader这个压缩图片的插件就可以
具体配置如下
module.exports = {
...
module: {
rules: [
{
test: /\.(png|jpe?g|gif|bpm|svg|webp)(\?.*)?$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 10kb
},
},
generator: {
filename: 'image/[name].[hash][ext]',
},
use: [
{
loader: 'image-webpack-loader',
options: {
mozjpeg: {
enabled: false,
},
gifsicle: {
interlaced: false,
},
optipng: {
optimizationLevel: 7,
},
pngquant: {
quality: [0.65, 0.9],
speed: 4,
}
},
},
],
},
],
},
}
编写自己的webpack loader
到这里我们已经知道,webpack loader只有两种写法normal loader与picth loader,所以我们在写自己的loader的时候可以如下分析
- 写这个
loader的作用是什么,根据自己的需求确认是normal loader还是picth loader,还是二者相结合 loader内的逻辑,是不是涉及到网络请求、文件IO等比较耗时的操作,如果是推荐使用异步loader,反之使用同步loadernormal loader确认是否接受到的内容是buffer类型,如果是则设置raw属性,反之不需要设置raw属性- 最后就是实现具体的业务逻辑
markdown-loader
需求:在markdown中直接渲染react组件,并同时能够调试react组件,类似dumi文档那样,又不需要那么多能力,所以自己简单写一个
不依赖现有loader,直接定义成normal loader,然后借助@mdx-js/mdx实现markdown中直接渲染react组件的能力,@mdx-js/mdx处理markdown可能有点耗时,所以选择异步loader
loader context
loader提供了上下文,供loader在转换资源的时候可以做更多的事情,常用的loader上下文字段
this.addDependency()向webpack模块图中添加新的依赖项this.async()获取异步callback具柄this.cacheable()开启loader缓存this.callback()返回loader结果this.data获取loader之前传递的数据this.emitError()向webpack输出一个错误this.emitFile()向webpack最终的assets列表中输出一个新的assetthis.fs获取webpack内部的inputFileSystemthis.getOptions()获取loader传入参数this.getResolve()获取新的module resolve函数this.query获取loader?后面的query参数this.request获取当前包含所有loader的绝对路径this.resolve()获取模块绝对路径this.resource获取当前被loader处理的模块绝对路径,包含query参数this.resourcePath获取当前被loader处理的模块绝对路径,不包含query参数this.resourceQuery获取当前被loader处理的模块绝对路径上的query参数this._module获取当前被loader处理的module实例对象this._compilaction获取当前webpack构建过程中的compilaction对象this._compiler获取当前webpack构建过程中的compiler对象
实现伪代码
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkStringify from 'remark-stringify';
import frontmatter from 'remark-frontmatter';
import codePlugin from './plugin/code';
import yaml from './plugin/yaml';
import sourceCode from './plugin/sourceCode';
import removeComment from './plugin/removeComment';
function processMdx(code: string, options: { filename: string }) {
const { filename } = options;
const importMap = new Map();
return unified()
.use(remarkParse)
.use(sourceCode)
.use(codePlugin)
.process(code)
return {
code: res.value,
data: res.data,
};
});
}
export function loader(value: string) {
const defaultConfig = {
jsxRuntime: 'classic',
format: 'mdx',
remarkPlugins: [remarkGfm, math],
rehypePlugins: [deleteSemicolon, addWrap, mathjax, table, p, slug, headingLink, link, img, [meta, { filename: this.resourcePath }], rehypePrism],
};
// 调用this.async()获取callback具柄,让normal loader成为异步loader
const callback = this.async();
const defaults = this.sourceMap ? { SourceMapGenerator, ...defaultConfig } : { ...defaultConfig };
// 获取传入的参数
const options = this.getOptions();
// 对markdown语法进行对应的转化
processMdx(value, { filename: this.resourcePath} )
.then(({ code, data }: { code: string; data: any}) => {
return compile({ value: code, path: this.resourcePath }, { ...defaults, ...options }).then(
(file) => {
// 通过callback具柄返回当前loader处理之后的内容
callback(null, file.value, file.map);
},
);
}).catch(callback);
}
使用
{
module: {
rules: [
{
test: /\.mdx?$/i,
use: [{ // 数组对象形式,建议这种方式,因为在webpack里面最终都会转化成数组形式
loader: 'babel-loader',
options: {}
},{
loader: path.join(__dirname, './markdown-loader.js'),
options: {}
}]
},
]
}
}
总结
日常开发掌握以下知识点就足以熟练使用webpack loader
loader的作用就是资源转化。loader可以转换各种类型的资源,并在 webpack 构建过程中让webpack可以正确使用这些资源。
loader 分为normal loader与picth loader。然后又根据不同的场景进行衍生,衍生出了inline loader、raw loader、pre loader、post loader,但是本质上还是normal loader与picth loader
normal loader 执行顺序是从左往右,从下往上。每个 loader 接收前一个 loader 的输出作为输入,并返回自己的输出,直到所有 loader 都被调用并返回结果为止。
使用enforce书写可以改变loader的执行顺序,最终的执行顺序以normal loader为例为pre > normal > inline > post
loader遵循单一职责原则,要在项目中使用 loader,一般需要组合使用loader,在使用loader之前我们需要清楚每个loader的职责,然后根据职责组合使用
pitch loader具有阻断能力,用于模块功能增强及loader功能增强
raw loader用于声明normal loader接受的内容要是buffer类型,而不是默认的string类型
感谢各位看官老爷耐心看完,如果觉得对看官老爷有帮助,动动手指头点个👍吧!
转载自:https://juejin.cn/post/7246056370626068535