你瞭解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