JavaScriptの開発におけるLoaderとは
昨今のJavaScriptおよびその周辺技術を用いた開発では、“Loader”が暗躍しています。
今回は「Loaderとは」から「実際どのように動くのか」をみていきます。
Loaderとは
今回述べるLoaderとは、簡単に説明すると「importを解決する仕組み」のことを指しています。
この時点でLoaderの心当たりがある方は、この記事の内容は読まなくても大丈夫です。
さて、importを解決するといっても、基本的にはECMAScriptの仕様に基づいて解決されます。
この仕様書を読んでもLoaderという単語はパッと見なさそうです。
ではこの記事では何を指してLoaderと言っているのかというと、
JavaScript以外のimportに用いられる仕組みを指しています。
JavaScript以外のimport
Reactを書いたことがある人は、一度は以下のようなコードを見た/書いたことがあるのではないでしょうか。
import classes from "./Component.module.css";
CSSのimportです。
他にも画像やMarkdown、WebAssemblyなど様々なファイル種別をimportすることがあります。
先ほど確認した?ようにECMAScriptにはECMAScript moduleの解決の仕組みが備わっていますが、CSSや画像などはこれに該当しません。
ここで登場するのがLoaderという仕組みです。
※:今回は総称としてLoaderと呼びますが、ツールによっては別の名称を用いている場合があります。
Loaderを体験する
Loaderがどのようなものかわかったところで、実際どのように動くのかを体験してみましょう。
以下のような2つのファイルがあると仮定します。
こんにちは世界!
import hoge from './hoge.txt';
console.log(hoge);
index.js
を実行したとき、どのように動くでしょうか。
当然ですが、ECMAScript moduleではない hoge.txt
は解決できず、エラーになります。
Node.jsでの実行結果
❯ node index.js
(node:77909) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
/Dev/loader-test/index.js:1
import hoge from './hoge.txt';
^^^^^^
SyntaxError: Cannot use import statement outside a module
at internalCompileFunction (node:internal/vm:73:18)
at wrapSafe (node:internal/modules/cjs/loader:1176:20)
at Module._compile (node:internal/modules/cjs/loader:1218:27)
at Module._extensions..js (node:internal/modules/cjs/loader:1308:10)
at Module.load (node:internal/modules/cjs/loader:1117:32)
at Module._load (node:internal/modules/cjs/loader:958:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
at node:internal/main/run_main_module:23:47
Node.js v18.16.0
このプログラムが動くようにLoaderを導入してみましょう。
今回はBunを使ってみます。
Bunにはプラグインの仕組みがあり、それを使うことで簡単にLoaderを設定できます。
以下の2ファイルを設置します。
preload = ["./preload.js"]
import { plugin } from "bun";
plugin({
name: "Text loader",
async setup(build) {
const { readFileSync } = await import("fs");
build.onLoad({ filter: /\.txt$/ }, (args) => {
const text = readFileSync(args.path, "utf8");
return {
contents: `export default ${JSON.stringify(text)}`,
loader: 'js',
};
});
},
});
実行してみます。
❯ bun index.js
こんにちは世界!
でました!
プラグイン部分を振り返っていきましょう。
plugin
関数を呼んでいますが、これはBunのランタイムにプラグインを登録する処理です。
その引数にオブジェクトが渡され、 name
にはプラグインの名前、 setup
ではプラグインの処理を書いていきます。
build.onLoad
で .txt
ファイルが読み込まれた時の処理(Loader部分)を実装しています。
その内部ではファイルを読み込み、文字列として export default
している様子がわかります。
実は、Bunでは今回のLoaderを書かずともエラーは発生しません。
今回のLoaderがない場合にどう動くのかは、ぜひ手元で試してみてください。
LoaderとTypeScript
LoaderによってJavaScript以外のコードが扱われる仕組みを確認しました。
ですが、普段の開発ではこれだけでは難があります。TypeScriptの存在です。
JavaScript上では、 .txt
を読み込めるようになりましたが、依然としてTypeScriptでは .txt
がモジュールとして解釈できません。
そこで、 *.d.ts
が登場します。
先の .txt
をTypeScriptでも扱えるようにするには次のような宣言が必要になります。
declare module "*.txt" {
const content: string;
export default content;
}
「*.txt
ファイルは string
型の値が default export されたモジュール」と宣言されていることがわかります。
このように、なにかしらのLoaderが働いている場合、そのLoaderに対応する型定義が必要になります。
おわりに
Loaderとはどのようなものかとその動きを体験しました。
今回紹介したLoaderの仕組みは昨今のフロントエンド開発では欠かせないものとなっています。
ぜひ、普段使っているimportがどのように解決されているのかを確認してみてください。
Discussion