ブラウザーで ECMAScript Modules を使うことを調べました 1(Asynchronous Module Definition)

前回の記事では、 Node.js で ECMAScript Modules を使ってみました。 今回は、ブラウザーでも ECMAScript Modules を使ってみようと思いました。

結果的には、 ECMAScript Modules のままでは動かせていなくて、 Babel を使ってトランスパイルしてからブラウザーで動かしています。

調べたことを書いておきます。

環境

  • babel-cli 6.26.0
  • babel-preset-env 1.6.1
  • babel-plugin-transform-es2015-modules-amd 6.24.1
  • RequireJS 2.3.5
  • Microsoft Edge 16.16299
  • Firefox 59.0.2

ECMAScript Modules の export と import

最初に、 JavaScript のリファレンスとして参考にしている MDN Web Docs の exportimport を見てみました。 このサイトは、ブラウザーの互換性も記載されているので、ありがたいです。

export

This feature is only implemented natively in Safari, Chrome, and Edge at this time. It is implemented in many transpilers, such as the Traceur Compiler, Babel, and Rollup.

export - JavaScript | MDN


import

This feature is only implemented natively in Safari, Chrome, and Edge at this time. It is implemented in many transpilers, such as the Traceur Compiler, Babel, and Rollup.

import - JavaScript | MDN

Safari, Chrome, Edge は実装されているようです。

export のページに記載されている Examples を Edge で動かしてみます。 すると、次のエラーがコンソールに出力されました。

SCRIPT1086: SCRIPT1086: Module import or export statement unexpected here

Firefox でも動かしてみます。 すると、次のエラーがコンソールに出力されました。

SyntaxError: import declarations may only appear at top level of a module

解釈に誤りがあったのか、実装が悪いのか、動かし方が悪いのか、エラーになってしまいました。

This feature is only implemented natively

の natively って、そのままの ECMAScript Modules を動かせる、って解釈じゃいけないのかな。

exportimport のページには、次のようなトランスパイラーで実装されていると記載されていました。

  • Traceur Compiler
  • Babel
  • Rollup

ので、トランスパイルすることにしました。

Babel

Babel は使ったことがあったので、 Babel を使うことにしました。

Plugins · Babel の Modules には、次の 4 つが記載されていて、どれを選択すればいいのか、判断がつかなかったため、一つずつ使ってみることにしました。

  • es2015-modules-amd
  • es2015-modules-commonjs
  • es2015-modules-systemjs
  • es2015-modules-umd

es2015-modules-amd

This plugin transforms ES2015 modules to AMD.

Asynchronous Module Definition (AMD)

ES2015 modules to AMD transform · Babel

これは、 Asynchronous Module Definition(AMD) という仕様に合わせてトランスパイルするための、 Babel のプラグインのようです。

Asynchronous Module Definition(AMD) と RequireJS

This page talks about the design forces and use of the Asynchronous Module Definition (AMD) API for JavaScript modules, the module API supported by RequireJS. There is a different page that talks about general approach to modules on the web.

Why AMD?

AMD という仕様を、 RequireJS で実装しているようです。 AMD という単語を見て、まったく何のことか分からなかったのですが、 RequireJS は聞いたことがあります。

Examples

ECMAScript Modules の仕様のコードを、 Babel を使って AMD の仕様のコードにトランスパイルして、 RequireJS を使ってブラウザーで実行する流れで進めてみます。

次の引用の ECMAScript Modules のコードを使ってみます。

Examples

Using named exports

In the module, we could use the following code:

// module "my-module.js"
function cube(x) {
  return x * x * x;
}
const foo = Math.PI + Math.SQRT2;
var graph = {
    options:{
        color:'white',
        thickness:'2px'
    },
    draw: function(){
        console.log('From graph draw function');
    }
}
export { cube, foo, graph };

This way, in another script, we could have:

import { cube, foo, graph } from 'my-module';
graph.options = {
    color:'blue',
    thickness:'3px'
}; 
graph.draw();
console.log(cube(3)); // 27
console.log(foo);    // 4.555806215962888

export - JavaScript | MDN

Babel のインストール

パッケージをインストールしました。

$ npm init -y
$ npm install --save-dev babel-cli babel-preset-env babel-plugin-transform-es2015-modules-amd

Babel の設定ファイル

Usage

Via .babelrc (Recommended)

.babelrc

{
  "plugins": ["transform-es2015-modules-amd"]
}

ES2015 modules to AMD transform · Babel

Babel の設定ファイル (.babelrc) を次のように作成しました。

{
  "presets": ["env"],
  "plugins": ["transform-es2015-modules-amd"]
}

トランスパイル

トランスパイルします。 ECMAScript Modules のコードを src/ ディレクトリーに保存しておいて、 AMD のコードを lib/ ディレクトリーに出力します。

$ ./node_modules/.bin/babel src/ -d lib/
src/main.js -> lib/main.js
src/my-module.js -> lib/my-module.js

my-module.js

トランスパイルされたファイルです。 最初に、 my-module.js です。

define(['exports'], function (exports) {
  'use strict';

  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  function cube(x) {
    return x * x * x;
  }
  var foo = Math.PI + Math.SQRT2;
  var graph = {
    options: {
      color: 'white',
      thickness: '2px'
    },
    draw: function draw() {
      console.log('From graph draw function');
    }
  };
  exports.cube = cube;
  exports.foo = foo;
  exports.graph = graph;
});

define 関数が使われています。

exports.cube = cube; のように、 exports のプロパティに設定していました。 ここだけ見ると、 exports は Node.js の Modules と似た印象を持ちました。

main.js

次に、 main.js です。

define(['./my-module.js'], function (_myModule) {
  'use strict';

  _myModule.graph.options = {
    color: 'blue',
    thickness: '3px'
  };

  _myModule.graph.draw();
  console.log((0, _myModule.cube)(3)); // 27
  console.log(_myModule.foo); // 4.555806215962888
});

こちらも define 関数が使われています。

ECMAScript Modules のコードでは、 cube, foo, graph の 3 つの変数に import していましたが、コールバック関数の引数で受け取るのは、 _myModule の 1 つだけのようでした。 _myModule.graph のように使っていました。

import した関数を使うところが独特です。 (0, _myModule.cube)(3) って、単純に、 _myModule.cube(3) じゃいけないのかな。

project.html

How to get started with RequireJS を参考に、 main.js を呼び出す HTML ファイルを、次のように作成しました。

<!-- project.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>My Sample Project</title>
    <!-- data-main attribute tells require.js to load
      scripts/main.js after require.js loads. -->
    <script data-main="main" src="//requirejs.org/docs/release/2.3.5/minified/require.js"></script>
  </head>
  <body>
    <h1>My Sample Project</h1>
  </body>
</html>

data-main にエントリーポイントとなるファイルを記述するようです。

このファイルをトランスパイルしたディレクトリー (lib/) に保存しました。

実行

ブラウザーから project.html を開きます。

すると、コンソールに次のように出力されました。

From graph draw function
27
4.555806215962888

モジュールが読み込めているようです。

終わり

AMD(RequireJS) は、ブラウザー上でモジュールの依存関係を解決するもののようでした。 2018 年の現在は、 webpack とかを使って、事前にモジュールの依存関係を解決したファイルを読み込ませる方が主流の印象を受けます。 その方がブラウザーから何度もリクエストを送る必要がなくて効率的なのかな。

今回は、これで終わりにします。

4 April 2018 追記

次の記事を書きました。