一、Loader
1.1 loader 干啥的?
webpack 只能理解 JavaScript 和 JSON 文件,這是 webpack 開箱可用的自帶能力。**loader **讓 webpack 能夠去處理其他類型的文件,并將它們轉換為有效模塊,以供應用程序使用,以及被添加到依賴圖中。
也就是說,webpack 把任何文件都看做模塊,loader 能 import 任何類型的模塊,但是 webpack 原生不支持譬如 css 文件等的解析,這時候就需要用到我們的 loader 機制了。 我們的 loader 主要通過兩個屬性來讓我們的 webpack 進行聯動識別:
- test 屬性,識別出哪些文件會被轉換。
- use 屬性,定義出在進行轉換時,應該使用哪個 loader。
那么問題來了,大家一定想知道自己要定制一個 loader 的話需要怎么做呢?
1.2 開發準則
俗話說的好,沒有規矩不成方圓,編寫我們的 loader 時,官方也給了我們一套用法準則(Guidelines),在編寫的時候應該按照這套準則來使我們的 loader 標準化:
- 簡單易用。
- 使用鏈式傳遞。(由于 loader 是可以被鏈式調用的,所以請保證每一個 loader 的單一職責)
- 模塊化的輸出。
- 確保無狀態。(不要讓 loader 的轉化中保留之前的狀態,每次運行都應該獨立于其他編譯模塊以及相同模塊之前的編譯結果)
- 充分使用官方提供的 loader utilities。
- 記錄 loader 的依賴。
- 解析模塊依賴關系。
根據模塊類型,可能會有不同的模式指定依賴關系。例如在 CSS 中,使用@import 和 url(...)語句來聲明依賴。這些依賴關系應該由模塊系統解析。 可以通過以下兩種方式中的一種來實現:
- 通過把它們轉化成 require 語句。
- 使用 this.resolve 函數解析路徑。
- 提取通用代碼。
- 避免絕對路徑。
- 使用 peer dependencies。如果你的 loader 簡單包裹另外一個包,你應該把這個包作為一個 peerDependency 引入。
1.3 上手
一個 loader 就是一個 nodejs 模塊,他導出的是一個函數,這個函數只有一個入參,這個參數就是一個包含資源文件內容的字符串,而函數的返回值就是處理后的內容。也就是說,一個最簡單的 loader 長這樣:
- module.exports = function (content) {
- // content 就是傳入的源內容字符串
- return content
- }
當一個 loader 被使用的時候,他只可以接收一個入參,這個參數是一個包含包含資源文件內容的字符串。 是的,到這里為止,一個最簡單 loader 就已經完成了!接下來我們來看看怎么給他加上豐富的功能。
1.4 四種 loader
我們基本可以把常見的 loader 分為四種:
- 同步 loader
- 異步 loader
- "Raw" Loader
- Pitching loader
① 同步 loader 與 異步 loader
一般的 loader 轉換都是同步的,我們可以采用上面說的直接 return 結果的方式,返回我們的處理結果:
- module.exports = function (content) {
- // 對 content 進行一些處理
- const res = dosth(content)
- return res
- }
也可以直接使用 this.callback() 這個 api,然后在最后直接 **return undefined **的方式告訴 webpack 去 this.callback() 尋找他要的結果,這個 api 接受這些參數:
- this.callback(
- err: Error | null, // 一個無法正常編譯時的 Error 或者 直接給個 null
- content: string | Buffer,// 我們處理后返回的內容 可以是 string 或者 Buffer()
- sourceMap?: SourceMap, // 可選 可以是一個被正常解析的 source map
- meta?: any // 可選 可以是任何東西,比如一個公用的 AST 語法樹
- );
接下來舉個例子:
這里注意[this.getOptions()](https://webpack.docschina.org/api/loaders/#thisgetoptionsschema) 可以用來獲取配置的參數
從 webpack 5 開始,this.getOptions 可以獲取到 loader 上下文對象。它用來替代來自loader-utils中的 getOptions 方法。
- module.exports = function (content) {
- // 獲取到用戶傳給當前 loader 的參數
- const options = this.getOptions()
- const res = someSyncOperation(content, options)
- this.callback(null, res, sourceMaps);
- // 注意這里由于使用了 this.callback 直接 return 就行
- return
- }
這樣一個同步的 loader 就完成了!
再來說說異步: 同步與異步的區別很好理解,一般我們的轉換流程都是同步的,但是當我們遇到譬如需要網絡請求等場景,那么為了避免阻塞構建步驟,我們會采取異步構建的方式,對于異步 loader 我們主要需要使用 this.async() 來告知 webpack 這次構建操作是異步的,不多廢話,看代碼就懂了:
- module.exports = function (content) {
- var callback = this.async()
- someAsyncOperation(content, function (err, result) {
- if (err) return callback(err)
- callback(null, result, sourceMaps, meta)
- })
- }
② "Raw" loader
默認情況下,資源文件會被轉化為 UTF-8 字符串,然后傳給 loader。通過設置 raw 為 true,loader 可以接收原始的 Buffer。每一個 loader 都可以用 String 或者 Buffer 的形式傳遞它的處理結果。complier 將會把它們在 loader 之間相互轉換。大家熟悉的 file-loader 就是用了這個。簡而言之:你加上 module.exports.raw = true; 傳給你的就是 Buffer 了,處理返回的類型也并非一定要是 Buffer,webpack 并沒有限制。
- module.exports = function (content) {
- console.log(content instanceof Buffer); // true
- return doSomeOperation(content)
- }
- // 劃重點↓
- module.exports.raw = true;
③ Pitching loader
我們每一個 loader 都可以有一個 pitch 方法,大家都知道,loader 是按照從右往左的順序被調用的,但是實際上,在此之前會有一個按照從左往右執行每一個 loader 的 pitch 方法的過程。pitch 方法共有三個參數:
- remainingRequest:loader 鏈中排在自己后面的 loader 以及資源文件的絕對路徑以!作為連接符組成的字符串。
- precedingRequest:loader 鏈中排在自己前面的 loader 的絕對路徑以!作為連接符組成的字符串。
- data:每個 loader 中存放在上下文中的固定字段,可用于 pitch 給 loader 傳遞數據。
在 pitch 中傳給 data 的數據,在后續的調用執行階段,是可以在 this.data 中獲取到的:
- module.exports = function (content) {
- return someSyncOperation(content, this.data.value);// 這里的 this.data.value === 42
- };
- module.exports.pitch = function (remainingRequest, precedingRequest, data) {
- data.value = 42;
- };
注意! 如果某一個 loader 的 pitch 方法中返回了值,那么他會直接“往回走”,跳過后續的步驟,來舉個例子:
假設我們現在是這樣:use: ['a-loader', 'b-loader', 'c-loader'],那么正常的調用順序是這樣:
現在 b-loader 的 pitch 改為了有返回值:
- // b-loader.js
- module.exports = function (content) {
- return someSyncOperation(content);
- };
- module.exports.pitch = function (remainingRequest, precedingRequest, data) {
- return "誒,我直接返回,就是玩兒~"
- };
那么現在的調用就會變成這樣,直接“回頭”,跳過了原來的其他三個步驟:
1.5 其他 API
- this.addDependency:加入一個文件進行監聽,一旦文件產生變化就會重新調用這個 loader 進行處理
- this.cacheable:默認情況下 loader 的處理結果會有緩存效果,給這個方法傳入 false 可以關閉這個效果
- this.clearDependencies:清除 loader 的所有依賴
- this.context:文件所在的目錄(不包含文件名)
- this.data:pitch 階段和正常調用階段共享的對象
- this.getOptions(schema):用來獲取配置的 loader 參數選項
- this.resolve:像 require 表達式一樣解析一個 request。resolve(context: string, request: string, callback: function(err, result: string))
- this.loaders:所有 loader 組成的數組。它在 pitch 階段的時候是可以寫入的。
- this.resource:獲取當前請求路徑,包含參數:'/abc/resource.js?rrr'
- this.resourcePath:不包含參數的路徑:'/abc/resource.js'
- this.sourceMap:bool 類型,是否應該生成一個 sourceMap
官方還提供了很多實用 Api ,這邊只列舉一些可能常用的,更多可以戳鏈接