ES modules基礎知識 第1回 仕様の概要とその周辺課題

ES2015仕様において策定されたES modulesは、JSファイルから別のJSファイルを読み込む仕組みです。まずはこの仕様がどのようなものなのかを知り、さらにその背景と課題について考えてみます。

発行

著者 山田 順久 フロントエンド・エンジニア
ES modules基礎知識 シリーズの記事一覧

ES modulesとは

ES modulesは、ES2015仕様において策定された、JavaScriptファイルから別のJavaScriptファイルを読み込む仕組みです。

Node.jsでは、ほかのJSファイルの読み込みはCommonJSの仕様に沿った方法ですでに実現していましたが、ES modulesは標準としてNode.jsとブラウザ両方に対応したモジュールシステムの仕様と位置づけられます。

これまでそのようなものがなかったブラウザと、別の方法でやってきてしまったNode.jsの間で互換性をとるため、両者における従来の作業工程にさまざまな変化がありますが、第1回ではブラウザ側の視点を中心にES modulesを取り上げていきます。

実装状況

執筆時のブラウザへの実装状況(2017年10月)は次のとおりです。

  • Safari 10.1: 実装済み
  • Chrome 61: 実装済み
  • Firefox 56: 試験運用版として実装済み
  • Edge 15: 試験運用版として実装済み

ES modulesの簡単な例

ブラウザの世界では、JavsScriptファイルを読み込むためにはHTMLファイルの中に<script>タグを記述する必要がありました。ES modules仕様が実装された環境では、JavaScriptファイルの中にimport文という新しい構文を記述して、ほかのJavaScriptファイルの内容を読み込むことができます。読み込まれる側では、これも新しい構文であるexport文を使ってどの値を公開するのかを指定します。

たとえば、ブラウザ上にテキストを表示する関数を書いて、これをexport文で外部へ公開してみます。コード中にあるdefaultの意味などは次回以降に解説していくので、今は雰囲気だけ理解できれば大丈夫です。

export文を使って関数を公開する

// render-text-to-body.js

// 公開したい関数を定義
const renderTextToBody = (text) => {
  const div = document.createElement('div');
  const p = document.createElement('p');
  const textNode = document.createTextNode(text);
  p.appendChild(textNode);
  div.appendChild(p);
  document.body.appendChild(div);
};

// 定義した関数を公開
export default renderTextToBody;

公開された値を読み込む側では、次のようにimport文を記述します。

import文を使って公開されている関数を読み込む

// main.js

// 公開された関数を読み込み
import renderTextToBody from './render-text-to-body.js';

// 読み込んだ関数を呼び出す
renderTextToBody('お使いのブラウザはES modulesに対応しています。');

これをHTMLから読み込ませる場合には、次のように<script>タグに対してtype="module"の属性を指定しながら読み込みます。これは、ただのJavaScriptとES modules対応のJavaScriptを区別するために必要となっています。

ES modulesを利用したJSファイルを読み込む

<script type="module" src="./main.js"></script>

実際に動くように配置したものが次のデモになります。

ES modulesに対応したブラウザであれば、export文とimport文が解釈された上でコードが正しく動作して、次のような表示となります。

ES modules対応済みのChrome 61での表示結果。

ES modules非対応ブラウザへの対応

このデモにはES modulesに対応していないブラウザで表示した場合のための動作も用意してあり、その場合には次の表示を返します。

ES modules未対応の通常起動したFirefox 56での表示結果。

これはHTMLからES modules非対応ブラウザ向けのJavaScriptが読み込まれて実行されているためです。その挙動を実現するために、<script>タグにnomodule属性を加えて別のJSファイルを読み込み、表示を切り替えています。

ES modulesに対応していないブラウザ用のJSファイルを読み込む

<script nomodule src="./nosupport.js"></script>

このようにして、ES modules対応ブラウザと非対応ブラウザ向けのJSファイルを切り替えられるようになっています。

また、type="module"nomodule属性のどちらともインラインで直接コードを書いても動作します。

次のコードはtype="module"を宣言した<script>タグの中にインラインでimport文を使ったコードを書いた例です。

type="module"指定のscriptタグ内にインラインでコードを書く例

<script type="module">
  import renderTextToBody from './render-text-to-body.js';
  renderTextToBody('お使いのブラウザはES modulesに対応しています。');
</script>

ここまでのまとめ

  • ES modulesが実装されていれば、JSファイルからほかのJSファイルを読み込める
  • ES modulesを使ったJavaScriptを書く・読み込む場合は<script>type="module"を指定する
  • ES modules非対応環境向けのJavaScriptを書く・読み込む場合は<script>nomodule属性を指定する

バンドルツールはいらなくなるのか

ブラウザでES modulesが使えるようになると、分割したモジュールの依存解決をするためにこれまで使ってきた、Browserifywebpackのようなツールが必要なくなるという考えも当然出てくるでしょう。

バンドルツールがいらなくなれば、webpackなどの設定に悩むことがなくなり、ファイルを更新したあとのビルド時間もなくなるので、そういった点で便利になるということが考えられます。

ただ、ES modulesかバンドルツールかのどちらかという対比では、バンドルツールの担っている仕事の幅が広く、今すぐそれを手放せるものでもありません。さまざまな課題はあると思いますが、Webアプリケーションのクライアントサイドの実装をすることが多い立場としては、バンドルツールが必要な理由として次の点が挙げられます。

  • サードパーティのnpmモジュールの依存解決
  • 上記の依存解決がクリアできた場合のリクエスト数

npmパッケージの依存関係とリクエスト数

webpackなどのツールは、npmからインストールしてnode_modules/配下に置かれているライブラリもうまく取り込んでビルドしてくれます。しかし、ES modulesの場合にはそうしたnpm由来のモジュールを依存関係も含めて、うまくサーバー上に配置しなければなりません。

たとえばnpmからインストールしたLodashライブラリのモジュール群がnode_modules/lodash/*.jsに置かれていたとして、Webサイトで必要とするLodashモジュールの依存関係を含めてpublic/js/vendor/lodash/*.jsのようなパスで提供できるような配置をどこかで行う必要はあるのではないかと思います。

そうした依存関係をサーバーから提供できたとしても、今度はたくさんの依存ファイルのリクエストに遭遇する可能性があります。

ここでまた別のデモを紹介します。LodashのES modules版から1つの関数をブラウザ上でimportするものです。

コードは次のとおりで、ここでは例としてlodash.find()をimportしてみます。

ES modules版のlodash.find()をimportする

import find from './lodash/find.js';

const users = [
  { name: 'Alice' },
  { name: 'Bob' }
];

const alice = find(users, u => u.name === 'Alice');
console.log(alice);

次のリンクからブラウザで動くデモとそのコードを見ることができます。画面表示は何もしていないので、開発者ツールのネットワーク欄を見るなどして、importの動作を確かめてみてください。

筆者の手元で確認したものが次のキャプチャです。JSファイルへのリクエスト数は122件で、その総ファイルサイズは64.5kb、読み込み完了までは1.32秒を示しています。

Chrome DevtoolsのNetworkタブでlodash.find()が依存するモジュールのすべてがリクエストされている様子。 ライブラリ1つ読み込むだけで膨大な依存関係を辿ることもある。

デモの環境ではHTTP/2でリソースが配信されているため、1つのコネクションで複数リクエストを扱えています。そのおかげで幾分悪くない速度にはなっていますが、1リクエストに対して1コネクションとなるHTTP/1.1の場合には問題になります。本番の製品開発ではこうした依存解決のリクエストだけで数百、数千の数に上ることが想像できます。

ES modules対応を進める場合には、HTTP/2への対応も前提として考えるべきでしょう。

今なにを使えばいいのか

先述した懸念もあり、今のところでは、Webサイト・WebアプリケーションのためのJSファイルの提供方法としては、webpackのようなツールでビルドしたJSファイルを提供するのが安全な方法だと考えています。

ライブラリのような規模であれば、ES modules対応を試みてもよさそうです。長期的に見れば、ES modules対応済みのコードはそのままNode.jsでもブラウザでも動くことが期待できるので将来的な移行が楽になります。ライブラリの対象環境にもよりますが、現状では、Node.jsとブラウザ間の互換性に気を配りつつ、CommonJS版に変換したビルドも別途提供を続けていくことになりそうです。

まとめ

ES modules仕様に関する話題は、具体的にブラウザやNode.jsそれぞれの環境下でどのように動くかを決めていく必要があったため実装に時間がかかっていました。特にNode.jsには既存のCommonJS方式の仕組みがあり、その仕組みとの互換性を取っていく必要もあります。

それから、ブラウザ上で動かす場合には依存しているモジュールをサーバーからどのように配信していくのかといった、周辺の課題も点在しています。今回はブラウザでの動作例とともに、そうした課題の一例を紹介しました。

次回はES modules自体の具体的な仕様や構文について解説します。