loader 的本质
loader 就是导出了函数的 JS 模块
,它会返回处理过后的结果给下一个 loader
// ./loaders/myLoader.js
function myLoader(content, map, meta) {
//自定义 loader 代码
}
//导出 myLoader 函数
module.exports = myLoader
上面的代码就是一个自定义 loader,它接受三个参数,其中最重要的是 content
参数:
content
:前一个 loader 处理后的结果,如果是当前 loader 是第一个执行的,content 就是某个模块的源代码- map:可以被 sourceMap 使用的数据
- meta:任意的数据内容
这里只需要关注 content
就行了
webpack 中使用自定义 loader
使用自定义 loader 有三种方式:
方式一:配置 loader 绝对路径
在使用自定义 loader 的时候就指定自定义loader 的绝对路径
const path = require("path");
module.exports = {
// ...
module: {
rules: [
{
test: /\.js$/,
use: [
{
// 以绝对路径的形式匹配自定义 loader
loader: path.resolve(__dirname, "./loaders/loader1.js"),
},
],
},
],
},
};
方式二:resolveLoader.alias 别名的形式
在 webpack 配置文件中,配置 resolveLoader.alias
const path = require("path");
module.exports = {
// ...
// 给自定义 loader 取别名
resolveLoader: {
alias: {
loader1: path.resolve(__dirname, "./loaders/loader1.js"),
},
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: "loader1",
},
],
},
],
},
};
这种形式当自定义loader 多了后,每使用一个自定义 loader 都要别名一次
,太复杂,所以不推荐
方式三(推荐):resolveLoader.modules 匹配规则
在 webpack 配置文件中,配置 resolveLoader.modules
字段指定 loader 的匹配规则
const path = require("path");
module.exports = {
// ...
// 配置 loader 匹配规则
resolveLoader: {
//找 loader 时,先去 loaders 目录下找,第三方 loader 会自动去 node_modules 下找
modules: ["loaders", "node_modules"],
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: "loader1",
},
],
},
],
},
};
loader 的分类
loader 的类型分为四种:
- pre(前置)
- normal(普通)
- inline(行内)
- post(后置)
什么意思?其实 loader 的类型跟 enforce
字段有关,比如当我设置 enforce: "pre"
的时候,此时所使用的 loader 的类型就成了 pre(前置) loader
,同理 post(后置),没有设置 enforce 时,就是 normal loader
而对于 inline loader,它的配置方式是:
// inline loader 通过 loader名 + '!' 的形式
import xxx from "inline-loader1!inline-loader2!/src/xxx.js";
它表示:用 inline-loader1
和 inline-loader2
去解析 xxx.js 文件
loader 执行时的顺序
我们知道,当用多个 loader 去解析某个类型的文件时,会按照 从右到左,从下到上
的顺序执行 loader,那为什么会这样呢?
loader 的两个阶段
首先,loader 分为两个阶段:
-
Pitching
阶段:也就是去执行 loader 身上的pitch 方法
,loader.pitch 可以有返回值,会按照post(后置)、inline(行内)、noraml(普通)、pre(前置)
的类型顺序调用 loaderfunction loader1(content) { return content } //其实就是去执行 loader1 身上的 pitch 方法 loader1.pitch = function() { console.log(' loader1 的 pitching 阶段') }
-
normal
阶段:也就是去执行 loader 本身这个函数,会按照pre(前置)、noraml(普通)、inline(行内)、noraml(普通)、post(后置)
的类型顺序调用 loader,模块源码的转换发生在这个阶段
,也就是上面 return content 的这个地方 -
然后,对于同类型的 loader,他们的顺序才会按照从
右往左,从下到上
的顺序
注意:在 Loader 的运行过程中,如果发现该 Loader 上有 pitch 属性,会先执行 pitch 阶段,再执行 normal 阶段
举个例子:
例子1:
-
首先我先写三个自定义 loader1、loader2、loader3
// ./loaders/loader1.js function loader1(content, map, meta) { console.log("=========loader1 的 normal 阶段=========") return content + "//(loader1)"; } loader1.pitch = function() { console.log("=========loader1 的 pitching 阶段=========") } module.exports = loader1;
// ./loaders/loader2.js function loader2(content, map, meta) { console.log("=========loader2 的 normal 阶段=========") return content + "//(loader2)"; } loader2.pitch = function() { console.log("=========loader2 的 pitching 阶段=========") } module.exports = loader2;
// ./loaders/loader3.js function loader3(content, map, meta) { console.log("=========loader3 的 normal 阶段=========") return content + "//(loader3)"; } loader3.pitch = function() { console.log("=========loader3 的 pitching 阶段=========") } module.exports = loader3;
-
配置 webpack.config.js
const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = { entry: "./src/main.js", output: { path: path.resolve(__dirname, "dist"), filename: "main.js", }, resolveLoader: { modules: ["loaders", "node_modules"], }, module: { rules: [ { test: /\.js$/, //这里,loader1、loader2、loader3 都是 normal loader use: [ "loader1", "loader2", "loader3", ], }, ], }, plugins: [new HtmlWebpackPlugin({ template: "./src/index.html" })], mode: "development", devtool: "source-map", };
-
根据上面的配置,执行
npm run build
,我们发现控制打印的是:
我们来分析一波:
我们在 wepack.config.js 中配置 loader 时,loader1、loader2、loader3 都是 normal loader(因为没有设置 enforce 字段时默认为 normal)
,根据前面我们说的 webpack 会根据 loader 的类型和阶段来决定 loader 的执行顺序
,所以它的流程如下:
- 根据配置的顺序执行 loader
- 发现每一个 loader 都有 pitch 属性,所以都会执行
Pitching 阶段
,按照post(后置)、inline(行内)、noraml(普通)、pre(前置)
的顺序执行 - 依次输出 loader1-Pitching、loader2-Pitching、loader3-Pitching
- 然后,执行
Normal阶段
,会按照pre(前置)、noraml(普通)、inline(行内)、noraml(普通)、post(后置)
的顺序执行,又因为是同类型的,所以会按照从右往左,从后往前
的顺序执行 - 依次输出 loader3-Normal、loader2-Normal、loader1-Normal
图解如下:
那如果我们配置 enforce
,人为干预 loader 的执行顺序呢?
例子2:
// 更改 webpack.config.js 的配置
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/main.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "main.js",
},
resolveLoader: {
modules: ["loaders", "node_modules"],
},
module: {
rules: [
{
test: /\.js$/,
use: [
"loader1",
],
enforce: 'pre' // loader1 是 preLoader
},
{
test: /\.js$/,
use: [
"loader2", // loader2 是 normalLoader
],
},
{
test: /\.js$/,
use: [
"loader3",
],
enforce: 'post' // loader3 是 postLoader
},
],
},
plugins: [new HtmlWebpackPlugin({ template: "./src/index.html" })],
mode: "development",
devtool: "source-map",
};
我们通过 enforce
,将 loader1、loader2、loader3 的类型改变后,执行 npm run build,控制台打印结果如下:
再来分析一波流程:
- 根据配置的顺序执行 loader
- 每个 loader 都有 pitch 属性,所以会执行
Pitching 阶段
,根据post > inline > normal > pre
的顺序执行,此时 loader3 是 postLoader 先输出,loader2是 normalLoader 第二个输出,loader1 是 preLoader 最后一个输出 - 然后执行
Normal
阶段,根据pre > normal > inline > post
的顺序执行,loader1 是 preLoader 最先输出,loader2 是 normalLoader 第二个输出,loader3 是 postLoader 最后输出
图解如下:
所以,loader 的执行顺序我们现在就很清楚了:
-
Pitching 阶段: 调用 loader.pitch 方法,该方法可以有返回值,按照
后置(post) > 行内(inline) > 普通(normal) > 前置(pre)
的顺序调用。 -
Normal 阶段: 执行 loader 本身函数,按照
前置(pre) > 普通(normal) > 行内(inline) > 后置(post)
的顺序调用。模块源码的转换, 发生在这个阶段
Pitching 有返回值的情况
前面说了,loader.pitch 方法可以有返回值,如果有返回值的时候会怎么样呢?
我们把前面的 例子2
中的 loader2 修改一下:
function loader2(content, map, meta) {
console.log("=========loader2 的 normal 阶段=========")
return content + "//(loader2)";
}
loader2.pitch = function() {
console.log("=========loader2 的 pitching 阶段=========")
return 'Jolyne'
}
module.exports = loader2
然后执行 npm run build,控制台输出如下:
来分析一波流程:
- 首先都有 pitch,走
Pitching 阶段
,按照post > inline > normal > pre
的顺序执行 loader - 执行到 loader2 时,它的 pitch 方法
return 'Joylne'
- 跳回到 loader3,执行 loader3 的
Normal 阶段
- 所有 loader 执行完毕
经过分析我们可以得出结论:
在 Pitching 阶段,如果当前 Loader.pitch 有返回值,就直接
结束当前 loader 的 Pitching 阶段
,并直接跳到当前 Loader 执行 pitching 阶段时的前一个 loader 的normal 阶段
,然后继续执行(若无前置 loader,则直接返回)
图解如下:
inlineLoader 的过滤运算符
!
前缀:排除 normal Loader!!
前缀:只使用 inline Loader,其他类型的 loader 被禁用-!
前缀:排除 pre Loader、normal Loader
就拿前面的例子来看:比如 loader1 是 preLoader,loader2 是 normal Loader,loader3 是 post Loader 时:
-
!
前缀//webpack.config.js module.exports = { //... module: { rules: [ { test: /\.js$/, use: [ "loader1", ], }, { test: /\.js$/, use: [ "loader3", ], enforce: 'post' }, ], }, } // ./src/a.js const print = () => { console.log("Jolyne"); }; export default print; // ./src/main.js import print from "!loader2!./a"; // 这里使用 !前缀,排除掉 normal loader const a = 1; print();
执行 npm run build,控制台输出如下:
我们发现,确实排除了 loader1
图解如下:
-
!!
前缀// ./src/main.js import print from "!!loader2!./a"; // 这里使用 !!前缀,只使用 inline loader const a = 1; print();
打包后,控制台输出如下:
图解如下:
-
-!
前缀import print from "!!loader2!./a"; // 这里使用 -!前缀,排除 preLoader、normal Loader const a = 1; print();
打包后,控制台输出如下:
图解如下:
总结
-
loader 本质是一个
导出结果为函数的 JS 模块
,它可以接受前面的 loader 处理后的结果作为参数,能返回处理后的结果 -
loader 根据
enforce
的配置,分为pre(前置)
、normal(普通)
、inline(行内)、post(后置)
四个类型 -
loader 的执行阶段分为两个:
Pitching
阶段、Normal
阶段Pitching
阶段:就是执行 loader 的 pitch 方法,它可以有返回值
,如果有返回值,就直接结束当前 loader 的 Pitching 阶段
,并直接跳到当前 Loader 执行 pitching 阶段时的前一个 loader 的normal 阶段
,然后继续执行(若无前置 loader,则直接返回),该阶段按照post > inline > normal > pre
的顺序执行Noraml
阶段:也就是去执行 loader 本身这个函数,模块源码的转换发生在这个阶段
,该阶段按照pre > normal > inline > post
的顺序执行
-
inline Loader 有
三种
前缀修饰符!
前缀:排除 normal Loader!!
前缀:只使用 inline Loader,其他类型的 loader 被禁用-!
前缀:排除 pre Loader、normal Loader