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

前回の記事では、 SystemJS を使ってみました。 今回は、 Universal Module Definition を使ってみます。

環境

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

es2015-modules-umd

This plugin transforms ES2015 modules to UMD.

Universal Module Definition (UMD)

ES2015 modules to UMD transform · Babel

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

Universal Module Definition (UMD)

UMD (Universal Module Definition) patterns for JavaScript modules that work everywhere.

UMD (Universal Module Definition)

This repository formalizes the design and implementation of the Universal Module Definition (UMD) API for JavaScript modules. These are modules which are capable of working everywhere, be it in the client, on the server or elsewhere.

The UMD pattern typically attempts to offer compatibility with the most popular script loaders of the day (e.g RequireJS amongst others). In many cases it uses AMD as a base, with special-casing added to handle CommonJS compatibility.

GitHub - umdjs/umd: UMD (Universal Module Definition) patterns for JavaScript modules that work everywhere.

仕様というより、パターン、のように受け取りました。

クライアントでも、サーバーでも動かせるモジュールを定義するためのパターンのようです。

モジュールを定義する仕様として、 Asynchronous Module Definition(AMD) がベースにあって、 CommonJS Modules も互換性のために使うようです。

Examples

前回の記事と同様に、 MDN Web Docs の export のページの Examples を使って進めてみます。

Babel のインストール

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

$ npm install --save-dev babel-cli babel-preset-env babel-plugin-transform-es2015-modules-umd

Babel の設定ファイル

Usage

Via .babelrc (Recommended)

.babelrc

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

ES2015 modules to UMD transform · Babel

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

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

トランスパイル

トランスパイルします。 ECMAScript Modules のコードを src/ ディレクトリーに保存しておいて、 UMD のコードを 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 です。

(function (global, factory) {
  if (typeof define === "function" && define.amd) {
    define(['exports'], factory);
  } else if (typeof exports !== "undefined") {
    factory(exports);
  } else {
    var mod = {
      exports: {}
    };
    factory(mod.exports);
    global.myModule = mod.exports;
  }
})(this, 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;
});

3 行目の define(['exports'], factory); が AMD で、 5 行目の factory(exports); が CommonJS Modules かな。

全体が即時関数のパターンになっていて、 1 行目の 2 つ目の引数 factory は、 13 行目から始まる関数 function (exports) { が渡されているようです。

main.js

次に、 main.js です。

(function (global, factory) {
  if (typeof define === "function" && define.amd) {
    define(['./my-module.js'], factory);
  } else if (typeof exports !== "undefined") {
    factory(require('./my-module.js'));
  } else {
    var mod = {
      exports: {}
    };
    factory(global.myModule);
    global.main = mod.exports;
  }
})(this, 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
});

3 行目の define(['./my-module.js'], factory); や、 5 行目の factory(require('./my-module.js')); で、 my-module.js を読み込んでいるようです。

AMD

最初に、 AMD として読み込んでみます。

ブラウザーで ECMAScript Modules を使うことを調べました 1(Asynchronous Module Definition) のように、 RequireJS を使います。

amd.html

次のような HTML ファイルを作成しました。

<!-- amd.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <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>

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

実行

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

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

From graph draw function
27
4.555806215962888

AMD のモジュールとして、ブラウザーから動的に読み込めたようです。

CommonJS Modules

次に、 CommonJS Modules として読み込んでみます。

ブラウザーで ECMAScript Modules を使うことを調べてみました 2(CommonJS Modules) のように、 Browserify を使って事前にバンドルします。

Browserify って、 AMD の define 使わないよね? と思って調べてみました。

CommonJS

Browserify works natively with the CommonJS module definition:

…略…

AMD

Browserify can consume AMD modules with deamdify:

dontkry.com

CommonJS Modules はそのままの Browserify で動かせるようです。 deamdify というパッケージと合わせて使うことで、 Browserify でも AMD を動かせるようです。 なので、今回は、そのままの Browserify を使うことで、 CommonJS Modules として読み込めていることを確かめられそうです。

Browserify のインストール

Browserify をインストールします。

$ npm install --save-dev browserify

バンドル

バンドルします。

$ ./node_modules/.bin/browserify lib/main.js -o lib/bundle.js

commonjs.html

次のような HTML ファイルを作成しました。

<!-- commonjs.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>CommonJS</title>
  </head>
  <body>
    <h1>CommonJS</h1>
    <script src="bundle.js"></script>
  </body>
</html>

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

実行

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

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

From graph draw function
27
4.555806215962888

CommonJS Modules として読み込めたようです。

終わり

UMD は、クライアントでも、サーバーでも動かせるモジュールの定義のようですが、ここまで共通にする必要のあるライブラリーみたいなものを作ったことがないので、あまりメリットを感じられませんでした。 でも、 Babel が勝手にやってくれるなら、 AMD, CommonJS Modules のどちらか一方にするよりも、 UMD にした方がよさそうに感じました。


これで、 Babel のプラグインの 4 つが確認できました。

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

結局、どれがいいのだろう? 個人的には、 Node.js Modules で慣れていたので、 CommonJS Modules を使いたいと思いました。 まだ、 ECMAScript Modules を使う気にはなれませんでした。

SystemJS は AMD も CommonJS Modules も読み込めるので、ローダーとして使ってもよさそうに思いました。 が、 SystemJS は使わずに、 webpack でバンドルして動かした方がいいのかな。


トレンドです。

AMD や UMD は仕様やパターンなので、すごく低いです。 AMD の実装の RequireJS はすごく高いし、判断するのに困ります。 どうせならすぐに消えてしまわないものを採用したいです。

参考

次の記事に、 AMD, CommonJS, UMD あたりの経緯が記載されていて、勉強になりました。