[JavaScript講座] 動的インポートとモジュール情報

当ページのリンクには広告が含まれています。
目次

静的インポート vs 動的インポート

静的インポート(おさらい)

これまで使ってきたのが 静的インポート です:

// mathUtil.js
export function add(a, b) {
  return a + b;
}
// main.js
import { add } from "./mathUtil.js";

console.log(add(1, 2));

特徴:

  • ファイルの先頭レベル(top-level)でしか書けない
    • if 文の中や関数の中では書けない(構文エラー)
  • モジュール解析時に、依存関係がすべてわかる(静的解析可能)
  • 実行前に、「どのモジュールを読み込むか」が確定している

動的インポート

一方、実行時の状況に応じてモジュールを読みたいことがあります。

  • ユーザーがボタンを押したときにだけ、重いライブラリを読み込みたい
  • 特定の条件に当てはまるときだけ、別モジュールに処理を任せたい
  • プラグイン名を文字列で受け取って、その名前のモジュールを読み込みたい

このようなときに使うのが 動的インポート です:

// 関数っぽい構文だが、実は特殊構文
const promise = import("./mathUtil.js");

特徴:

  • import("./path.js") は Promise を返す
  • どこでも書ける(関数の中 / if の中 / イベントハンドラなど)
  • 文字列でパスを組み立てられる(import(./plugins/${name}.js) など)

import() の基本構文と使い方

then / catch で使う

import("./mathUtil.js")
  .then((module) => {
    console.log(module.add(1, 2));
  })
  .catch((err) => {
    console.error("モジュール読み込み失敗:", err);
  });
  • module は「モジュールオブジェクト」
    export されたものがプロパティとしてぶら下がっているイメージ
// mathUtil.js がこうだとすると:
export const PI = 3.14;
export function add(a, b) { ... }
export default function mul(a, b) { ... }
// import() 結果の module には
module.PI;        // 3.14
module.add;       // 関数
module.default;   // デフォルトエクスポート(mul)

await import(...) で使う(よく使う形式)

async 関数の中では、await を使って同期っぽく書けます:

async function main() {
  try {
    const module = await import("./mathUtil.js");
    console.log(module.add(1, 2));
  } catch (e) {
    console.error("読み込み失敗:", e);
  }
}

構造分割で直接取り出す書き方もよく使われます:

async function main() {
  const { add, PI } = await import("./mathUtil.js");
  console.log(add(1, 2), PI);
}
  • 静的インポートと違い、「実行時に読み込む」ことを強く意識する必要があります
  • エラー処理は try/catch で OK(await なので reject → 例外化)

動的インポートの主な用途

コード分割(遅延読み込み)

例:ボタンを押したときだけ、重いグラフ描画ライブラリを読み込みたい。

document.getElementById("showChart").addEventListener("click", async () => {
  const { drawChart } = await import("./chart.js");
  drawChart();
});
  • 初期表示では chart.js を読み込まないので、初回ロードが軽くなる
  • 実際のプロダクションでは、バンドラ(webpack / Vite など)が
    import() を手掛かりに JS ファイルを分割してくれます(ここではイメージだけでOK)

条件付きで別実装を読み込む

async function loadStorageImpl() {
  if ("indexedDB" in window) {
    return import("./storage-indexeddb.js");
  } else {
    return import("./storage-fallback.js");
  }
}

async function main() {
  const storageModule = await loadStorageImpl();
  // storageModule.save(), storageModule.load() などを使う
}
  • 環境に応じて「どの実装を使うか」を切り替えたいときに便利
  • プラグイン構造 にも応用しやすい

「名前でモジュールを指定」するプラグイン的な使い方

async function loadPlugin(name) {
  const module = await import(`./plugins/${name}.js`);
  return module;
}

async function runPlugin(name, context) {
  const plugin = await loadPlugin(name);
  await plugin.run(context); // 各プラグインは run() をエクスポートしている想定
}
  • パスを動的に組み立てられるのは静的 import にはない強み
  • バンドラ使用時は、「どのパスがビルド対象か」を設定で制限する場合があります
    → ここは将来的に現場で設定ファイルを触るときに意識すればOK

動的インポートのエラー処理とキャッシュ

読み込み失敗時の挙動

import() が失敗する主なケース:

  • ネットワークエラー(ファイルが取得できない)
  • 404 などでモジュールが存在しない
  • モジュールの構文エラー(パースに失敗)

これらは Promise の reject として扱われます。

async function main() {
  try {
    const module = await import("./not-exist.js");
  } catch (e) {
    console.error("読み込みエラー:", e);
  }
}

キャッシュ挙動(静的インポートと同じ)

同じ URL(パス)に対しての import() は、

  • 最初の1回だけ実際に読み込まれて評価される
  • 2回目以降は キャッシュされたモジュールオブジェクトが返る
async function loadTwice() {
  const m1 = await import("./counter.js");
  const m2 = await import("./counter.js");

  console.log(m1 === m2); // true(同じモジュールインスタンス)
}
  • 静的インポートの場合と同様、「モジュールは1度だけ評価→以降は共有」というイメージでOKです。

import.meta:モジュールに関するメタ情報

import.meta とは

モジュールの内部で import.meta と書くと、
そのモジュールに関するメタデータを持つオブジェクトにアクセスできます。

代表的なのは:

  • import.meta.url:そのモジュール自身のURL
// someModule.js(モジュール内)
console.log(import.meta);      // { url: "..." } など(環境依存で他のプロパティも)
console.log(import.meta.url);  // このファイルのURL文字列

import.meta は モジュール内でしか使えません。
従来の <script>(非モジュール)では未定義です。

import.meta.url の使いどころ(ブラウザ)

ブラウザでは、import.meta.url は大体こんな値になります:

https://example.com/js/modules/someModule.js

これを基準に new URL を使うことで、
「このモジュールファイルから見た相対パス」を解決できます。

// someModule.js
const jsonUrl = new URL("./data/config.json", import.meta.url);
// → https://example.com/js/modules/data/config.json など

fetch(jsonUrl)
  .then(res => res.json())
  .then(config => {
    console.log("設定:", config);
  });
  • HTML ファイルからの相対パスではなく、
    「このJSモジュールからの相対パス」 を扱えるのがポイント
  • 複数の場所からモジュールが読み込まれても、自モジュールに付随するファイルを確実に参照できる

Node.js での import.meta.url

Node.js では import.meta.url は、だいたいこんな文字列になります:

file:///Users/you/project/src/someModule.mjs

これを fileURLToPath で OS のパスに変換して、

  • __dirname / __filename 相当の値を得る
  • 同じディレクトリにあるファイルを読み込む

といった用途で使います。

「Node でもモジュールパスの基準として使える」ぐらいの認識でOKです。

例:ダイアログ機能を遅延ロードする

簡単な実用イメージを一つ紹介します。

構成

/project
  index.html
  main.js
  dialog.js

dialog.js

export function showDialog(message) {
  alert("ダイアログ: " + message);
}

main.js

document.getElementById("openDialog").addEventListener("click", async () => {
  const { showDialog } = await import("./dialog.js");
  showDialog("こんにちは!");
});

index.html

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>Dynamic Import Example</title>
  </head>
  <body>
    <button id="openDialog">ダイアログを開く</button>
    <script type="module" src="./main.js"></script>
  </body>
</html>

ポイント:

  • ページ表示時点では dialog.js はまだ読み込まれていない(=初期ロードが軽い)
  • ボタンが押された瞬間にだけ import("./dialog.js") が走り、
    そのモジュールがネットワーク越しに取得される
  • バンドラを噛ませると、自動で「チャンク分割」される

<<前へ(ES Modules の基本)

>>次へ(npmとパッケージ管理)

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次