⛓️

JavaScriptの開発におけるLoaderとは

2023/12/09に公開

昨今の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つのファイルがあると仮定します。

hoge.txt
こんにちは世界!
index.js
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ファイルを設置します。

bunfig.toml
preload = ["./preload.js"]
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