ブラウザーで ECMAScript Modules を使うことを調べました 3(SystemJS)

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

環境

  • babel-cli 6.26.0
  • babel-preset-env 1.6.1
  • babel-plugin-transform-es2015-modules-systemjs 6.24.1
  • SystemJS 0.21.1
  • Microsoft Edge 16.16299
  • Firefox 59.0.2

es2015-modules-systemjs

This plugin transforms ES2015 modules to SystemJS.

SystemJS

ES2015 modules to SystemJS transform · Babel

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

SystemJS

Dynamic ES module loader

SystemJS

Configurable module loader enabling dynamic ES module workflows in browsers and NodeJS.

GitHub - systemjs/systemjs: Dynamic ES module loader

module loader と記載されています。 モジュールのローダーのようです。 モジュールを定義するための仕様、という意味合いは弱い印象を持ちました。

じゃあ、どんな定義のモジュールを読み込めるのか? module formats のページを見てみました。

Module Formats

The following module formats are supported:

  • esm: ECMAScript Module
  • cjs: CommonJS
  • amd: Asynchronous Module Definition
  • global: Global shim module format
  • system: System.register or System.registerDynamic compatibility module format *(Need more? Check the plugins.)

systemjs/module-formats.md at master · systemjs/systemjs · GitHub

読み込むモジュールの定義は ECMAScript Module でも CommonJS でも Asynchronous Module Definition でもいいみたいです。

To load ES6 code with in-browser transpilation, one of the following transpiler plugins must be configured:

  • Babel
  • TypeScript
  • Traceur

GitHub - systemjs/systemjs: Dynamic ES module loader

ES6 のコードはトランスパイルする必要があるみたいです。 ひとつ前の引用で、 ECMAScript Module でも読み込めるように記載がありましたが、そのままの ECMAScript Module では読み込めなくて、トランスパイルする必要があるみたいでした。

この記事を書き終えた後で、 ECMAScript Module をそのまま読み込んでみたら、 Firefox のコンソールに次のエラーが出力されました。

XML パースエラー: 整形式になっていません。
URL: file:///path/src/main.js
行番号: 1, 列番号: 1:
Error: Unable to dynamically transpile ES module
   A loader plugin needs to be configured via `SystemJS.config({ transpiler: 'transpiler-module' })`.
  Instantiating file:///path/src/main.js
  Loading ./main.js

Microsoft Edge では何も起きませんでした。 正しい結果はコンソールに出力されないし、エラーもコンソールに出力されませんでした。

Examples

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

Babel のインストール

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

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

Babel の設定ファイル

Usage

Via .babelrc (Recommended)

.babelrc

Without options:

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

ES2015 modules to SystemJS transform · Babel

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

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

トランスパイル

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

'use strict';

System.register([], function (_export, _context) {
  "use strict";

  var foo, graph;
  function cube(x) {
    return x * x * x;
  }
  return {
    setters: [],
    execute: function () {
      _export('foo', foo = Math.PI + Math.SQRT2);

      _export('graph', graph = {
        options: {
          color: 'white',
          thickness: '2px'
        },
        draw: function draw() {
          console.log('From graph draw function');
        }
      });

      _export('cube', cube);

      _export('foo', foo);

      _export('graph', graph);
    }
  };
});

System.register 関数を使うようです。 _export 関数で export しているようで、独特な印象を受けました。

SystemJS って、モジュールのローダーだけかと思っていましたが、モジュールの定義の仕様もあったようでした。

main.js

次に、 main.js です。

'use strict';

System.register(['./my-module.js'], function (_export, _context) {
  "use strict";

  var cube, foo, graph;
  return {
    setters: [function (_myModuleJs) {
      cube = _myModuleJs.cube;
      foo = _myModuleJs.foo;
      graph = _myModuleJs.graph;
    }],
    execute: function () {
      graph.options = {
        color: 'blue',
        thickness: '3px'
      };

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

こちらでも System.register 関数が使われています。

System.register 関数の中の return しているオブジェクトの中に、メインの処理を記述するのが SystemJS の特徴なのかな。 そのオブジェクトの execute のプロパティがメインっぽい印象を受けました。 setters プロパティはモジュールの依存関係かな。

systemjs.html

トランスパイルしたコードを呼び出すためにはどうすればよいのか? getting started のページを見てみました。

Loading Modules

Any URL can be loaded as a module with standard URL syntax:

<script src="system.js"></script>
<script>
  // loads relative to the current page URL
  SystemJS.import('./local-module.js');

  // load from an absolute URL directly
  SystemJS.import('https://code.jquery.com/jquery.js');
</script>

systemjs/getting-started.md at master · systemjs/systemjs · GitHub

system.js のファイルを先に読み込んでおいて、 SystemJS.import 関数にエントリーポイントのファイルのパスを渡してあげればよさそうです。

これを参考に、次のような HTML ファイルを作成しました。

<!-- systemjs.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>SystemJS</title>
  </head>
  <body>
    <h1>SystemJS</h1>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.21.0/system.js"></script>
    <script>
      // loads relative to the current page URL
      SystemJS.import('./main.js');
    </script>
  </body>
</html>

system.js のファイルは CDN から読み込むようにしてみました。

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

実行

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

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

From graph draw function
27
4.555806215962888

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

終わり

SystemJS は、 Dynamic ES module loader と記載されていたように、ブラウザー上で動的にモジュールの依存関係を解決するもののようでした。 これは AMD(RequireJS) と同様の特徴のように感じました。 SystemJS は AMD の仕様のモジュールも読み込めるようなので、 3 つ前の記事でも RequireJS を使うこともなかったのかもしれません。

前回の記事では、 CommonJS Modules の仕様のコードを Browserify を使って事前にバンドルしましたが、SystemJS を使えばバンドルしなくてもブラウザーで使えるようでした。

SystemJS は比較的最近出てきたのかな。 読み込むモジュールの定義は CommonJS でも AMD でもよいところが便利そうでした。

たくさんモジュールがある場合は、ブラウザーから何度もリクエストを送ることになるのかな。 そうなると、パフォーマンス的には事前にバンドルする仕組みの方ががいいのかな。

4 April 2018 追記

次の記事を書きました。