JavaScriptには、モジュールという仕組みがあります。ECMAScript 2015のModulesの標準仕様として策定されており、現在はすべてのブラウザで利用できます。この機能は、ES2015 Modules、ECMAScript Modules、ES Modules、ESMなどと呼ばれています(以下、ES Modulesと記載します)。
webpackやViteなどのフロントエンドのツールを通して、すでにES Modulesを使っているエンジニアも多いと思います。この記事では、ブラウザネイティブで使えるES Modulesに焦点をあて、ES Modulesの導入で解決できる課題と利点を紹介します。
HTML+JSではモジュールの仕組みがなかった
JavaScript自体には他のJSファイルを取り込む標準的な仕様が昔は存在しませんでした。外部JSファイルを読み込みたい時に、HTMLファイルにscript
タグを書き込むことで別ファイルを読み込んでいました。たとえば次のようなコードです。
<script src="js/vender/jquery.min.js"></script>
<script src="js/vender/jquery.cookie.js"></script>
<script src="js/vender/jquery.easing.1.3.js"></script>
<script src="js/common.js"></script>
<script src="js/utils.js"></script>
<script src="js/app.js"></script>
この方法だと扱いづらい点があります。
- HTMLファイルと密結合となる
- JSファイル単独で管理できない
- 読み込みの順番を気にしなければならない
これを解決するための手段として、さまざまなアプローチが登場しました。有名なところだと、AMDやCommonJSなどの技術があります。これらは独自仕様だったため、これからはES Modulesが標準仕様として取って代わっていくと考えられます。
サンプルで理解するES Modules
ES Modulesをブラウザで使うにはいくつか手順があります。サンプルを通して、1つひとつおさえていきましょう。
利用可能なブラウザ
現在のブラウザではES Modulesを利用できます。「Can I Use…」によると、利用可能なブラウザのバージョンは以下の通りです。
- iOS Safari 10.1以上(2017年3月リリース)
- macOS Safari 10.3以上(2017年3月リリース)
- Chrome 61以上(2017年8月リリース)
- Edge 16以上(2017年10月リリース)
- Firefox 60以上(2018年5月リリース)
読み込まれる側の処理
モジュールとしての外部JSファイルを用意しましょう。次のコードはブラウザでアラートを表示するだけのサンプルコードです。
▼sample-alert.js
export function sayMessage(message) {
alert(message);
}
外部ファイルのJSファイルではexport
文を使って定義します。export
で宣言されたものだけが他のJSファイルから参照できます。
読み込む側の処理
HTMLには次のコードを記述します。従来はtype="text/javascript"
と記載していましたが、ES Modulesを使う時はtype="module"
と書きます。module
を指定しないと、import
やexport
などのJSコード内の宣言がエラーとなります。
▼パターン1
<html>
<head>
<meta charset="UTF-8" />
<script type="module">
import {sayMessage} from "./sample-alert.js";
sayMessage("こんにちは世界");
</script>
</head>
<body>
</body>
</html>
src
属性で外部ファイルを指定できます。インラインでも外部ファイルでも、どちらでもES Modulesを読み込めます。
▼パターン2
<html>
<head>
<meta charset="UTF-8" />
<script type="module" src="index.js"></script>
</head>
<body>
</body>
</html>
import { sayMessage } from "./sample-alert.js";
sayMessage("こんにちは世界");
import
文のfrom
の中では必ず./
や../
、/
、といったパスで記述しなければなりません。xxx.js
というように記述するのはNGで、./xxx.js
とファイルパスを明確に記載します。拡張子も必須です。Node.jsでは拡張子無しの記述ができましたが、ブラウザでは必ず拡張子を記載ください。
これをブラウザで開くとJSファイルで記載されたコードが実行されていることがわかります。
外部JSも扱える
もう1つ興味深いサンプルを紹介しましょう。import
文にはURLも指定できます。ES Modulesに対応した外部JSであれば、CDNから読み込めるので次のように記載できます。
<html>
<head>
<meta charset="UTF-8" />
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.164.1/build/three.module.js';
// Three.jsの起動コード (略)
</script>
</head>
<body>
</body>
</html>
次の作例はWebGL用のJSライブラリThree.jsを使ったサンプルです。
注意点として、どのJSライブラリでもES Modulesとして読み込めるわけではありません。Three.jsはES Modulesで設計された数少ないJSライブラリです。有名どころだとjQueryやReactなどはES Modulesとして配布されていないため、現時点ではブラウザネイティブでES Modulesとして利用できません。
Import mapsでESMのエイリアスをはる
ブラウザでもライブラリ名を指定したインポートを利用する方法があります。たとえば、from "three"
やfrom "vue"
といった記載が可能になります。
script
タグにtype=importmap
属性を付与することで、インポート先のライブラリをエイリアス登録ができます。<script type="importmap">
のタグの中には、JSON形式でエイリアスと実態のURLを指定します。
<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>
CDNのJSライブラリでも利用できますし、相対パスに対するエイリアスもはれます。
Vue.js
Vue.jsの場合は以下のように記述します。
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3.4.25/dist/vue.esm-browser.prod.js"
}
}
</script>
<script type="module">
import {
ref,
computed,
createApp,
} from "vue";
const app = createApp({
setup() {
// ・・・任意の処理
},
});
app.mount("#app");
</script>
Three.js
Three.jsの場合は以下のように記述します。
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.164.1/build/three.module.js"
}
}</script>
<script type="module">
import * as THREE from "three";
// レンダラーを作成
const renderer = new THREE.WebGLRenderer();
// ・・・省略
</script>
対応ブラウザ
Import mapsの対応ブラウザは『Import maps | Can I use...』を参照ください。現行ブラウザはすべて対応しています。
ES Modulesを導入した場合の課題
ES Modulesを採用し、JSファイルを細かく分けた場合、ウェブサーバーから転送すべきファイルの数が増える傾向があります。
HTTP/1.1プロトコルでは同時接続数が限られるため多くのファイルを転送するのが苦手です。ウェブ制作の現場ではCSSスプライトやJSファイルの結合などの手法でファイルを1つに結合し、転送ファイルの数を減らしました。
転送ファイルが増えることの解決には、HTTP/2に対応したウェブサーバーへ移行するのが適した手段の1つです。ES ModulesでJSファイルの数が増えても、HTTP/2プロトコルでは支障にならないでしょう。
それどころか、HTTP/2環境下ではウェブサイトの読み込みでメリットがあります。ウェブサイト内の共通JSやベンダーJSはブラウザのキャッシュに任せて(キャッシュヒット率を高めて)、ページ固有のJSファイルだけ転送する形としたとします。そうすると、ページごとに必要な差分JSしか読み込みが発生しなくなるはずです。
キャッシュ対策が難しくなる
ブラウザキャッシュの対策として、リクエストURLに以下のようにパラメーターを付与する運用があります。パラメーターを日付等を指定することになり、URLがユニークとなり、キャッシュを避けられ、新しいファイルを配信できます。
<script src="main.js?2022_09_15"></script>
ES Modulesの場合は、ファイル参照とコードのimport
文が同一なため、この手法を利用する場合はimport
文を書き換えます。ただ、JavaScript側を修正することになるので、あまり賢い方法とは言えません。
import sayMessage from './main.js?2022_09_15';
sayMessage('こんにちは世界');
モジュールバンドラーとの比較
規模のあるJavaScriptの開発ではモジュールの仕組みは必須です。昨今のフロントエンドの開発ではwebpackやViteなどのツールを使ってモジュールのJS開発をしている方がほとんどでしょう。
これらのツールは現行ブラウザでもモジュールの仕組みを利用できるように、ES Modulesを使ったコードをES5互換のJSファイルへと変換します。
ES Modulesがブラウザ標準で使える現在、このワークフローは変わっていくのでしょうか。
モジュールバンドラーは不要になる?
都内の勉強会に参加すると、「ES Modulesが普及すると、バンドルツールは不要となる」といった意見をよく聞きます。その時代にはきっとES2015の全機能はブラウザ標準で使えるようになっているでしょう。わざわざNode.jsでBrowserifyやBabelなどの環境構築をする必要がなくなるかもしれません。ネイティブにそのままブラウザで確認できる利便性は大きな利点です。
モジュールバンドラーが必要な場面はある
ただし、パッケージマネージャーnpmで管理するJSライブラリをバンドルするにはwebpackなどのツールが必要で、JSXやTypeScriptなどのコンパイラも必須なのは変わらないはずです。高度なSPA開発では引き続きこの手のツールは使われるはずです。
ES Modulesが現状のフロントエンドの作り方を破壊するのではなく、組み合わせることでさまざまな課題にアプローチできるようになると考える方が望ましいでしょう。
まとめ
ES ModulesはこれまではJavaScriptの開発スタイルに変化をもたらす大きな機能です。すでにモジュールバンドラーの利用者はES Modulesに親しんできたかもしれませんが、これからはブラウザネイティブで利用できる時代となっていくでしょう。
今回は、ES Modulesの概要を紹介しただけでしたが、続編記事ではES Modulesのさまざまな記述方法・使い方を解説します。この記事で解説したサンプルはGitHubにて公開しています。
※この記事が公開されたのは7年前ですが、半年前の4月に内容をメンテナンスしています。