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

语言: CN / TW / HK

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

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

```js { 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中: js 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的文件名 ```js // 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中所包含的内容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的执行顺序又是啥样的呢? ```js 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](http://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/76ff9415b2394d2dabd80496b2219b79~tplv-k3u1fbpfcp-watermark.image?) emm。。。结果和上面的loader在同一个rule中的执行顺序`一致,和我想的一样。打包过程的终端中输出的内容是:

image.png

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

多个rules中有相同的loader

```js 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又是什么? ```js 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方法: 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 }; // 下面的内容是向loader中需要添加的 module.exports.pitch = function (remainingRequest, precedingRequest, data) { console.log('pitch in loader1') }; 输出瞅一眼:

image.png

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

image.png

iteratePitchingLoaders

这里我们再看一下iteratePitchingLoaders的内容是什么(已经简化) ```js 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中加入返回值: ```js 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属性对顺序的影响) js 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一致 ```js 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') }; 2. webpack配置的rule中的配置修改如下:js 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中也要做出修改: ```js 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的文件,你会看到大概下面的内容: ```js 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: http://zhuanlan.zhihu.com/p/360421184