ES Modules入門
JavaScriptでモジュールを使う時代

234
132
99

JavaScriptには、モジュールという仕組みがありますECMAScript 2015のModulesの標準仕様として策定されており、現在はすべてのブラウザで利用できます。この機能は、ES2015 Modules、ECMAScript Modules、ES Modules、ESMなどと呼ばれています(以下、ES Modulesと記載します)。

webpackViteなどのフロントエンドのツールを通して、すでに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ファイル単独で管理できない
  • 読み込みの順番を気にしなければならない

これを解決するための手段として、さまざまなアプローチが登場しました。有名なところだと、AMDCommonJSなどの技術があります。これらは独自仕様だったため、これからは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を指定しないと、importexportなどの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.skypack.dev/three';
    
    // 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.2.27/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.skypack.dev/three"
  }
}</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などのツールが必要で、JSXTypeScriptなどのコンパイラも必須なのは変わらないはずです。高度なSPA開発では引き続きこの手のツールは使われるはずです。

ES Modulesが現状のフロントエンドの作り方を破壊するのではなく、組み合わせることでさまざまな課題にアプローチできるようになると考える方が望ましいでしょう。

まとめ

ES ModulesはこれまではJavaScriptの開発スタイルに変化をもたらす大きな機能です。すでにモジュールバンドラーの利用者はES Modulesに親しんできたかもしれませんが、これからはブラウザネイティブで利用できる時代となっていくでしょう。

今回は、ES Modulesの概要を紹介しただけでしたが、続編記事ではES Modulesのさまざまな記述方法・使い方を解説します。この記事で解説したサンプルはGitHubにて公開しています。

※この記事が公開されたのは6年前ですが、1年前の2023年3月に内容をメンテナンスしています。

池田 泰延

ICS代表。筑波大学 非常勤講師。ICS MEDIA編集長。個人実験サイト「ClockMaker Labs」のようなビジュアルプログラミングとUIデザインが得意分野です。

この担当の記事一覧