Loader进阶
rule.enforce
该参数为webpack
对loader
配置时的可选配置项,其参数值有三种:pre
(前置),normal
(默认),post
(后置)。
除此之外还有一种叫做inline
(行内,也称内联)
使用方式及执行顺序
通过之前的学习,有了解到loader
的执行顺序应该是从右到左,从下到上
。
不过在了解完这一节之后就开始不一样了。(题外话:虽然我觉得大部分项目一般都不会使用这个配置,不过也可能是我接触的项目太少了)
module: {
rules: [
{
enforce: "pre",
test: /\.js$/,
loader: "loader1",
},
{
// 写与不写都一样,一般来说normal都是不写的,因为它是默认值
enforce: "normal",
test: /\.js$/,
loader: "loader2",
},
{
enforce: "post",
test: /\.js$/,
loader: "loader3",
},
],
},
如果无视以上代码中的enforce
参数,那么以上loader
的执行顺序应该是loader3
=> loader2
=> loader1
。
但是如果考虑到这个配置项的时候,他的执行顺序就变成了loader1
=> loader2
=> loader3
。
inline loader
而inline
的使用方式比较特殊,它是在每次import
样式的时候指定loader
。
import Styles from 'style-loader!css-loader!./styles.css';
含义:
- 使用
css-loader
和style-loader
处理styles.css
文件 - 通过
!
将资源中的loader
连接起来
执行顺序:
// 添加 ! 前缀,跳过 normal loader
import Styles from '!style-loader!css-loader?modules!./styles.css';
// 添加 -! 前缀,跳过 pre/normal loader
import Styles from '-!style-loader!css-loader?modules!./styles.css';
// 添加 !! 前缀,跳过 pre/normal/post loader
import Styles from '!!style-loader!css-loader?modules!./styles.css';
如果不配置任何前缀的话,执行优先级是:pre
> normal
> inline
> post
。不推荐使用inline loader
注意事项:要区分!
是作为连接符存在还是作为配置方式存在,不要混淆。
如何编写一个Loader(简单版)
写在前面
loader
对于webpack而言,就像是webpack
官网画的图那样,多条支流汇总成为一条(姑且成为一条)大的河流。而每一条支流,就是一个或多个loader
的执行过程。
而每个开发者对于河流的定义是不一样的,可能我喜欢这个,他喜欢那个。所以各种各样的loader
会帮助开发者去处理得到自己期望的结果。
了解loader
既然要编写一个loader
,那就得了解一下loader
。
module: {
rules: [
{
test: /\.js$/,
loader: "loader1",
},
{
test: /\.js$/,
loader: "loader2",
},
{
test: /\.js$/,
loader: "loader3",
},
],
},
还是拿上面的代码来举例,通过上面的学习我们已经知道,JavaScript
文件会先通过loader3
的处理,再经过loader2
的处理,最后经过loader1
的处理。
那么是不是就说明,每个loader
其实就像是流水线上的一环,拿到上层结果,处理之后再传递给下层。
这是非常关键的一点,你要知道loader
最根本的一个原理,因为当你处理过的数据,不能被下层接受使用的话,那你写的loader
就是一个失败品。
开始学习
示例
module.exports = function(content, map, meta) {
console.log('hello world!')
// 本来只想写上面那一行的,但是上面那一行不能很好的代表loader对文件的处理
// 于是写了下面这一段
const now = Date.now()
const newContent= `
/**
* compile time: ${now}
**/
` + content
return newContent;
};
先从参数开始讲起
content
是上层loader传下来的经过处理的内容或者文件原始内容(在没有上层loader的情况下)map
是有关SourceMap的一些东西(此处不多讲,其实是因为我也一知半解)meta
上层loader传过来的数据
函数体内容不用多讲,无非就是如何对文件内容进行处理,最后返回给下层loader
。
而返回的方式要讲一下:
return
直接返回处理后的文件内容给下层。
但是从参数中我们可以看到,其实是有三个参数的,而return
只能返回单个值。这就引出了第二种方式。
this.callback
this.callback(err: Error | null, content: String | Buffer, sourceMap?: SourceMap, meta?: any)
module.exports = function(content, map, meta) {
const now = Date.now()
const newContent= `
/**
* compile time: ${now}
**/
` + content
this.callback(null, newContent);
};
参数形式基本上就比loader
的执行函数入参前面多一个err
错误信息。通过声明规范可以看出content
有两种类型String
和Buffer
。后者一般常见于对资源文件(图片、文档等)的处理。
所以loader最简单的写法就是以上的实例,不考虑SourceMap
的情况下。而最重要的就是处理content
。
loader类型
- 同步Loader 无论是
return
还是this.callback
都可以同步地返回转换后的content
值。(注意事项:同步loader拒绝任何的异步操作) - 异步Loader 顾名思义,不多说了。区别有两个:1.
callback
函数需要通过this.async
来获取且没有return
;2. 记得把callback写对位置(笑)。
module.exports = function(content, map, meta) {
var callback = this.async();
someAsyncOperation(content, function(err, result, sourceMaps, meta) {
if (err) return callback(err);
callback(null, result, sourceMaps, meta);
});
};
- Raw Loader 默认情况下,资源文件会被转化为 UTF-8 字符串,然后传给 loader。通过设置
raw
,loader 可以接收原始的Buffer
。每一个 loader 都可以用String
或者Buffer
的形式传递它的处理结果。Complier 将会把它们在 loader 之间相互转换。
module.exports = function (content) {
assert(content instanceof Buffer);
return someSyncOperation(content);
// 返回值也可以是一个 `Buffer`
// 即使不是 "raw",loader 也没问题
};
module.exports.raw = true;
- Pitching Loader 这个得细说一下,见下一小节
Pitching Loader (越过Loader)
上一小节告诉大家,loader
其实就是一个函数,而pitching loader的区别在于会在原函数的基础上再新增一个pitch方法。
那么这时候loader
其实是存在两个方法的,原来的方法我们称之为loader
方法,新增的方法就称之为pitch
方法。
之前我们一直提到的执行顺序,都只是指loader
方法的执行顺序:
pre
>normal
>inline
>post
- 从右往左,从下往上
而pitch
方法则刚好相反,如下图
post
>inline
>normal
>pre
- 从左往右,从上往下
那我们来研究一下pitch方法的结构
module.exports = function(content) {
console.log(this.data.value) // 42
return someSyncOperation(content, this.data.value);
};
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
data.value = 42;
};
入参:
remainingRequest
剩余需要执行的pitch loaderprecedingRequest
已经执行过得pitch loaderdata
如代码,此时修改data
时,在进入loader
方法时,可以通过this.data
获取
需要注意的是,pitch
是否会返回非空值:
- 返回空值(或无返回) 继续接下来的
pitch
方法 - 返回非空值 会跳过本身的
loader
方法,以及后面所有的loader
方法和pitch
方法
举个栗子: normal-loader1
module.exports = function(content) {
return someSyncOperation(content);
};
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
if (someCondition()) {
// 这个返回值会成为上一个执行pitch的loader中的content参数
return "const a = 1";
// 这个是webpack的例子 有点问题
return "module.exports = require(" + JSON.stringify("-!" + remainingRequest) + ");";
}
};
这时候normal-loader1
的pitch
执行完后,会直接执行上一个loader的loader
方法。
这里讲一下为什么webpack
给的例子有问题,他会以CommonJS
的形式导出一个inline-loader
的使用方式编译文件的一个代码。而参数又是remainingRequest
(剩下未执行需要执行的loader
),所以实际上并未达成一个跳过的效果。
建议小伙伴们都可以去试一下,这件事告诉我们即使是官网也不可信。
this对象
Loader Interface | webpack 中文文档 (docschina.org)
loader API | webpack 中文网 (webpackjs.com)
两个webpack文档,互相对照着看吧,需要用的时候也要考虑一下。
(小声BB:毕竟文档也不一定对)
复刻两个简简单单的loader(毕竟难的我也不会写)
clean-log-loader
module.exports = function (content) {
// 清除文件内容中console.log(xxx)
return content.replace(/console.log(.*);?/g, "");
};
add-message-loader
module.exports = function(content) {
const options = this.getOptions(); // 获取loader中传递的options
const bannerText = `
/**
* author: ${options.author}
* time: ${options.time}
* */
`
return bannerText + content;
}
webpack配置
{
test: /.js$/,
loader: "./loaders/add-message-loader",
options: {
author: "寒拾",
time: '2022-8-22'
},
},