如何編寫本身的webpack加載器

所謂的加載器其實就是一個導出函數的node模塊。若是有資源須要該加載器處理,webpack就會自動調用該加載器。這個返回的函數經過上下文提供的this能夠訪問Loader API.javascript

設置

在咱們深刻了解不一樣種類的加載器以及他們的用法、相關案例以前,咱們先來看看在本地開發和測試加載器的三種方式吧。
想要測試一個加載器,你能夠在rules對象裏面用pathresolve一個本地文件:css

{
  test: /\.js$/
  use: [
    {
      loader: path.resolve('path/to/loader.js'),
      options: {/* ... */}
    }
  ]
}

想要測試多個加載器,你能夠經過resolveLoader.modules配置項告訴webpack應該去哪兒尋找加載器。
好比項目裏面有個目錄/loaders:html

resolveLoader: {
  modules: [
    'node_modules',
    path.resolve(__dirname, 'loaders')
  ]
}

最後,若是你以前已經單獨爲加載器建立了一個倉庫,不要忘了使用npm link將加載器連接到你當前要測試的項目。java

簡單用法

加載器處理資源的時候只是傳入一個字符串參數,也就是那個資源文件裏面的內容。
同步加載器能夠簡單地返回一個表明被處理資源的值。不過更復雜狀況下,也能夠經過this.callback(err, values...)返回任意數量的值。錯誤(Errors)要麼被傳入那個this.callback函數裏面,要麼就在同步加載器裏面拋出。
加載器應該返回1個或者2個值。第一個應該是String或者Buffer類型值,表明處理中的JS代碼;第二個是一個JS對象,就是所謂的SourceMap .node

複雜用法

多個加載器鏈式調用時,值得注意的就是,他們的執行順序是倒敘的---根據數組格式寫法,要麼從右向左,要麼從下向上。webpack

  1. 最後一個加載器,最早被調用,它要處理原始的資源。
  2. 第一個加載器,最後被調用,應該返回JS代碼以及一個可選的 source map.
  3. 中間的加載器,每個加載器處理的都是前一個加載器執行完的結果。

所以,下面的例子中,foo-loader會首先被調用,處理原始資源,而後返回值傳給bar-loader.bar-loader執行完後返回最終的JS結果,有必要的話還會返回Source map.web

{
  test: /\.js/,
  use: [
    'bar-loader',
    'foo-loader'
  ]
}

編寫原則

加載器編寫須要遵循如下原則。根據重要性排列以下。其中有些只在特定場景下使用。想了解更多能夠查看隨後的詳情介紹。npm

  • 儘量簡單
  • 使用鏈式
  • 模塊化輸出
  • 確保無狀態
  • 利用加載器公共功能
  • 標記加載器依賴
  • 解析模塊依賴
  • 抽離公共代碼
  • 避免絕對路徑
  • 使用peer dependencies

簡單

加載器應該只作某個單一的事情。這不但使得維護更容易,同時也容許在更多場景下去鏈式使用加載器。(其實就是功能比較單一的加載器在複雜的場景下能夠被鏈式的組合使用)json

鏈式調用

充分利用加載器能夠鏈式調用這一特性。好比,咱們應該編寫5個加載器,讓每一個加載器去處理一項任務,而不是編寫一個處理5項任務的加載器。這種分離不但使得加載器極其簡單,並且有時候還能在你原先沒有想到場景下使用。
考慮一下。使用加載器options選項或者query參數提供的數據去渲染一個模板文件的場景。這個加載器會首先讀取源模板文件,執行,並最終返回一個包含所有html代碼的字符串。然而爲了符合上面準則(簡單,單一原則),apply-loader能夠用來簡單的連接其餘開源加載器。api

  • jade-loader:將模板轉換爲一個導出函數的模塊。
  • apply-loader:使用加載器options執行那個函數,並返回原生的html。
  • html-loader:傳入html,並返回一個有效的JS模塊。
事實上加載器能夠連接也就意味着他們不必定都返回JS代碼。只要加載器執行對列下一個加載器可以處理,加載器就能返回任意類型的模塊。

模塊化

保持模塊化輸出。加載器產生的模塊應該遵循一樣的設計原則。

無狀態

保證加載器在模塊轉換的時候不要保持狀態。買一次執行加載器都不該該收到其餘已編譯或者同一個模塊往次編譯加過的影響。

加載器實用工具

使用加載器工具包loader-utils。它提供了不少有用的工具,尤爲重要的是獲取傳入加載器的options的工具。同時使用schema-utils包能夠被用來保證加載器options校驗的一致性。下面有一個簡短的案列。

import { getOptions } from 'loader-utils';
import validateOptions from 'schema-utils';

const schema = {
  type: 'object',
  properties: {
    test: {
      type: 'string'
    }
  }
}

export default function(source) {
  const options = getOptions(this);

  validateOptions(schema, options, 'Example Loader');

  // Apply some transformations to the source...

  return `export default ${ JSON.stringify(source) }`;
};

加載器依賴

若是加載器使用了外部資源(好比,從文件系統讀入),就必需要指明。在watch模式下,這個信息能夠用來清除失效的加載器緩存,並從新編譯。下面是使用addDependency方法實現這一功能的一個簡短案例:
loader.js:

import path from 'path';

export default function(source) {
  var callback = this.async();
  var headerPath = path.resolve('header.js');

  this.addDependency(headerPath);

  fs.readFile(headerPath, 'utf-8', function(err, header) {
    if(err) return callback(err);
    callback(null, header + "\n" + source);
  });
};

模塊依賴

因爲模塊的類型各類各樣,因此指定模塊依賴的方式也各有不一樣。好比在css中,咱們就使用@importurl(....)來指定相關依賴的。模塊系統會解析這些依賴的。
下面的兩種方法能夠完成:

  • 把他們所有轉換爲require語句。
  • 經過this.resolve函數解析路徑。

css-loader加載器就是第一種方案很好的一個案例。它把樣式表文件裏面的@import和url所有轉換爲require語句去引入相關資源了。
less-loader因爲要處理複雜的變量和mixins,因此根本無法把每個@import給轉換爲require。所以,less-loader在less編譯器的基礎上擴展了自定義的路徑解析邏輯。而後它利用上述第二種方案裏面的this.resolve去解析相關依賴。

注意:若是語言只接受相對路徑的url,你可使用`~`去引用已安裝的模塊(好比`node_modules`裏的模塊)。這種狀況下看起來是這樣子的`url('~some-library/image.jpg')`.

公共代碼

爲了不在加載器處理的模塊裏面生成重複的代碼,應該在加載器裏面生成一個運行時文件放在獨立的模塊裏面,而後讓每個加載器處理的模塊require那個共享的運行時文件。

絕對路徑

不要在代碼裏面使用絕對路徑,不然一旦項目根目錄遷移,之前的哈希名稱什麼的會破壞掉。loader-utils裏面的stringifyRequest方法能夠把絕對路徑轉換爲相對路徑。

同伴依賴

若是你的加載器只是在別的包上封裝而成的。你應該把那個包指定爲一個peerDependency.這樣容許開發者在package.json裏面指定準確的版本號。
好比。sass-loader指定node-sass做爲peer dependency

"peerDependencies": {
  "node-sass": "^4.0.0"
}

測試

如今你已經根據上面的原則編寫好本身的加載器了,並在本地運行起來了,接下來幹嗎呢?我們運行一個簡單的單元測試以確保加載器符合咱們的預期吧。這兒咱們使用Jest這個框架。爲了使用import / export async / await咱們把babel-jest以及一些babel預設都給安裝上。如今開始安裝並把他們保存爲devDependencies

npm install --save-dev jest babel-jest babel-preset-env

.babelrc

{
  "presets": [[
    "env",
    {
      "targets": {
        "node": "4"
      }
    }
  ]]
}

咱們的加載器會處理txt格式的文件,而且僅僅只是使用傳入加載器options裏面的name參數去替換txt文件裏面的[name].而後就會輸出轉換好的JS代碼咯。
src/loader.js

import { getOptions } from 'loader-utils';

export default function loader(source) {
  const options = getOptions(this);

  source = source.replace(/\[name\]/g, options.name);

  return `export default ${ JSON.stringify(source) }`;
};

而後使用這個加載器去處理下面這個文件:
test/example.txt

Hey [name]!

請注意接下來的這一步哦,咱們要使用Node API和memory-fs來執行webpack。這樣就避免了把輸出結果輸出到硬盤上了,經過訪問stats數據,咱們就能夠拿到轉換好了的模塊了。

npm install --save-dev webpack memory-fs

test/compiler.js

import path from 'path';
import webpack from 'webpack';
import memoryfs from 'memory-fs';

export default (fixture, options = {}) => {
  const compiler = webpack({
    context: __dirname,
    entry: `./${fixture}`,
    output: {
      path: path.resolve(__dirname),
      filename: 'bundle.js',
    },
    module: {
      rules: [{
        test: /\.txt$/,
        use: {
          loader: path.resolve(__dirname, '../src/loader.js'),
          options: {
            name: 'Alice'
          }
        }
      }]
    }
  });

  compiler.outputFileSystem = new memoryfs();

  return new Promise((resolve, reject) => {
    compiler.run((err, stats) => {
      if (err) reject(err);

      resolve(stats);
    });
  });
}
上面,咱們內聯了webpack的配置項。可是你依然可讓導出的函數接受一個webpack配置項做爲參數,這樣就可使用同一個編譯器模塊去測試不一樣的設置了。

如今,咱們就能夠編寫測試案例,並添加npm腳本,跑起來咯:
test/loader.test.js

import compiler from './compiler.js';

test('Inserts name and outputs JavaScript', async () => {
  const stats = await compiler('example.txt');
  const output = stats.toJson().modules[0].source;

  expect(output).toBe(`export default "Hey Alice!\\n"`);
});

package.json

"scripts": {
  "test": "jest"
}

一切就緒,如今就能夠跑起來看看咱們的加載器測試是否經過咯。

PASS  test/loader.test.js
  ✓ Inserts name and outputs JavaScript (229ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.853s, estimated 2s
Ran all test suites.

成功了!如今你就能夠去開發,測試,部署你本身的加載器咯。期待你的分享!

相關文章
相關標籤/搜索