從零實現一個 Webpack Loader

參考:css

  • Webpack Book --- Extending with Loaders。
  • Webpack Doc --- Loader Interface

Loader 是 Webpack 幾大重要的模塊之一。當你須要加載資源,就須要設置對應的 Loader,這樣就能夠對其源代碼進行轉換。node

因爲 Webpack 社區的繁榮,使得大部分的業務場景所使用的資源都有對用的 loader,能夠參考官網的 available loaders,可是因爲業務的獨特性,也可能沒有適用的 loader。webpack

接下來會經過幾個示例來讓你學會如何開發一個本身的 loader。但在此以前,最好先了解如何單獨調試它們。web

利用 loader-runner 調試 Loaders

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: [] } 複製代碼

從輸出結果中能夠看出工具

  • result:loader 完成了,咱們賦予它的任務,將目標模塊的內容複製了一邊;
  • resourceBuffer:模塊內容被轉換爲了 Buffer。

若是,須要將轉換後的文件輸出出來,只須要修改 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)
  }
);
複製代碼

開發一個異步的 Loader

儘管你能夠經過上述這種同步式接口(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

爲了將所需的配置傳遞給 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 或使用查詢參數來查看不一樣組合會發生什麼。

鏈接 webpack 與 自定義 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 上測試一下。

Pitch Loaders

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;
  • webpack loader 是依據輸入來生成輸出的;
  • loader 分爲同步、異步兩種形式,異步的能夠經過 this.async 來編寫異步的 loader;
  • 能夠利用 loader 來爲 webpack 動態地生成代碼,這種狀況下,loader 沒必要接受輸入;
  • 使用 loader-utils 可以編譯 loader 的配置,還能夠經過 schema-utils 進行驗證;
  • 利用 resolveLoader.alias 來完成局部的自定義 loader 引入,防止影響全局;
  • Pitching 階段容許你對 loader 的輸入進行修改或攔截執行順序。
相關文章
相關標籤/搜索