node.js では、ES2015 よりも前にモジュールを分割する require 構文が実装されていたりなどしていたが、現在はネイティブの JavaScript でモジュール構文を使用することができる。

ECMAScript 2015のModules の標準仕様として策定されており、この機能は、ES2015 Modules、ECMAScript Modules、ES Modules、ESMなどと呼ばれている。

<script type="module" src="/module1.mjs"></script>

JavaScript モジュール使用の際の注意点と使用方法

  • モジュールを使用する場合は file プロトコルだと CORS エラーが発生するため、http サーバーを立てる必要がある
  • モジュール内は常に Strict モードのため、use strict 構文を追加する必要はない
  • 各モジュールには独自の最上位のスコープがあり、モジュール内の最上位の変数や関数は他のスクリプトからは使用できず、export と import 構文を通してしか利用できない(ブラウザの場合、グローバル変数である window プロパティを介してグローバルな変数を作成できるが、あきらかなバッドプラクティスである)
  • ルートコンテキストで this はブラウザでは window だが、undefined となる
  • IE 以外は使用可能
    https://caniuse.com/?search=type%20module
  • ネイティブの JavaScript モジュールを使用する場合は、ファイル内容を明確化するため拡張子を .mjs とすることが推奨されているが、ほとんどのサーバーは .mjs に対して Content-Type ヘッダーで text/javascript を返さないため、MIME タイプチェックエラーが表示され、ブラウザーは JavaScript を実行しない。
    そのため、いまだよく .js 拡張子が使用されている
    https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Modules
  • HTMLに記述した script タグのリンク先にGETパラメーターをつけてのキャッシュバスター(例:hoge.js?2023)ができないため、JavaScript のインポート文を直接修正しなければならない
    ※ ただし、小さい容量の JavaScript ファイルのみキャッシュを削除する場合には、必要分だけキャッシュを削除することがしやすくなり、キャッシュヒット率を高めることができる
  • JSファイルが細かくなりすぎることで、転送ファイル数が大きくなりやすくなり、HTTP/1.1プロトコルでは同時接続数が限られるため遅くなってしまう
    ※ HTTP/2プロトコルの場合は同時接続数に制限がなく、昔流行ったCSSスプライトなどもやりたい放題である
  • node.js と同じく、デフォルトエクスポートは、1つのモジュールにつき1つで、デフォルトエクスポートされたものは、インポート先で自由に名前をつけることができる
export default function hoge() {
  // hoge
}
-------------------------------------------------------------------
import Hoge from './src/hoge'
// src/hoge.js
export const hogeArray = [1, 2, 3, 4, 5]
export const HogeObject = {
  id: 1,
}
export const HOGE_CONST = 100
-------------------------------------------------------------------
// すべてインポートしたい場合
import * as Hoges from './src/hoge'
// 必要なもののみ
import { HogeObject } from 'src/hoge`
// 別名を付ける場合
import { HogeObject as Hoge } from 'src/hoge`
// キャッシュバスター
import * as Hoges from './src/hoge.js?2023'
// ブラウザで利用できるJavaScriptモジュールは、常に動的にモジュールを読み込む
// つまり、モジュールが必要なときだけ読み込むようにするので、パフォーマンス上の利点がある
// importを関数として実行してパラメータとしてパスを指定することができ、次のようにPromiseを返す

import('src/hoge')
    .then(module => {
        console.log(HogeObject.id)
    })
~

参考:https://qiita.com/azukiazusa/items/98845e79807fff0dd3cb

また import 文には URL も指定ができる。

// どのJSライブラリでもES Modulesとして読み込めるわけではない
// jQuery や React などは ES Modules として配布されていない
import * as THREE from 'https://cdn.skypack.dev/three';

モジュールコードは import 時の初回のみ評価されるため、以下のようなコードの場合は、一度しか発火しない。

// alert.js
alert( 'hoge' );

// hoge.js
import './alert.js'; // このときだけ alert( 'hoge' ); が発火する
import './alert.js'; // ここでは発火しない

// human.js
export let human = { name: 'hoge' };

// hoge.js
import { human } from './human.js';
human.name = 'foo';
import { human } from './human.js';
console.log( human.name ); // foo

Import maps について

Import maps とは、インポート先のライブラリをエイリアス登録することができる機能のこと。

<script type="importmap">
{
  "imports": {
    "lodash": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.min.js",
    "@" : "./sample-alert.js"
  }
}
</script>

<script type="module">
  import * as _ from 'lodash';
  import {sayMessage} from '@';

  const a = {'a': 1};
  const b = {'a': 3, 'b': 2};
  const c = _.defaults(a, b);

  sayMessage(JSON.stringify(c));// {a: 1, b: 2}
</script>

引用元:https://ics.media/entry/16511/

import.meta について

現在のモジュールの関する情報を確認するためのオブジェクトである。
import / export もそうだが、モジュールファイル内以外では未定義になるため注意。

<script type="module">
  alert(import.meta.url);
  // script url (インラインスクリプトに対する HTML ページの url)
</script>

参考:https://ja.javascript.info/modules-intro

ブラウザ固有の特徴

ベア(むき出しの)モジュールは許可されない

Node.js 環境では以下のように記述できるが、

import Hoge from 'Hoge'; // ブラウザではエラーが発生

ブラウザではかならず、相対 URL か絶対 URL である必要がある。

import Hoge from './Hoge.js';

Node.js やバンドルツールのような特定の環境では、モジュールを見つけるための独自の方法や、それらを調整するためのフックがあるため、剥き出しのモジュールを使用することができます。しかしブラウザではまだベアモジュールはサポートされていません。

https://ja.javascript.info/modules-intro

遅延読み込み

モジュールスクリプトは外部スクリプトとインラインスクリプト両方で、常に遅延され、defer 属性(参考:ページのライフサイクル)と同じ効果を持つ。
そのため、以下のような特徴を持つ。

  • HTMLの読み込みをブロックしない
  • HTMLの読み込みが完了してから発火
  • 相対的な順序は維持され、ドキュメントの最初にあるスクリプトが最初に実行される

参考:https://ja.javascript.info/modules-intro

async 属性について

モジュールスクリプトでない場合は、async 属性は外部スクリプトでのみ動作し、async スクリプトは、他のスクリプトや HTML ドキュメントとは関係なく、準備ができ次第すぐに実行される。
参考:https://isaxxx.com/archives/5356/

モジュールスクリプトの場合(インラインスクリプトでも動作する)も同様に動作し、準備ができたときに実行される。
そのため、カウンターや広告、解析スクリプトなどでは async 属性を付与するとよい。

外部スクリプトについて

参考:https://ja.javascript.info/modules-intro

同じ src 属性値の場合は、一度のみ実行

<!-- hoge.js は一度だけ取得され実行される -->
<script type="module" src="hoge.js"></script>
<script type="module" src="hoge.js"></script>

別ドメインの外部スクリプトはCORS ヘッダが必要

<!-- hoge.com は「Access-Control-Allow-Origin: *」ヘッダーを設定しなければならない -->
<script type="module" src="https://hoge.com/hoge.js"></script>

nomodule 属性について

参考:https://ja.javascript.info/modules-intro

JavaScript モジュールをサポートしないブラウザの場合、type=module を付与するとそのスクリプトは無視される。
その場合は、nomodule 属性を使ってフォールバックすることができる。

<script type="module">
  alert("Runs in modern browsers");
</script>

<script nomodule>
  alert("現在のブラウザは type=module と nomodule どちらも知っているので、これはスキップされます")
  alert("古いブラウザは未知の type=module を持つスクリプトは無視しますが、これは実行します");
</script>

JavaScript モジュール以前に使用されていたモジュールシステム

JavaScript モジュールは前述のように言語レベルのモジュールシステムとして 2015 年に ES2015 で策定され、現在ではすべての主要なブラウザとNode.js でサポートされた。
その前には、以下のような技術が使用され、いまでも古いスクリプトで使用されている。

  • AMD(Asynchronous module definition)
    最も古いモジュールシステムの1つで、最初はライブラリrequire.jsで実装
  • CommonJS
    Node.js サーバ用に作られたモジュールシステム
  • UMD(Universal Module Definition)
    もう1つのモジュールシステムで、ユニバーサルなものとして提案され、AMD と CommonJS と互換性がある