一、loader的本质是什么?

loader 本质上是导出为函数的 JavaScript 模块。loader runner(有兴趣自行了解)会调用此函数,然后将上一个 loader 产生的结果或者资源文件传入进去。函数中的 this 作为上下文会被 webpack 填充,并且 loader runner 中包含一些实用的方法。

如下代码是loader函数

1
2
3
4
5
6
7
8
9
10
11
/**
*
* @param {string|Buffer} content 源文件的内容
* @param {object} [map] 可以被 https://github.com/mozilla/source-map 使用的 SourceMap 数据
* @param {any} [meta] meta 数据,可以是任何内容
*/
function webpackLoader(content, map, meta) {
// 你的 webpack loader 代码
}

module.exports = webpackLoader;

二、webpack loader的作用是什么?

loader 用于对模块的源代码进行转换

解释:

在实际开发过程中,webpack 默认只能打包处理以 .js 后缀名结尾的模块。其他非 .js 后缀名结尾的模块,webpack 默认处理不了,需要调用 loader 加载器才可以正常打包,否则会报错。

需要注意的是,loader返回的必须是js代码(要不然webpack还是处理不了…)。

三、loader的调用顺序

以一下代码为例

1
2
3
4
{
test: /\.css$/i,
use: [ 'style-loader', 'css-loader' ]
},
  1. webpack 默认只能打包处理.js文件,处理不了其它后缀的文件
  2. 由于前端项目中包含了其它后缀文件,当处理不了这些文件时,会查找webpack配置文件,看module.rules数组中,是否配置了对应的loader加载器
  3. webpack把.css文件转交给css-loader
  4. 当css-loader处理完毕后,会把处理结果转交下一个loader(style-loader)
  5. 当style-loader处理完毕后,发现没有下一个loader了,于是就把处理结果转交给webpack
  6. webpack进行后续处理

当链式调用多个 loader 的时候,它们的执行顺序是反方向(从右向左或者从下向上执行)

四、loader的分类

根据Rule.enforce字段的类型值,可以把loader分为:前置(pre)、普通(normal)、行内(inline)、后置(post)四种,其中pre、post可以通过enforce字段进行指。normal类型需要enforce字段无值,最后的类型开启方式则是loader通过 import/require 行内方式加载时,才表示inline类型。

举例
normal类型

1
2
3
4
{
test: /\.vue$/,
loader: 'vue-loader'
},

因为loader中并没有enforce字段,所以为normal
pre类型

1
2
3
4
5
{
test: /\.vue$/,
loader: 'vue-loader',
enforce: 'pre'
},

post类型

1
2
3
4
5
{
test: /\.vue$/,
loader: 'vue-loader',
enforce: 'post'
},

inline类型

1
import test from 'vue-loader!./test.vue';

(一) loader的引入方式

  1. 配置方式(推荐):在 webpack.config.js 文件中指定 loader。
  2. 内联方式:在每个 import 语句中显式指定 loader。
    注意在 webpack v4 版本可以通过 CLI 使用 loader,但是在 webpack v5 中被弃用。

具体loader引入细节:请访问webpack

(二) pre、normal、inline、post 优先级

  1. 在 Pitching 阶段: loader 上的 pitch 方法,按照 后置(post)、行内(inline)、普通(normal)、前置(pre) 的顺序调用。(post>inline>normal>pre)

  2. 在 Normal 阶段: loader 上的 常规方法,按照 前置(pre)、普通(normal)、行内(inline)、后置(post) 的顺序调用。模块源码的转换, 发生在这个阶段。(pre>normal>inline>post)

注: Pitching和Normal后面会有讲解

例子1: Normal阶段下不同类型loader执行顺序

inlineLoader

1
2
3
4
5
6
// inlineLoader.js

module.exports = function (source, map) {
console.log('inlineLoader');
return source;
}

normalLoader

1
2
3
4
5
6
// normalLoader.js

module.exports = function (source, map) {
console.log('normalLoader');
return source;
}

postLoader

1
2
3
4
5
6
// postLoader.js

module.exports = function (source, map) {
console.log('postLoader');
return source;
}

preLoader

1
2
3
4
5
6
// preLoader.js

module.exports = function (source, map) {
console.log('preLoader');
return source;
}

webpack 中相关loader配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
test: /\.txt$/,
exclude: /node_modules/,
loader: require.resolve('./preLoader.js'),
enforce: 'pre'
},
{
test: /\.txt$/,
exclude: /node_modules/,
loader: require.resolve('./postLoader.js'),
enforce: 'post'
},
{
test: /\.txt$/,
exclude: /node_modules/,
loader: require.resolve('./normalLoader.js'),
},

页面引入

1
2
require('./test.txt');
import 'E:\\iyouTest\\kh-new-project\\build\\inlineLoader.js!./test.txt';

注: 行内loader只能在页面中引入

执行结果

normal阶段不同loader执行顺序

注:图中前三个打印(preLoader、normalLoader、postLoader)是require()调用引起的

例子2: pitch阶段下不同类型loader执行顺序

保持其他内容不变的情况下,在各个loader文件中加入pitch函数,例如

1
2
3
4
5
6
7
8
9
module.exports = function (source, map) {
console.log('normalLoader');
return source;
}

// 新增pitch函数
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
console.log("pitch-normalLoader");
}

运行结果为
pitch阶段不同loader执行顺序

可以发现pitch阶段优先级为pre>normal>inline>post。
有人会不解,为什么有的类型loader会出现两次?这是因为inline-loader在处理时,会执行其余三种类型的loader。
如果不想这么做可以在前面加上’!!’;代码如下

1
import '!!E:\\iyouTest\\kh-new-project\\build\\inlineLoader.js!./test.txt';

至于’!!’详细细节查看webpack-概念-loader

五、loader 的Pitch和Normal阶段

webpack执行loader时分为两个阶段,分别是Pitch和Normal阶段。其中Pitch阶段先执行,Normal阶段后执行,如下图所示
Pitch和Normal阶段执行顺序

对于不同阶段,loader执行顺序为

  1. Pitch阶段: 从左到右(或者从上到下)调用 loader 上的 pitch 方法。
  2. Normal阶段: 从右到左被(或者从下到上)调用loader

例子

1
2
3
4
{
test: /\.css$/i,
use: [ 'style-loader', 'css-loader' ]
},

Pitch阶段: style-loader->css-loader
Normal阶段: css-loader->style-loader
loader执行顺序

(一) 如何在loader添加pitch函数?

在之前的已经有演示了,就是在normal loader中添加pitch属性,并且该属性是函数。

1
2
3
4
5
6
7
8
/**
* remainingRequest: 表示剩余需要处理的loader的绝对路径以!分割组成的字符串
* precedingRequest: 表示pitch阶段已经迭代过的loader按照!分割组成的字符串
* data: 在pitch和loader之间交互数据
* /
function (remainingRequest, precedingRequest, data) {
console.log("pitch-normalLoader");
}

(二)、pitch loader返回非undefined值时,loader的处理情况

  1. 在第一个loader的pitch函数中直接返回有值,那么后续的loader不再执行,直接把结果返回给webpack
    直接返回给webpack

  2. 不处于第一种情况时,会跳过剩下的loader和读取文件资源,直接将返回值传入上一个normal loader中执行
    不是在第一个loader pitch函数中有返回值

六、编写loader

(一)、loader编写规范

  • 单一原则: 每个 Loader 只做一件事;
  • 链式调用: Webpack 会按顺序链式调用每个 Loader;
  • 统一原则: 遵循 Webpack 制定的设计规则和结构,输入与输出均为字符串,各个 Loader 完全独立,即插即用;

详细的loader编写规范查看webpack

(二)、同步/异步loader

同步loader: 默认创建的loader是同步loader。

两种同步loader写法

  1. 直接使用return返回内容

    1
    2
    3
    module.exports = function (content, map, meta) {
    return someSyncOperation(content);
    };
  2. 使用this.callback函数完成同步

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /**
    * @param {string|Buffer} content 源文件的内容
    * @param {object} [map] 可以被 https://github.com/mozilla/source-map 使用的 SourceMap 数据
    * @param {any} [meta] meta 数据,可以是任何内容
    */
    module.exports = function (content, map, meta) {
    this.callback(null, someSyncOperation(content), map, meta);
    return; // 当调用 callback() 函数时,总是返回 undefined
    };

    注: this.callback 方法则更灵活,因为它允许传递多个参数,而不仅仅是 content。而且callback函数准寻nodejs的错误优先函数风格

异步loader: 在函数中使用 this.async

1
2
3
4
5
6
7
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);
});
};

何时使用异步loader?

如果计算量很小,使用同步 loader,否则异步loader

(三)、编写一个简单的同步loader

这里以实现’替换txt文本中指定的字符串’功能为例,演示如何编写loader

  1. 创建文件夹并在其中创建将要使用的loader js文件
    创建loader

  2. 在刚才创建的loader文件中写一个简单的替换内容的函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /**
    *
    * @param {string|Buffer} content 源文件的内容
    * @param {object} [map] 可以被 https://github.com/mozilla/source-map 使用的 SourceMap 数据
    * @param {any} [meta] meta 数据,可以是任何内容
    */
    function testLoader(content, map, meta) {
    return `export default function(){
    return ${ JSON.stringify(content.replace('loader','testLoader'))}
    }`
    }
  3. 在webpack配置的module字段中写匹配规则

    1
    2
    3
    4
    5
    {
    test: /\.txt$/,
    exclude: /node_modules/,
    loader: require.resolve('./testLoader.js'),
    },

    引入loader

  4. 创建txt文件和内容并在vue文件中引入(创建的文件名和位置根据自己需求而定)

    txt文件中的内容
    创建txt文件和内容
    vue文件中引入

    1
    2
    3
    import test from './test.txt';

    console.log(test());
  5. 结果
    自定义loader运行结果