きっかけ
WordPressテーマの script.js がこのような構成になっていた。
import { initDrawer } from './_drawer.js';
import { initFadein } from './_fadein.js';
import { initTab } from './_tab.js';
// ...
document.addEventListener('DOMContentLoaded', () => {
initDrawer();
initFadein();
initTab();
});
「なぜ各ファイルで即時実行せず、export して呼び出すのか」という疑問から、ES Modules の仕組みを整理することになった。
export function init() パターンの意図
各ファイルで処理を export function initXxx() として定義し、script.js で呼び出すパターンには以下の意図が考えられる。
- 関心の分離:各ファイルが1つの機能に集中できる
- オーケストレーター:
script.jsが「何をいつ実行するか」を管理する役割になる - 再利用性:関数として定義されているため、別のエントリーポイントや条件付き実行に使いまわせる
シンプルな import "./_fadein.js" との違い
ファイルをインポートするだけのパターンと比較すると、根本的な違いは「いつ実行されるか」。
importだけ:インポートされた瞬間にファイル内の処理が即時実行されるexport+ 呼び出し:明示的に関数を呼んだときだけ実行される
type="module" 環境では DOM タイミング問題は起きない
<script type="module"> はデフォルトで defer 相当の挙動をする。DOM の解析が完了してから実行されるため、各ファイルに DOMContentLoaded を書く必要はない。
実行順序の違いはほぼない
インポート順に実行されるという点では、2つのパターンの制御の精度は変わらない。
実用上の差は2点
- 条件付き実行:
export+ 呼び出しパターンならifで制御できる(各ファイルに書いても同等) - コードの見通し:エントリーポイントを見るだけで「何が起動するか」がわかる(好みの範囲)
プロジェクト規約として「全ファイルを export + エントリーポイントで呼ぶ」に統一することで、ファイルを追加するときのルールが明確になるという実際的なメリットがある。
type="module" の本質
「ファイルをスコープとして独立させること」 が本質。
モジュールなしの問題
<script src="a.js"></script>
<script src="b.js"></script>
複数の <script> がグローバルスコープを共有するため、変数名の衝突が起きる。
// a.js
const foo = 'hello';
// b.js
console.log(foo); // 'hello' ← 別ファイルの変数が参照できてしまう
const foo = 'world'; // ❌ エラー:すでに foo が宣言されている
const/let は window オブジェクトには付かないが、同一ページ内の別ファイルから参照できてしまう点は var と同様の問題が起きる。
モジュールありでの解決
type="module" を付けると、各ファイルが独自のスコープを持ち、export しない限り他のファイルから見えなくなる。
// a.js(モジュール)
const foo = 'hello'; // このファイルのスコープに閉じる
// b.js(モジュール)
console.log(foo); // ❌ 参照できない
const foo = 'world'; // ✅ 衝突しない
import はモジュール環境でのみ使える
import 構文は type="module" 環境でのみ動作する。HTMLに書くのはエントリーポイント1ファイルだけでよく、import で連鎖するファイルをブラウザが自動取得する。
<!-- これだけでよい -->
<script type="module" src="script.js"></script>
バンドルなし vs Vite バンドル
import でファイルを分割して読み込む構成は、バンドラーなしでも動作する。では Vite などでバンドルする意味は何か。
| 観点 | バンドルなし | Vite バンドル |
|---|---|---|
| ファイル取得 | ファイル数分のHTTPリクエスト(HTTP/2で並列) | 1ファイルにまとまる |
| npm パッケージ | 使えない | 使える |
| SCSS等の変換 | 別途コンパイルが必要 | 一括処理できる |
| 本番最適化 | なし | minify・ハッシュ付きファイル名 |
| キャッシュ制御 | ファイル名が変わらず難しい | ハッシュで自動制御 |
HTTP/2環境ではファイルが並列取得されるため、ファイル数が増えても実用上の問題は少ない。課題になりやすいのはキャッシュの制御。
npm パッケージを使わず、SCSS は別途コンパイルしている構成であれば、バンドラーなしでも十分成立する。
バンドル後のスコープ
バンドラーは各モジュールを関数スコープで包んで独立させるため、type="module" の有無にかかわらず、バンドル後はスコープが保護される。
// バンドル後のイメージ
(function() {
// a.js の中身
const foo = 'hello';
})();
(function() {
// b.js の中身
const foo = 'world'; // 衝突しない
})();
これにより、古いブラウザ向けに type="module" なしで出力することも可能になる。
まとめ
type="module"の本質は「ファイルをスコープとして独立させること」import構文はモジュール環境でのみ使えるため、エントリーポイント1ファイルをHTMLに書けばよいexport function init()パターンとシンプルなimportパターンの実用上の差は小さい。一貫性のある規約として採用されることが多い- バンドラーの主なメリットはnpmパッケージの利用・本番最適化・キャッシュ制御。ファイル数の問題ではない