參考:css
- Webpack Book --- Extending with Loaders。
- Webpack Doc --- Loader Interface
Loader 是 Webpack 幾大重要的模塊之一。當你須要加載資源,就須要設置對應的 Loader,這樣就能夠對其源代碼進行轉換。node
因爲 Webpack 社區的繁榮,使得大部分的業務場景所使用的資源都有對用的 loader,能夠參考官網的 available loaders,可是因爲業務的獨特性,也可能沒有適用的 loader。webpack
接下來會經過幾個示例來讓你學會如何開發一個本身的 loader。但在此以前,最好先了解如何單獨調試它們。web
loader-runner 容許你不依靠 webpack 單獨運行 loader,首先安裝它npm
mkdir loader-runner-example
npm init
npm install loader-runner --save-dev
複製代碼
接下來,建立一個 demo-loader,來進行測試bash
mkdir loaders
echo "module.exports = input => input + input;" > loaders/demo-loader.js
複製代碼
這個 loader 會將引入模塊的內容複製一次並返回。建立所引入的模塊異步
echo "Hello world" > demo.txt
複製代碼
接下來,經過loader-runner運行加載器:async
// 建立 run-loader.js
const fs = require("fs");
const path = require("path");
const { runLoaders } = require("loader-runner");
runLoaders(
{
resource: "./demo.txt",
loaders: [path.resolve(__dirname, "./loaders/demo-loader")],
readResource: fs.readFile.bind(fs),
},
(err, result) =>
(err ? console.error(err) : console.log(result))
);
複製代碼
當你運行 node run-loader.js
,會看到終端上 log 出來函數
{ result: [ 'Hello world\nHello world\n' ],
resourceBuffer: <Buffer 48 65 6c 6c 6f 20 77 6f 72 6c 64 0a>, cacheable: true, fileDependencies: [ './demo.txt' ], contextDependencies: [] } 複製代碼
從輸出結果中能夠看出工具
若是,須要將轉換後的文件輸出出來,只須要修改 runLoaders 的第二個參數,如
runLoaders(
{
resource: "./demo.txt",
loaders: [path.resolve(__dirname, "./loaders/demo-loader")],
readResource: fs.readFile.bind(fs),
},
(err, result) => {
if (err) console.error(err)
fs.writeFileSync("./output.txt", result.result)
}
);
複製代碼
儘管你能夠經過上述這種同步式接口(synchronous interface)實現一系列的 loader,可是這種形式並不能適用全部場景,例如將第三方軟件包包裝爲 loader 時就會強制要求你執行此操做。
爲了將上述例子調整爲異步的形式,咱們使用 webpack 提供的 this.async()
API。經過調用這個函數能夠返回一個遵照 Node 規範的回調函數(error first,result second)。
上述例子能夠改寫爲:
loaders/demo-loader.js
module.exports = function(input) {
const callback = this.async();
// No callback -> return synchronous results
// if (callback) { ... }
callback(null, input + input);
};
複製代碼
webpack 經過
this
進行注入,因此不能使用 () => {}。
以後運行 node run-loader.js
會在終端上打印出相同的結果。若是你想要在對 loader 執行期間產生的異常進行處理,則能夠
module.exports = function(input) {
const callback = this.async();
callback(new Error("Demo error"));
};
複製代碼
終端上打印的日誌會包含錯誤:demo error,堆棧跟蹤顯示錯誤發生的位置。
loader 也能夠用於單獨輸出代碼,能夠這樣實現
module.exports = function() {
return "foobar";
};
複製代碼
爲何要這麼作呢?你能夠將 webpack 的入口文件傳遞給 loader。來代替指向預先設定的文件的狀況,這樣能夠動態地生成對應 code 的 loader。
若是你想要 return 一個 Buffer 形式的輸出,能夠設定 module.exports.raw = true,將原有的 string 改成 buffer。
有一些 loader,像 file-loader,會生成文件。對此 webpack 提供了一個方法,this.emitFile
,可是 loader-runner 暫時還不支持,因此須要主動實現
runLoaders(
{
resource: "./demo.txt",
loaders: [path.resolve(__dirname, "./loaders/demo-loader")],
// 爲 this 添加 emitFile method
context: {
emitFile: () => {},
},
readResource: fs.readFile.bind(fs),
},
(err, result) => (err ? console.error(err) : console.log(result))
);
複製代碼
要實現 file-loader 的基本思想,您必須作兩件事:找出文件並返回它的路徑。 你能夠按以下方式實現:
const loaderUtils = require("loader-utils");
module.exports = function(content) {
const url = loaderUtils.interpolateName(this, "[hash].[ext]", {
content,
});
this.emitFile(url, content);
const path = `__webpack_public_path__ + ${JSON.stringify(url)};`;
return `export default ${path}`;
};
複製代碼
Webpack 提供了額外的兩個 emit
方法:
this.emitWarning(<string>)
this.emitError(<string>)
這些方法都是用來替代控制檯。 與 this.emitFile
同樣,你必須模擬它們才能使loader-runner工做。
接下來的問題是,如何將文件名傳遞給 loader。
爲了將所需的配置傳遞給 loader,咱們須要作一些修改
run-loader.js
const fs = require("fs");
const path = require("path");
const { runLoaders } = require("loader-runner");
runLoaders(
{
resource: "./demo.txt",
loaders: [
{
loader: path.resolve(__dirname, "./loaders/demo-loader"),
options: {
name: "demo.[ext]",
},
},
],
context: {
emitFile: () => {},
},
readResource: fs.readFile.bind(fs),
},
(err, result) => (err ? console.error(err) : console.log(result))
);
複製代碼
能夠看到,咱們將 loaders 從原有的
loaders: [path.resolve(__dirname, "./loaders/demo-loader")]
複製代碼
改成了,從而能夠傳遞 options
loaders: [
{
loader: path.resolve(__dirname, "./loaders/demo-loader"),
options: {
name: "demo.[ext]",
},
},
]
複製代碼
爲了可以獲取到,咱們傳遞的 options,依然利用 loader-utils 來解析 options。
別忘了 npm install loader-utils --save-dev
爲了將它與 loader 進行鏈接
loaders/demo-loader.js
const loaderUtils = require("loader-utils");
module.exports = function(content) {
// 獲取 options
const { name } = loaderUtils.getOptions(this);
const url = loaderUtils.interpolateName(this, "[hash].[ext]", {
content,
});
const url = loaderUtils.interpolateName(this, name, { content });
);
};
複製代碼
運行 node run-loader.js,你會發如今終端上打印出了
{ result:
[ 'export default __webpack_public_path__ + "f0ef7081e1539ac00ef5b761b4fb01b3.txt";' ],
resourceBuffer: <Buffer 48 65 6c 6c 6f 20 77 6f 72 6c 64 0a>,
cacheable: true,
fileDependencies: [ './demo.txt' ],
contextDependencies: [] }
複製代碼
能夠看出結果與 loader 應返回的內容一致。 你能夠嘗試將更多選項傳遞給 loader 或使用查詢參數來查看不一樣組合會發生什麼。
爲了進行一步地使用 loader,咱們須要將它與 webpack 聯繫起來。在這裏,咱們採用內聯的形式引入自定義 loader
// webpack.config.js 中引入
resolveLoader: {
alias: {
"demo-loader": path.resolve(
__dirname,
"loaders/demo-loader.js"
),
},
},
// 在文件中指定 loader,引入
import "!demo-loader?name=foo!./main.css"
複製代碼
固然你還能夠經過規則處理 loader。一旦它足夠穩定,就創建一個基於 webpack-defaults 的項目,將邏輯推送到 npm,而後開始將 loader 做爲包使用。
儘管咱們使用 loader-runner 來做爲開發、測試 loader 的環境。可是它與 webpack 仍是有細微的不一樣的,因此還須要在 webpack 上測試一下。
webpack 分爲兩個階段來執行 loader:pitching、evaluating。若是你熟悉 web 的事件系統,它與事件的捕獲、冒泡很類似。webpack 容許你在 pitching 階段進行攔截執行。它的順序是,從左到右pitch,從右到左執行。
一個 pitch loader 容許你對請求進行修改,甚至終止它。 例如,建立
loaders/pitch-loader.js
const loaderUtils = require("loader-utils");
module.exports = function(input) {
const { text } = loaderUtils.getOptions(this);
return input + text;
};
module.exports.pitch = function(remainingReq, precedingReq, input) {
console.log(` Remaining request: ${remainingReq} Preceding request: ${precedingReq} Input: ${JSON.stringify(input, null, 2)} `);
return "pitched";
};
複製代碼
並將其添加到 run-loader.js 中,
...
loaders: [
{
loader: path.resolve (__dirname, './loaders/demo-loader'),
options: {
name: 'demo.[ext]',
},
},
path.resolve(__dirname, "./loaders/pitch-loader"),
],
...
複製代碼
執行 node run-loader.js
Remaining request: ./demo.txt
Preceding request: .../webpack-demo/loaders/demo-loader?{"name":"demo.[ext]"}
Input: {}
{ result: [ 'export default __webpack_public_path__ + "demo.txt";' ],
resourceBuffer: null,
cacheable: true,
fileDependencies: [],
contextDependencies: [] }
複製代碼
你會發現 pitch-loader 完成了信息的插入以及執行的攔截。
webpack loader 實質上就是在描述一種文件格式如何轉換爲另外一種文件格式。你能夠經過研究 API 文檔或現有的 loader 來弄清楚如何實現特定的功能。
回顧下:
loader-runner
是一個很是實用的工具,用來開發、調試 loader;this.async
來編寫異步的 loader;loader-utils
可以編譯 loader 的配置,還能夠經過 schema-utils
進行驗證;resolveLoader.alias
來完成局部的自定義 loader 引入,防止影響全局;