ローダーの記述

ローダーは、関数をエクスポートするノードモジュールです。この関数は、リソースがこのローダーによって変換される必要があるときに呼び出されます。指定された関数は、それに提供される`this`コンテキストを使用して、ローダーAPIにアクセスできます。

設定

さまざまなタイプのローダー、その使用方法、および例について詳しく説明する前に、ローカルでローダーを開発およびテストできる3つの方法を見てみましょう。

単一のローダーをテストするには、ルールオブジェクト内でローカルファイルを解決するために`path`を使用できます。

webpack.config.js

const path = require('path');

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

複数をテストするには、`resolveLoader.modules`設定を使用して、webpackがローダーを検索する場所を更新できます。たとえば、プロジェクトにローカルの`/loaders`ディレクトリがある場合

webpack.config.js

const path = require('path');

module.exports = {
  //...
  resolveLoader: {
    modules: ['node_modules', path.resolve(__dirname, 'loaders')],
  },
};

ちなみに、ローダー用に個別のリポジトリとパッケージを既に作成している場合は、npm linkを使用して、テストしたいプロジェクトにリンクできます。

簡単な使用方法

単一のローダーがリソースに適用されると、ローダーは1つのパラメーター(リソースファイルのコンテンツを含む文字列)のみを使用して呼び出されます。

同期ローダーは、変換されたモジュールを表す単一の値を返すことができます。より複雑なケースでは、ローダーは`this.callback(err, values...)`関数を使用して任意の数の値を返すことができます。エラーは`this.callback`関数に渡されるか、同期ローダーでスローされます。

ローダーは1つまたは2つの値を返す必要があります。最初の値は、文字列またはバッファーとしての結果のJavaScriptコードです。2番目のオプションの値は、JavaScriptオブジェクトとしてのソースマップです。

複雑な使用方法

複数のローダーがチェーンされると、それらが逆順(配列形式に応じて右から左、または下から上)に実行されることを覚えておくことが重要です。

  • 最初に呼び出される最後のローダーには、生のリソースの内容が渡されます。
  • 最後に呼び出される最初のローダーは、JavaScriptとオプションのソースマップを返す必要があります。
  • 間のローダーは、チェーン内の前のローダーの結果を使用して実行されます。

次の例では、`foo-loader`には生のリソースが渡され、`bar-loader`は`foo-loader`の出力を受信し、必要に応じて最終的な変換されたモジュールとソースマップを返します。

webpack.config.js

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.js/,
        use: ['bar-loader', 'foo-loader'],
      },
    ],
  },
};

ガイドライン

ローダーを作成する際には、次のガイドラインに従う必要があります。重要度の順に並べられており、一部は特定のシナリオでのみ適用されます。詳細については、次のセクションを参照してください。

  • シンプルに保つ。
  • チェーンを利用する。
  • モジュール式の出力を生成する。
  • ステートレスにする。
  • ローダーユーティリティを使用する。
  • ローダーの依存関係をマークする。
  • モジュールの依存関係を解決する。
  • 共通コードを抽出する。
  • 絶対パスを避ける。
  • ピア依存関係を使用する。

シンプル

ローダーは単一のタスクのみを実行する必要があります。これにより、各ローダーの保守作業が容易になるだけでなく、より多くのシナリオで使用するためにチェーンできるようになります。

チェーン

ローダーをチェーンできることを利用します。5つのタスクに取り組む単一のローダーを作成する代わりに、この作業を分割する5つのより単純なローダーを作成します。それらを分離することで、個々のローダーをシンプルに保つだけでなく、当初は思いつかなかった用途にも使用できる可能性があります。

ローダーオプションまたはクエリパラメーターで指定されたデータを使用してテンプレートファイルを表示する場合を考えてみましょう。ソースからテンプレートをコンパイルし、実行して、HTMLコードを含む文字列をエクスポートするモジュールを返す単一のローダーとして記述できます。しかし、ガイドラインに従って、他のオープンソースローダーとチェーンできる`apply-loader`が存在します。

  • `pug-loader`:テンプレートを関数をエクスポートするモジュールに変換します。
  • `apply-loader`:ローダーオプションを使用して関数を実行し、生のHTMLを返します。
  • `html-loader`:HTMLを受け取り、有効なJavaScriptモジュールを出力します。

モジュール式

出力をモジュール式に保ちます。ローダーによって生成されたモジュールは、通常のモジュールと同じ設計原則を遵守する必要があります。

ステートレス

ローダーがモジュールの変換間で状態を保持しないようにします。各実行は、他のコンパイル済みモジュールだけでなく、同じモジュールの以前のコンパイルからも常に独立している必要があります。

ローダーユーティリティ

さまざまな便利なツールを提供するloader-utilsパッケージを利用します。loader-utilsに加えて、schema-utilsパッケージを使用して、ローダーオプションの一貫性のあるJSONスキーマベースの検証を行う必要があります。両方を活用した簡単な例を以下に示します。

loader.js

import { urlToRequest } from 'loader-utils';
import { validate } from 'schema-utils';

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

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

  validate(schema, options, {
    name: 'Example Loader',
    baseDataPath: 'options',
  });

  console.log('The request path', urlToRequest(this.resourcePath));

  // Apply some transformations to the source...

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

ローダーの依存関係

ローダーが外部リソースを使用する場合(つまり、ファイルシステムから読み取る場合)、それを**必ず**示す必要があります。この情報は、キャッシュ可能なローダーを無効にし、ウォッチモードで再コンパイルするために使用されます。`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では、`@import`および`url(...)`ステートメントが使用されます。これらの依存関係は、モジュールシステムによって解決される必要があります。

これは2つの方法のいずれかで実行できます。

  • `require`ステートメントに変換することによって。
  • `this.resolve`関数を使用してパスを解決することによって。

`css-loader`は、最初の方法の良い例です。`@import`ステートメントを他のスタイルシートへの`require`に、`url(...)`を参照ファイルへの`require`に置き換えることで、依存関係を`require`に変換します。

less-loaderの場合、すべての.lessファイルは変数とmixinの追跡のために一度にコンパイルする必要があるため、各@importrequireに変換することはできません。そのため、less-loaderはカスタムパス解決ロジックを使用してlessコンパイラを拡張します。その後、第2の方法であるthis.resolveを利用して、Webpackを介して依存関係を解決します。

共通コード

ローダーが処理するすべてのモジュールで共通コードを生成するのを避けてください。代わりに、ローダーにランタイムファイルを作成し、その共有モジュールへのrequireを生成してください。

src/loader-runtime.js

const { someOtherModule } = require('./some-other-module');

module.exports = function runtime(params) {
  const x = params.y * 2;

  return someOtherModule(params, x);
};

src/loader.js

import runtime from './loader-runtime.js';

export default function loader(source) {
  // Custom loader logic

  return `${runtime({
    source,
    y: Math.random(),
  })}`;
}

絶対パス

プロジェクトのルートが移動された場合、ハッシュ処理が壊れるため、絶対パスをモジュールコードに挿入しないでください。以下のコードを使用して、絶対パスを相対パスに変換できます。

// `loaderContext` is same as `this` inside loader function
JSON.stringify(loaderContext.utils.contextify(loaderContext.context || loaderContext.rootContext, request));

ピア依存関係

作成中のローダーが別のパッケージの単純なラッパーである場合、そのパッケージをpeerDependencyとして含める必要があります。この方法により、アプリケーションの開発者は必要に応じてpackage.jsonで正確なバージョンを指定できます。

たとえば、sass-loaderこのようにnode-sassをピア依存関係として指定しています。

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

テスト

ローダーを作成し、上記のガイドラインに従ってローカルで実行するように設定しました。次は何でしょうか? ローダーが期待通りに動作していることを確認するための単体テストの例を見ていきましょう。Jestフレームワークを使用してこれを行います。また、import / exportおよびasync / awaitを使用できるようにするbabel-jestといくつかのプリセットもインストールします。まずはこれらをdevDependenciesとしてインストールして保存しましょう。

npm install --save-dev jest babel-jest @babel/core @babel/preset-env

babel.config.js

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          node: 'current',
        },
      },
    ],
  ],
};

ローダーは.txtファイルを処理し、[name]のインスタンスをローダーに指定されたnameオプションに置き換えます。その後、テキストをデフォルトエクスポートとして含む有効なJavaScriptモジュールを出力します。

src/loader.js

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

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

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

このローダーを使用して、次のファイルを処理します。

test/example.txt

Hey [name]!

次のステップに注意してください。Webpackを実行するためにNode.js APImemfsを使用します。これにより、ディスクへのoutputの出力は避けられ、変換されたモジュールを取得するために使用できるstatsデータにアクセスできます。

npm install --save-dev webpack memfs

test/compiler.js

import path from 'path';
import webpack from 'webpack';
import { createFsFromVolume, Volume } from 'memfs';

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,
          },
        },
      ],
    },
  });

  compiler.outputFileSystem = createFsFromVolume(new Volume());
  compiler.outputFileSystem.join = path.join.bind(path);

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

      resolve(stats);
    });
  });
};

そして最後に、テストを記述し、それを実行するためのnpmスクリプトを追加できます。

test/loader.test.js

/**
 * @jest-environment node
 */
import compiler from './compiler.js';

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

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

package.json

{
  "scripts": {
    "test": "jest"
  },
  "jest": {
    "testEnvironment": "node"
  }
}

すべてが整ったら、実行して新しいローダーがテストに合格するかどうかを確認できます。

 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.

成功しました!この時点で、独自のローダーの開発、テスト、デプロイを開始する準備が整っているはずです。皆様の作品をコミュニティの皆さんと共有していただければ幸いです!

7 貢献者

asulaimanmichael-ciniawskybyzykanikethsahajamesgeorge007chenxsandev-itsheng