【webpack】loader 知識分享
什么是 loader
webpack 只能理解 JavaScript 和 JSON 文件,這是 webpack 開箱可用的自帶能力。loader 讓 webpack 能夠去處理其他類型的文件,并將它們轉(zhuǎn)換為有效 模塊[1],以供應(yīng)用程序使用,以及被添加到依賴圖中。loader 本質(zhì)上是導(dǎo)出為函數(shù)的 JavaScript 模塊。—— webpack 官方中文文檔
如下圖所示,在 webpack 使用過程中,經(jīng)常會出現(xiàn)以下兩種形式,前者更多是我們在 webpack 配置文件中,去根據(jù)文件匹配信息,去配置 loader 相關(guān)信息;后者更多是在 loader / plugin 中去修改/替換/生成的行內(nèi) loader 信息。這就涉及到 loader 的相關(guān)分類。
// webpack.config.js
{
module: {
rules: [
{
test: /.txt$/,
use: [
{
loader: getLoader("a-loader.js"),
},
],
enforce: "pre",
},
{
test: /.txt$/,
use: [
{
loader: getLoader("b-loader.js"),
}
],
enforce: "post",
},
],
},
}
// app.js
import "/Users/jiangyuereee/Desktop/loader/d-loader.js!./txt.txt"
loader 的分類
在 webpack 里,loader 可以被分為四類,分別是:后置post,普通normal,行內(nèi)inline,前置pre。
enforce
對于post,normal,pre,主要取決于在配置里Rule.enforce的取值:pre || post,若無設(shè)置,則為normal。
注意:相對于的是 Rule,并非某個 loader。那么作用于的就是對應(yīng) Rule 的所有 loader。
inline
行內(nèi) loader 比較特殊,是在import / require的時候,將 loader 寫入代碼中。而對于inline而言,有三種前綴語法:
!:忽略normalloader
-!:忽略preloader 和normalloader!!:忽略所有 loader(pre/noraml/post)
行內(nèi) loader 通過!將資源中的 loader 進(jìn)行分割,同時支持在 loader 后面,通過?傳遞參數(shù),參數(shù)信息參考 loader.options 內(nèi)容。
而以上說的三種前綴語法,則是寫在內(nèi)聯(lián) loader 字符串的前綴上,來表示忽略哪些配置 loader
example
以a-loader為pre loader,b-loader為normal loader,c-loader為post loader為例。
補充:本文的 loader 均為
module.exports = function (content) {
console.log("x-loader");
return content;
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
console.log("x-loader-pitch");
};
無前綴信息
import "/Users/jiangyuereee/Desktop/loader/d-loader.js!./txt.txt"
c-loader-pitch
d-loader-pitch
b-loader-pitch
a-loader-pitch
a-loader
b-loader
d-loader
c-loader
!前綴信息
import "!/Users/jiangyuereee/Desktop/loader/d-loader.js!./txt.txt";
c-loader-pitch
d-loader-pitch
a-loader-pitch
a-loader
d-loader
c-loader
-!前綴信息
import "-!/Users/jiangyuereee/Desktop/loader/d-loader.js!./txt.txt";
c-loader-pitch
d-loader-pitch
d-loader
c-loader
!!前綴信息
import "!!/Users/jiangyuereee/Desktop/loader/d-loader.js!./txt.txt";
d-loader-pitch
d-loader
loader 的優(yōu)先級
四種 loader 調(diào)用先后順序為:pre > normal > inline > post 。
在相同種類 loader 的情況下,調(diào)用的優(yōu)先級為,自下而上,自右向左。(pitch 情況下,則反過來)。
例如:
{
module: {
rules: [
{
test: /.txt$/,
use: [
{
loader: getLoader("a-loader.js"),
},
],
enforce: "post",
},
{
test: /.txt$/,
use: [
{
loader: getLoader("b-loader.js"),
},
{
loader: getLoader("c-loader.js"),
},
],
enforce: "post",
},
],
},
}
a-loader-pitch
b-loader-pitch
c-loader-pitch
c-loader
b-loader
a-loader
Loader 調(diào)用鏈
每個 loader 都有自己的 normal 函數(shù)和 pitch 函數(shù),而調(diào)用過程則是先根據(jù)從低到高的優(yōu)先級順序,調(diào)用 loader 各自的 pitch 函數(shù),再由高到低調(diào)用各自的 normal 函數(shù),其過程,更像是一個洋蔥模型。

Loader - pitch
loader 總是 從右到左被調(diào)用。有些情況下,loader 只關(guān)心 request 后面的 元數(shù)據(jù)(metadata),并且忽略前一個 loader 的結(jié)果。在實際(從右到左)執(zhí)行 loader 之前,會先 從左到右 調(diào)用 loader 上的 pitch 方法。—— webpack 官方中文文檔
對于一個 loader,除了通過module.exports導(dǎo)出處理函數(shù)外,還可以通過module.exports.pitch導(dǎo)出pitch方法。正常來說,在loader從右向左調(diào)用之前,會進(jìn)行一次從左到右的pitch方法調(diào)用,而在pitch調(diào)用過程中,如果任何一個有返回值,那么將阻斷后續(xù)的loader調(diào)用鏈,進(jìn)而將自身的返回結(jié)果傳遞給上一個loader作為content。
補充:pitch方法同樣有同步 / 異步之分,同樣可以選擇return或者this.callback去傳遞更多的信息,后續(xù)會講到。
loader 的調(diào)用過程:

而一旦其中的一個pitch返回了結(jié)果,那么將跳過后續(xù)的loader,將返回結(jié)果傳遞給前一個loader。

像常見的 style-loader / vue-loader 等等,就會利用 pitch 階段進(jìn)行攔截處理工作,從而實現(xiàn) loader 特色化工作。
同步 / 異步 loader
如果是單個處理結(jié)果,可以在 同步模式 中直接返回。如果有多個處理結(jié)果,則必須調(diào)用
this.callback()。在 異步模式 中,必須調(diào)用this.async()來告知 loader runner[2] 等待異步結(jié)果,它會返回this.callback()回調(diào)函數(shù)。隨后 loader 必須返回undefined并且調(diào)用該回調(diào)函數(shù)。—— webpack 官方中文文檔
在 webpack 中,loader 可能會由于依賴于讀取外部配置文件、進(jìn)行網(wǎng)絡(luò)請求等等原因,從而比較耗時。而此時如果進(jìn)行同步操作,就會導(dǎo)致 webpack 阻塞住,所以 loader 會有同步 / 異步之分。
在 loader 中,可以通過兩種方式返回數(shù)據(jù):
return:return只能返回content信息;
callback:
this.callback(
err: Error | null, // 錯誤信息
content: string | Buffer, // content信息
sourceMap?: SourceMap, // sourceMap
meta?: any // 會被 webpack 忽略,可以是任何東西(例如:AST、一些元數(shù)據(jù)啥的)。
);
同步 loader
對于同步 loader 而言,使用return或者this.callback均可以達(dá)到想要的效果。只是說,相對于return,this.callback可以返回更多的信息。
module.exports = function(content, map, meta) {
// return handleData(content);
this.callback(null, handleData(content), handleSourceMap(map), meta);
return; // 當(dāng)調(diào)用 callback() 函數(shù)時,總是返回 undefined
};
異步 loader
對于異步 loader 而言,需要通過this.async(),來獲取到callback函數(shù)。
module.exports = function(content, map, meta) {
var callback = this.async();
asycnHandleData(content, function(err, result) {
if (err) return callback(err);
callback(null, result, map, meta);
});
};
loader 的參數(shù)
一般來說,起始 loader 只有一個入?yún)ⅲ嘿Y源文件的內(nèi)容。默認(rèn)情況下,資源文件的內(nèi)容會以 UTF-8 字符串傳遞給 loader。而在有需要的情況下,loader 可以通過設(shè)置module.exports.raw = true;來表示,需要接受一個Buffer。同時,每個 loader 都可以傳遞String || Buffer,complier 會將其在loader之間根據(jù)raw的值來進(jìn)行轉(zhuǎn)換。(例如:圖片文件等)
而根據(jù)callback的參數(shù)也可以知道,除了起始 loader 之外,loader 可以接受三個參數(shù):content、sourceMap、meta。
loader.pitch 的參數(shù)
pitch 一共有三個參數(shù):
remainingRequest:當(dāng)前 loader 右側(cè)的所有 loader 加上資源路徑,根據(jù)!分割,連接而成的內(nèi)聯(lián) loader。
precedingRequest:當(dāng)前 loader 左側(cè)的所有 loader,根據(jù)!分割,連接而成的內(nèi)聯(lián) loader。data:在 pitch 階段和 normal 階段之間共享的 data 對象。即:pitch 階段的參數(shù) data 和 normal 階段通過 this.data 獲取的 data 為同一對象。
補充:左右側(cè),相對于 loader 調(diào)用鏈,normal 階段。
example
例如:有a、b、c三個 loader,( use: [a, b, c] )在處理某個文件的時候,對應(yīng)的 a、b、c 各自的 pitch 參數(shù)分別為:
a: remainingRequest:b-loader.js!c-loader.js!file.txt;precedingRequest: '';
b:
remainingRequest:c-loader.js!file.txt;precedingRequest: a-loader.js;c:
remainingRequest:file.txt;precedingRequest: a-loader.js!b-loader.js;
loader 的輸出
compiler 預(yù)期得到最后一個 loader 產(chǎn)生的處理結(jié)果。這個處理結(jié)果應(yīng)該為
String或者Buffer(能夠被轉(zhuǎn)換為 string)類型,代表了模塊的 JavaScript 源碼。另外,還可以傳遞一個可選的 SourceMap 結(jié)果(格式為 JSON 對象)。—— webpack 官方中文文檔
當(dāng) loader 鏈路到了最后一個loader時,compiler期望得到的處理結(jié)果是可轉(zhuǎn)換為String的Buffer或者String類型,表示當(dāng)前模板處理后的js源碼。同時根據(jù)callback函數(shù)可知,還可以傳遞一個sourceMap。
除去正常 loader 處理返回的形式輸出源碼外,還可以根據(jù)this.emitFile來進(jìn)行額外輸出文件。
emitFile(
name: string,
content: Buffer|string,
sourceMap?: {...}
);
loader 緩存
開發(fā)環(huán)境默認(rèn)情況下,loader 的處理結(jié)果會被標(biāo)記為可緩存。因為很多 loader 轉(zhuǎn)換的過程是非常耗時的,webpack 默認(rèn)會將所有 loader 標(biāo)記為可緩存的,在依賴文件沒有改變的情況下是不會重新進(jìn)行 loader 處理的過程。
可以通過this.cacheable=false來標(biāo)記 loader 為不可緩存,在大部分情況下,還是不推薦將 loader 標(biāo)記為不可緩存,可以使用this.addDependency添加文件依賴。
example
{
entry: "./app.js",
mode: "development",
module: {
rules: [
{
test: /.txt$/,
use: [
{
loader: getLoader("a-loader.js"),
},
],
},
]
}
}
在.txt 文件沒有發(fā)生變化的時候,watch 模式下,重新編譯是不會 重新調(diào)用 a-loader 去重新處理.txt 文件。如果期望在某個.js 文件發(fā)生變化的時候,重新調(diào)用 a-loader 進(jìn)行處理,就可以使用 this.addDependency 添加文件依賴。
module.exports = function (content) {
console.log("a-loader");
this.addDependency("/Users/jiangyuereee/Desktop/loader/c.js");
return content;
};
注意:整個 loader 調(diào)用鏈都會被重新激活。
loaderAPI
除了以上提到的一些 API 之外,在 loader 內(nèi)還可以使用一些其他 API:The Loader Context[3]
loader 開發(fā)工具包
Loader-runner[4]
可以通過該包進(jìn)行 loader 的開發(fā)和調(diào)試。
Loader-utils[5]
集成了 loader 開發(fā)中,常用的一些方法,方便開發(fā)。
Schema-utils[6]
便于驗證 loader options 的合法性(包括但不限于 loader 使用)。
參考資料
模塊: https://webpack.docschina.org/concepts/modules
[2]loader runner: https://github.com/webpack/loader-runner
[3]The Loader Context: https://webpack.docschina.org/api/loaders/#the-loader-context
[4]Loader-runner: https://github.com/webpack/loader-runner
[5]Loader-utils: https://github.com/webpack/loader-utils#readme
[6]Schema-utils: https://github.com/webpack/schema-utils
[7]webpack 官方中文文檔: https://webpack.docschina.org/concepts/loaders/
[8]Loader 機制: https://tsejx.github.io/webpack-guidebook/principle-analysis/implementation-principle/loader
