日常开发,我该掌握哪些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 loader
post 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
等方式插入到html
mini-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
,反之使用同步loader
normal 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
列表中输出一个新的asset
this.fs
获取webpack
内部的inputFileSystem
this.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