ローダーは、関数をエクスポートするノードモジュールです。この関数は、リソースがこのローダーによって変換される必要があるときに呼び出されます。指定された関数は、それに提供される`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オブジェクトとしてのソースマップです。
複数のローダーがチェーンされると、それらが逆順(配列形式に応じて右から左、または下から上)に実行されることを覚えておくことが重要です。
次の例では、`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`が存在します。
出力をモジュール式に保ちます。ローダーによって生成されたモジュールは、通常のモジュールと同じ設計原則を遵守する必要があります。
ローダーがモジュールの変換間で状態を保持しないようにします。各実行は、他のコンパイル済みモジュールだけでなく、同じモジュールの以前のコンパイルからも常に独立している必要があります。
さまざまな便利なツールを提供する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つの方法のいずれかで実行できます。
`css-loader`は、最初の方法の良い例です。`@import`ステートメントを他のスタイルシートへの`require`に、`url(...)`を参照ファイルへの`require`に置き換えることで、依存関係を`require`に変換します。
less-loader
の場合、すべての.less
ファイルは変数とmixinの追跡のために一度にコンパイルする必要があるため、各@import
をrequire
に変換することはできません。そのため、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 APIとmemfs
を使用します。これにより、ディスクへの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.
成功しました!この時点で、独自のローダーの開発、テスト、デプロイを開始する準備が整っているはずです。皆様の作品をコミュニティの皆さんと共有していただければ幸いです!