在這一小節我們會來介紹如何創建一個 webpack 可用的 loader。
## loader 是一個函數
先來看一個簡單的例子:
```
"use strict";
const marked = require("marked");
const loaderUtils = require("loader-utils");
module.exports = function (markdown) {
// 使用 loaderUtils 來獲取 loader 的配置項
// this 是構建運行時的一些上下文信息
const options = loaderUtils.getOptions(this);
this.cacheable();
// 把配置項直接傳遞給 marked
marked.setOptions(options);
// 使用 marked 處理 markdown 字符串,然后返回
return marked(markdown);
};
```
這是 [markdown-loader](https://github.com/peerigon/markdown-loader) 的實現代碼,筆者添加了一些代碼說明,看上去很簡單。
markdown-loader 本身僅僅只是一個函數,接收模塊代碼的內容,然后返回代碼內容轉化后的結果。webpack loader 的本質就是這樣的一個函數。
上述代碼中用到的 [loader-utils](https://github.com/webpack/loader-utils) 是 webpack 官方提供的一個工具庫,提供 loader 處理時需要用到的一些工具方法,例如用來解析上下文 loader 配置項的 `getOptions`。關于這個工具庫的內容和功能不是特別復雜,就不展開了,直接參考這個庫的官方文檔即可。
代碼中還用到了 [marked](https://github.com/markedjs/marked),marked 是一個用于解析 Markdown 的類庫,可以把 Markdown 轉為 HTML,markdown-loader 的核心功能就是用它來實現的。基本上,webpack loader 都是基于一個實現核心功能的類庫來開發的,例如 [sass-loader](https://github.com/webpack-contrib/sass-loader) 是基于 [node-sass](https://github.com/sass/node-sass) 實現的,等等。
## 開始一個 loader 的開發
我們可以在 webpack 配置中直接使用路徑來指定使用本地的 loader,或者在 loader 路徑解析中加入本地開發 loader 的目錄。看看配置例子:
```
// ...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: path.resolve('./loader/index.js'), // 使用本地的 ./loader/index.js 作為 loader
},
],
},
// 在 resolveLoader 中添加本地開發的 loaders 存放路徑
// 如果你同時需要開發多個 loader,那么這個方式會更加適合你
resolveLoader: {
modules: [
'node_modules',
path.resolver(__dirname, 'loaders')
],
},
```
如果你熟悉 Node 的話,也可以使用 `npm link` 的方式來開發和調試,關于這個方式,可以參考 npm 的官方文檔 [npm-link](https://docs.npmjs.com/cli/link)。
## 復雜一點的情況
當我們選擇上述任意一種方法,并且做好相應的準備后,我們就可以開始寫 loader 的代碼了,然后通過執行 webpack 構建來查看 loader 是否正常工作。
上面已經提到,loader 是一個函數,接收代碼內容,然后返回處理結果,有一些 loader 的實現基本上就是這么簡單,但是有時候會遇見相對復雜一點的情況。
首先 loader 函數接受的參數是有三個的:`content, map, meta`。`content` 是模塊內容,但不僅限于字符串,也可以是 buffer,例如一些圖片或者字體等文件。`map` 則是 sourcemap 對象,`meta` 是其他的一些元數據。loader 函數單純返回一個值,這個值是當成 content 去處理,但如果你需要返回 sourcemap 對象或者 meta 數據,甚至是拋出一個 loader 異常給 webpack 時,你需要使用 `this.callback(err, content, map, meta)` 來傳遞這些數據。
我們日常使用 webpack,有時候會把多個 loader 串起來一起使用,最常見的莫過于 css-loader 和 style-loader 了。當我們配置 `use: ['bar-loader', 'foo-loader']` 時,loader 是以相反的順序執行的,即先跑 foo-loader,再跑 bar-loader。這一部分內容在配置 loader 的小節中有提及,這里再以開發 loader 的角度稍稍強調下,搬運官網的一段說明:
* 最后的 loader 最早調用,傳入原始的資源內容(可能是代碼,也可能是二進制文件,用 buffer 處理)
* 第一個 loader 最后調用,期望返回是 JS 代碼和 sourcemap 對象(可選)
* 中間的 loader 執行時,傳入的是上一個 loader 執行的結果
雖然有多個 loader 時遵循這樣的執行順序,但對于大多數單個 loader 來說無須感知這一點,只負責好處理接受的內容就好。
還有一個場景是 loader 中的異步處理。有一些 loader 在執行過程中可能依賴于外部 I/O 的結果,導致它必須使用異步的方式來處理,這個使用需要在 loader 執行時使用 `this.async()` 來標識該 loader 是異步處理的,然后使用 `this.callback` 來返回 loader 處理結果。例子可以參考官方文檔:[異步 loader](https://doc.webpack-china.org/api/loaders/#%E5%BC%82%E6%AD%A5-loader)。
## Pitching loader
我們可以使用 `pitch` 來跳過 loader 的處理,`pitch` 方法是 loader 額外實現的一個函數,看下官方文檔中的一個例子:
```
module.exports = function(content) {
return someSyncOperation(content, this.data.value); // pitch 的緣故,這里的 data.value 為 42
}
// 掛在 loader 函數上的 pitch 函數
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
data.value = 42;
}
```
我們可以簡單把 `pitch` 理解為 loader 的前置鉤子,它可以使用 `this.data` 來傳遞數據,然后具備跳過剩余 loader 的能力。
在一個 `use` 配置中所有 loader 執行前會先執行它們對應的 `pitch`,并且與 loader 執行順序是相反的,如:
```
use: [
'bar-loader',
'foo-loader',
],
// 執行 bar-loader 的 pitch
// 執行 foo-loader 的 pitch
// bar-loader
// foo-loader
```
其中,當 pitch 中返回了結果,那么執行順序會回過頭來,跳掉剩余的 loader,如 `bar-loader` 的 pitch 返回結果了,那么執行只剩下
```
// 執行 bar-loader 的 pitch
```
可能只有比較少的 loader 會用到 pitch 這個功能,但有的時候考慮實現 loader 功能需求時把 pitch 納入范圍會有不一樣的靈感,它可以讓你更加靈活地去定義 loader 的執行。
這里的簡單介紹僅做拋磚引玉之用,詳細的學習和了解可以參考官方文檔 [Pitching loader](https://doc.webpack-china.org/api/loaders/#%E8%B6%8A%E8%BF%87-loader-pitching-loader-) 或者 bundler-loader 源碼 [bundler-loader](https://github.com/webpack-contrib/bundle-loader/blob/master/index.js)。
## loader 上下文
上述提及的一些代碼會使用到 `this`,即 loader 函數的上下文,包括 `this.callback` 和 `this.data` 等,可以這樣簡單地理解: `this` 是作為 loader 運行時數據和調用方法的補充載體。
loader 上下文有很多運行時的信息,如 `this.context` 和 `this.request` 等等,而最重要的方法莫過于 `this.callback` 和 `this.async`,關于上下文這里不做展開,官方文檔有比較詳細的說明:[loader API](https://doc.webpack-china.org/api/loaders/#this-version)。當你在開發 loader 過程中發現需要某些運行時數據時,就可以查閱 loader API,基本上該有的數據都有了。
## 一個好 loader 是怎么樣的
loader 作為 webpack 解析資源的一種擴展方式,最重要的是足夠簡單易用,專注于處理自己那一塊的內容,便于維護,可以和其他多個 loader 協同來處理更加復雜的情況。
官方對于 loader 的使用和開發有一些準則,一個好的 loader 應該符合官方的這些定義:[Loader 準則](https://doc.webpack-china.org/contribute/writing-a-loader/#%E7%94%A8%E6%B3%95%E5%87%86%E5%88%99-guidelines-)。
社區中有相當多的優秀 loader 可以作為參考,例如剛開始提及的 markdown-loader,相當地簡單易用。由于 loader 的這種準則和特性,大部分的 loader 源碼都相對容易解讀,便于我們學習參考。
作為一個 loader 開發者,你應該盡可能遵循這些準則(有些特殊情況需要特殊處理),這樣會讓你開發出質量更高、更易維護和使用的 webpack loader。
## 小結
本小節我們從下面幾個方面介紹了如何開發一個 webpack loader:
* loader 本質上的實現是一個函數
* 如何開始著手開發一個 loader
* loader 的輸入和輸出
* pitch 函數的作用
* loader 函數的上下文
* 一個好的 loader 是怎么樣的
loader 的實現相對簡單,webpack 社區現成可用的 loader 很多,當你在開發 loader 時遇見了問題,不妨去查閱一下現有 loader 的源碼,或許會有不一樣的靈感。
## 例子
本小節提及的一些簡單的 Demo 可以在 [webpack-examples](https://github.com/teabyii/webpack-examples) 找到。