你瞭解webpack中配置的loader的執行順序嗎?
為什麼要關注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處理,從後向前執行.
- 即先經過loader3加入了
console.log(loader3)
. - 再loader2處理就加入了
console.log(loader2)
- 最後經過loader1處理加入了
console.log(loader1)
所以執行順序就是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'), } ] },
]
}
``
結果如下:

emm。。。結果和上面的
loader在同一個rule中的執行順序`一致,和我想的一樣。打包過程的終端中輸出的內容是:
執行順序也是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'), } ] },
]
},
```
打包過程中輸出的終端內容為:
果然就是倒著執行嘛,從右向左執行,執行順序是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')
};
輸出瞅一眼:
看起來和文件上寫的一樣,整個過程有點像eventListener的冒泡和捕獲過程。
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階段,如下圖:
現在稍微更改一下我們的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'; }; ```
果然是這樣!那麼此時利用這個pitching的特性,是不是就可以給loader的執行順序中加入邏輯?目前來看,我只知道pitching返回一個值是可以直接跳到上一個loader的normal階段,那麼如果有更復雜的邏輯該怎麼辦呢?
影響loader執行順序因素之二:Rule.enforce的配置
在檢視文件
的時候,還看到一個配置:rule.enforce: pre/post/normal
這個配置也會影響loader的執行順序如下:
我們在我們的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'),
}
]
},
]
影響loader執行順序因素之三:inline-loader
inline-loader是除開pre, normal, post三種loader之外的另外一種loader,這種loader文件中並不建議我們自己手動加入,而是應該由其他的loader自動生成,當inline-loader加入全家桶之後loader的執行順序如下:
它的使用方式是這樣的:
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
-
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')
!!
意思是隻執行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')
-!
意思是隻執行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')
!
意思是隻執行inline-loader和post-loader和pre-loader,那麼我們的順序如下:
- 執行loader1的pitching (webpack沒有感知到又loader4這個inline-loader)pitching loader 1
- 執行loader2中的pitching,此時loader2中返回了inline-loader(感知到了loader4)pitching loader 2
- 因為loader2的pitching返回內容了,回馬槍執行了loader1的normal (第一輪結束)executed loader1
- 因為loader4加入全家桶,
!
只會不執行normal(loader2) - 按照四種loader的順序先執行post-loader的pitching pitching loader1
- 再執行inline-loader的pitching pitching loader4
- 再執行pre-loader的pitching pitching loader3
- 再執行pre-loader的normal executed loader3
- 接著inline-loader的normal executed loader4
- 接著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