你了解webpack中配置的loader的执行顺序吗?

7,108 阅读3分钟

为什么要关注loader的执行顺序?

最近在工作中需要写几个webpack的loader,但是发现好像自己并不清楚每次在webpack中配置的loader执行的顺序是如何的,可能只有我不太清楚吧。。😓 所以想写一个小demo把玩把玩~

 {
        test: /\.scss$/,
        use: [
            'style-loader',

            // MiniCssExtractPlugin.loader,
            'css-loader',
            {
                loader: 'postcss-loader',
                options: {
                    sourceMap: true
                }
            },
            'sass-loader'
        ]
    }

如果你已经知道上面这几个loader都是做什么的话,那你应该已经“大概”知道loader的执行顺序了,如果不知道的话,还请客官继续往下看看~

loader在同一个rule中的执行顺序

这里因为我想要知道在webpack中配置loader的执行顺序,所以我写了一个简单的demo用webpack进行打包,加入了几个简单的js loader:

  • 我们把重点放到webpack配置中module的rule中:
     module: {
       rules: [
            {
                test: /\.js$/,
                use: [
                    {
                        loader: require.resolve('./src/loaders/loader1.js'),
                    },
                    {
                        loader: require.resolve('./src/loaders/loader2.js'),
                    },
                    {
                        loader: require.resolve('./src/loaders/loader3.js'),
                    }
                ]
            },
        ]
    }
  • demo中的入口文件只是简单定义了几个变量,并且log出index的文件名
// webpack打包的入口entry文件: index.js
const index = 'index.js';
const loader1 = 'loader1.js';
const loader2 = 'loader2.js';
const loader3 = 'loader3.js';

console.log(index)
  • demo中再加入几个简单的loader,loader1,loader2,loader3都是一个简单loader,他们三个的内容非常简单,只是简单的加入console.log('文件名字对应的变量名'),这样方便测试最终打出来的bundle.js中所包含的内容
// loader1.js中输出的是loader1, loader2.js中输出的是loader2,loader3.js输出loader3
module.exports = function(source, options) {
    const str = '\n  console.log(loader1);'
    console.log('executed in loader1')
    return source + str
};
  • 那么最终打包出来的结果是什么样子的呢? 从下图能够看出来,入口文件index.js分别经过了三个loader处理,从后向前执行.
  1. 即先经过loader3加入了console.log(loader3).
  2. 再loader2处理就加入了console.log(loader2)
  3. 最后经过loader1处理加入了console.log(loader1) image.png 所以执行顺序就是loader3 -> loader2 -> loader1

loader在多个rule中的执行顺序

多个rule中每个loader都不同

接下来再做个实验,如果我配置三个相同的rule,里面的loader的执行顺序又是啥样的呢?

    module: {
        rules: [
            {
                test: /\.js$/,
                use: [
                    {
                        loader: require.resolve('./src/loaders/loader1.js'),
                    }
                ]
            },
            {
                test: /\.js$/,
                use: [
                    {
                        loader: require.resolve('./src/loaders/loader2.js'),
                    }
                ]
            },
            {
                test: /\.js$/,
                use: [
                    {
                        loader: require.resolve('./src/loaders/loader3.js'),
                    }
                ]
            },
          
        ]
    }

结果如下: image.png emm。。。结果和上面的loader在同一个rule中的执行顺序一致,和我想的一样。打包过程的终端中输出的内容是:

image.png

执行顺序也是loader3 -> loader2 -> loader1

多个rules中有相同的loader

      module: {
        rules: [
            {
                test: /\.js$/,
                use: [
                    {
                        loader: require.resolve('./src/loaders/loader1.js'),
                    },
                    {
                        loader: require.resolve('./src/loaders/loader2.js'),
                    }
                ]
            },
            {
                test: /\.js$/,
                use: [
                    {
                        loader: require.resolve('./src/loaders/loader1.js'),
                    },
                    {
                        loader: require.resolve('./src/loaders/loader2.js'),
                    },
                    {
                        loader: require.resolve('./src/loaders/loader3.js'),
                    }
                ]
            },
          
        ]
    },

image.png 打包过程中输出的终端内容为:

image.png

果然就是倒着执行嘛,从右向左执行,执行顺序是3 -> 2 -> 1 -> 2 -> 1,好像loaders中连去重都不会,也就是说你的loader配置了几次就会被执行几次,此时我的问题就来了,那么有没有可能我在执行3 -> 2 -> 1的时候在某种情况下并不想继续执行了,也就是说给loader的执行顺序中加入逻辑?带着疑问我点开了 node_modules/webpack/lib/NormalModule.js中找到了node_modules/loader-runner.js文件,里面有一个runLoaders的方法。。。至此开启了一趟奇妙的旅程。。

谢特! BRO

  • 原来以为loader的执行顺序无非就是一个数组的pop,push之类的,但当我看到了这里的代码的时候发现远比我想象中的复杂。从下面的代码片段中,前面的过程看似还比较容易理解,都是向locaderContext上面注入一些变量,比如remainingRequest: 剩余的loaders。previousRequest: 之前执行过的loaders等等,那么后面的这个iteratePitchingLoaders是什么鬼?pitching又是什么?
exports.runLoaders = function runLoaders(options, callback) {
        ...
        var loaders = options.loaders || [];
	var loaderContext = options.context || {};
        Object.defineProperty(loaderContext, "resource", {
           ...
	});
        Object.defineProperty(loaderContext, "request", {
           ...
	});
	Object.defineProperty(loaderContext, "remainingRequest", {
           ...
	});
	Object.defineProperty(loaderContext, "currentRequest", {
	   ...
	});
	Object.defineProperty(loaderContext, "previousRequest", {
	   ...
	});
	Object.defineProperty(loaderContext, "query", {
           ...
	});
	Object.defineProperty(loaderContext, "data", {
          ...
	});

	iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {
		...
	});
};

此处查询了一下webpack文档的内容,原来每个loader的执行顺序其实由两部分组成:

  1. Pitching 过程,loaders的pitching过程从前到后(loader1 -> 2 -> 3)
  2. Normal 过程, loaders的normal过程从后到前(loader3 -> 2 -> 1)

此时我们稍微修改一下loader中的内容:loader中再加入pitch方法:

// loader1.js中输出的是loader1, loader2.js中输出的是loader2,loader3.js输出loader3
module.exports = function(source, options) {
    const str = '\n  console.log(loader1);'
    console.log('executed in loader1')
    return source + str
};
// 下面的内容是向loader中需要添加的
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
    console.log('pitch in loader1')
   };

输出瞅一眼:

image.png

看起来和文档上写的一样,整个过程有点像eventListener的冒泡和捕获过程。

image.png

iteratePitchingLoaders

这里我们再看一下iteratePitchingLoaders的内容是什么(已经简化)

function iteratePitchingLoaders(options, loaderContext, callback) {
	if(loaderContext.loaderIndex >= loaderContext.loaders.length) { 
           //递归loaders,当目前的index大于loaders的数量的时候,即所有loader的pitching都执行完毕
            processResource() //执行loader的normal阶段
        }
	if(currentLoaderObject.pitchExecuted) {
        // 如果当前loader的pitching执行过了
            loaderContext.loaderIndex++; // index加一
            return iteratePitchingLoaders(options, loaderContext, callback); // 递归调用下一个loader的pitching函数
	}

	loadLoader(currentLoaderObject, function(err) {
		var fn = currentLoaderObject.pitch; // 拿到当前loader的pitching函数
		currentLoaderObject.pitchExecuted = true; //pitched标志位置true,用作下次递归
		if(!fn) return iteratePitchingLoaders(options, loaderContext, callback); // 如果没有pitching函数,就递归下一个loader的pitching
                runSyncOrAsync(fn, function(err) { // 运行pitching(即fn)方法 将结果传入callback
                    if(err) return callback(err);
                    var args = Array.prototype.slice.call(arguments, 1);
                    // 这里的args是pitching执行之后返回的内容
                    if(args.length > 0) {
                            //如果当前loader的pitching方法有返回内容,则执行前一个函数的normal阶段
                            loaderContext.loaderIndex--;
                            iterateNormalLoaders(options, loaderContext, args, callback) ;
                    } else {
                       // 如果当前的pitching函数没有返回值,递归下一个laoder的pitching
                            iteratePitchingLoaders(options, loaderContext, callback);
                    }
                }
            );
    });
}

就是先顺序执行所有loaders的pitching,再倒序执行normal!

影响loader执行顺序因素之一:pitching方法的返回内容

当看到runSyncOrAsync中的内容时我们发现,当一个loader的pitching函数有返回值的时候,就会跳过之后的步骤,直接来到前一个loader的normal阶段,如下图:

image.png 现在稍微更改一下我们的loader2,在它的pitching中加入返回值:

module.exports = function(source, options) {
    const str = ' \n  console.log(loader2);'
    console.log('executed in loader2')

    return source + str
};

module.exports.pitch = function (remainingRequest, precedingRequest, data) {
    console.log('pitch in loader2')
    return '123';
   };

image.png

果然是这样!那么此时利用这个pitching的特性,是不是就可以给loader的执行顺序中加入逻辑?目前来看,我只知道pitching返回一个值是可以直接跳到上一个loader的normal阶段,那么如果有更复杂的逻辑该怎么办呢?

影响loader执行顺序因素之二:Rule.enforce的配置

在查看文档的时候,还看到一个配置:rule.enforce: pre/post/normal这个配置也会影响loader的执行顺序如下:

image.png

我们在我们的demo中实验一下:(在上一部分我们在loader2的pitching中加入了返回值,现在要去掉,以免影响我们测试enforce属性对顺序的影响)

   rules: [
            {
                test: /\.js$/,
                enforce: 'pre',
                use: [
                    {
                        loader: require.resolve('./src/loaders/loader1.js'),
                    },
                ]
            },
            {
                test: /\.js$/,
                use: [
                    {
                        loader: require.resolve('./src/loaders/loader2.js'),
                    }
                ]
            },
            {
                test: /\.js$/,
                enforce: 'post',
                use: [
                    {
                        loader: require.resolve('./src/loaders/loader3.js'),
                    }
                ]
            },
        ]

image.png

影响loader执行顺序因素之三:inline-loader

inline-loader是除开pre, normal, post三种loader之外的另外一种loader,这种loader文档中并不建议我们自己手动加入,而是应该由其他的loader自动生成,当inline-loader加入全家桶之后loader的执行顺序如下:

image.png 它的使用方式是这样的:

requre("!!path-to-loader1!path-to-loader2!path-to-loader3!./sourceFile.js")

抛开'!!, !, -!'等标识来看,从右向左来看就是让sourceFile.js分别通过loader3,loader2,loader1三个loader来进行处理。

  • !表示所有的normal loader全部不执行(执行pre,post和inline loader)
  • -!表示所有的normal loader和pre loader都不执行(执行post和inline loader)
  • !! 表示所有的normal pre 和 post loader全部不执行(只执行inline loader)

不太懂?没关系,我们在demo中验证一下:

  1. 首先加入loader4,其内容和loader3一致
module.exports = function(source, options) {
    const str = ' \n  console.log("I am in the inline loader");'
    console.log('executed in loader4')
    return source + str

};

module.exports.pitch = function (remainingRequest, precedingRequest, data) {
    console.log('pitch in loader4')
   };
  1. webpack配置的rule中的配置修改如下:
 rules: [
            {
                enforce: 'post', //让loader1的类型变为post loader
                test: /\.js$/,
                use: [
                    {
                        loader: require.resolve('./src/loaders/loader1.js'),
                    },
                ]
            },
            {
                test: /\.js$/,
                use: [
                    {
                        loader: require.resolve('./src/loaders/loader2.js'),
                    }
                ]
            },
            {
                enforce: 'pre', // loader3的类型变为pre loader
                test: /\.js$/,
                use: [
                    {
                        loader: require.resolve('./src/loaders/loader3.js'),
                    }
                ]
            },
        ]

在之前demo的基础上我们将loader1变为了post-loader,loader3变为了pre-loader,目前执行的顺序此时还是loader3 -> loader2 -> loader1

  1. loader2中的pitching中也要做出修改:
module.exports = function(source, options) {
    const str = ' \n  console.log(loader2);'
    console.log('executed in loader2')

    return source + str
};

module.exports.pitch = function (remainingRequest, precedingRequest, data) {
    console.log('pitch in loader2')
    return `require('!!./src/loaders/loader4.js!./index.js')` // return一个inline-loader的调用
   };

此时我们的demo中有了post-loader(loader1),pre loader(loader3),normal loader(loader2)和inline loader(loader2中return的loader4)。只有loader2的pitching有返回值,开始编译!结果如下:

loader2中返回require('!!./src/loaders/loader4.js!./index.js')

image.png

!!意思是只执行inline-loader,那么我们的顺序如下:

  1. 执行loader1的pitching (webpack没有感知到又loader4这个inline-loader)pitching loader 1
  2. 执行loader2中的pitching,此时loader2中返回了inline-loader(感知到了loader4)pitching loader 2
  3. 因为loader2的pitching返回内容了,回马枪执行了loader1的normal(第一轮结束) executed loader1
  4. 因为loader4加入全家桶,!!最后只执行inline-loader类型的loader4的pitching和normal
  5. pitching loader4
  6. executed loader4

loader2中返回require('-!./src/loaders/loader4.js!./index.js')

image.png

-!意思是只执行inline-loader和post-loader,那么我们的顺序如下:

  1. 执行loader1的pitching (webpack还没有感知到有loader4这个inline-loader)pitching loader 1
  2. 执行loader2中的pitching,此时loader2中返回了inline-loader(感知到了loader4)pitching loader 2
  3. 因为loader2的pitching返回内容了,回马枪执行了loader1的normal (第一轮结束)executed loader1
  4. 因为loader4加入全家桶,-!只会执行post(loader1)和inline(loader4),
  5. 按照四种loader的顺序先执行post-loader的pitching pitching loader1
  6. 再执行inline-loader的pitching pitching loader4
  7. 接着inline-loader的normal executed loader4
  8. 接着post-loader的normal (第二轮结束)executed loader1

loader3中返回require('!./src/loaders/loader4.js!./index.js')

image.png

!意思是只执行inline-loader和post-loader和pre-loader,那么我们的顺序如下:

  1. 执行loader1的pitching (webpack没有感知到又loader4这个inline-loader)pitching loader 1
  2. 执行loader2中的pitching,此时loader2中返回了inline-loader(感知到了loader4)pitching loader 2
  3. 因为loader2的pitching返回内容了,回马枪执行了loader1的normal (第一轮结束)executed loader1
  4. 因为loader4加入全家桶,!只会不执行normal(loader2)
  5. 按照四种loader的顺序先执行post-loader的pitching pitching loader1
  6. 再执行inline-loader的pitching pitching loader4
  7. 再执行pre-loader的pitching pitching loader3
  8. 再执行pre-loader的normal executed loader3
  9. 接着inline-loader的normal executed loader4
  10. 接着post-loader的normal (第二轮结束)executed loader1

style-loader,css-loader,sass-loader的真实执行顺序:

如果你打开style-loader的文件,你会看到大概下面的内容:

module.exports = function () {};

module.exports.pitch = function (request) { // request是remianing的loader
    return [
    `var content = require(" + ${loaderUtils.stringifyRequest(this, "!!" + request) + ");}`,
    `var update = require(" + ${loaderUtils.stringifyRequest(this, "!" + path.join(__dirname, "lib", "addStyles.js")) + ")(content, options);}`,
    "",
    "module.exports = content.locals;",
    ],
}

style-loader中根本没有normal过程,而是pitching过程,并且pitching返回了inline-loader!!

style-loader -> css-loader -> sass-loader真实的执行顺序其实是:

  1. 先经过style-loader的pitching,此时pitching返回值有内容,简化为require('!!css-loader/index.js!sass-loader/dist/cjs.js!./index.sass'),第一轮直接结束。
  2. 因为style-loader的pitching返回了内容,所以剩下的loader阶段都不执行,转而执行inline-loader的内容(inline-loader中又包含了两个loader,是从后向前执行的,即现sass-loader再css-loader)
  3. 在inline-loader中,sass-loader对index.sass处理,将sass内容处理成css。
  4. css-loader对 “3” 中执行之后内容进行处理,css-loader将css转换成js字符串。
  5. 此时回到style-loader中的pitching,“4”之后的结果将被style-loader剩下的逻辑处理'addStyles',即加到style标签上再append到dom上。

总结:

loader的真实执行顺序和他们在rule中配置的顺序、类型(pre,normal,post,inline)、以及loader中在pitching中返回的内容都有关!

reference: zhuanlan.zhihu.com/p/360421184