ES6 (ES2015) ModulesのUMD化。HTMLのScript要素とES6 importでの同時読み込みに対応させる方法

最近はES6(ES2015)でJavaScriptを書くようになってきました。前回投稿した記事で紹介した自作のJavaScriptプラグイン「cb-typewriter-js」もES6の構文で書いて、Babelでコンパイルして作っています。その中で、そのプラグインのコードを書いていてかなり悩んだ事がありました。ES6で書いたコードを「HTMLのScript要素」と「ES6のimport」のどちらの方法でも読み込めるようにする事です。なんとか解決方法がわかったのでまとめておきます。

ES6(ES2015) Modulesで何がやりたいのか

JavaScriptのプラグインという性質上、用途は様々となりえます。scriptタグで読み込んで使う人もいれば、最近はES6(ES2015)で開発する人もいるので、ES6のimportで読み込んで使う人もいます。つまりプラグインはそれぞれの環境で使えるようにしておく必要があります。

<!-- scriptタグで読み込んで使う -->
<script src="Foo.js"></script>
// ES6(ES2015)のimportで読み込んで使う
import Foo from 'Foo.js';

今回特に悩んだのは、1つのファイルで同時に上記の2つの読み込み方法に対応させる事でした。つまずいたところから、解決に至るまでを順を追って説明していきます。

「HTMLのscript要素」で読み込ませるようにすると

例えばES6構文で以下のようなクラスを定義したfoo.jsファイルを書いたとします。

class Foo {
  bar() {
    alert('bar');
  }
}
foo.js

このファイルをBabelでコンパイルして、foo2.jsというファイル名で吐き出します。

babel foo.js -o foo2.js

foo2.jsを「HTMLのscript要素」で読み込むと、以下のようにfoo.jsで定義したFooクラスをnewして使えるようになります。

<script src="foo2.js"></script>
<script>
var foo = new Foo;
foo.bar(); // => 'bar'がアラートされる
</script>
HTMLファイル

新たにapp.jsというファイルを用意し、このfoo2.jsファイルを「ES6のimport」で読み込んで処理を書いてみます。

import Foo from "foo.js";
var foo = new Foo;
foo.bar();
app.js

上記のapp.jsをbrowserifyして、ブラウザで読み込んでみると、以下のようなエラーが発生してしまいます。

Uncaught TypeError: _foo2.default is not a function

「ES6(ES2015)のimport」で読み込ませるようにすると

そこで、今度はimportして使えるように、Fooクラスを以下のようにexportするようにしてみます。foo.jsファイルに以下の一文を追加します。

class Foo {
  bar() {
    alert('bar');
  }
}
export default Foo; // 追加
foo.js

再度Babelでfoo2.jsとしてコンパイルすると、今度はimportして使えるようになりますが、「HTMLのscript要素」で読み込んで使おうとすると、以下のようなエラーが出てしまいます。

<script src="foo2.js"></script>
<script>
var foo = new Foo;
foo.bar(); // => ReferenceError: exports is not defined
</script>
HTMLファイル

エラーの原因を探ってみると、Babelでコンパイルされて吐き出されたコードに以下が含まれているからということがわかりました。「exports」が定義されていないよと怒られます。

Object.defineProperty(exports, "__esModule", {
  value: true
});

Browserifyを使うとどうなる?

ということで、「exports」のエラー解消のためBrowserifyを使うことを考え、以下の方法を試しました。

上記のfoo.jsをbabelifyを使ってbrowserifyして、foo2.jsに変換します。

browserify -t babelify foo.js -o foo2.js

しかし、残念ながら変換したfoo2.jsを「HTMLのscript要素」で読み込むと、今度は以下のようなエラーが出てしまいます。

<script src="foo2.js"></script>
<script>
var foo = new Foo; // => Uncaught ReferenceError: Foo is not defined
foo.bar();
</script>
HTMLファイル

というのも、browserifyするとファイル単位でスコープができてしまうので、内部で指定したFooクラスにアクセスできなくなり、Fooを外部で使えなくなってしまうからです。プラグイン本体と処理をわけるような使い方には向いていません。

さらに、上記でbrowserifyで変換したfoo2.jsは、importして使うこともできません。以下のようにapp.jsファイルに「ES6のimport」で読み込んで処理を書いてみます。

import Foo from "foo2.js";
var foo = new Foo;
foo.bar();
app.js/figcaption>

上記app.jsファイルをbrowserifyして、ブラウザで読み込んでみると、以下のようなエラーが発生してしまいます。

Uncaught TypeError: _foo2.default is not a function

ということで、残念ながら今回のケースではBrowserifyは使えないということがわかりました。

babel-plugin-transform-es2015-modules-umdを使う

いろいろ調べている中で見つけたのが、この「babel-plugin-transform-es2015-modules-umd」です。Babelのコンパイル時にUMD(Universal Module Definition)に対応するように変換してくれるBabelのプラグインです。このプラグインを使ってみることにしました。

babel-plugin-transform-es2015-modules-umdのインストールは以下のコマンドで行います。

$ npm install babel-plugin-transform-es2015-modules-umd

インストールしたbabel-plugin-transform-es2015-modules-umdを使用するには、.babelrcファイルのpluginsの箇所に追記します。

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

ただ、このbabel-plugin-transform-es2015-modules-umdを追加しただけでは、「Foo is not defined」が出て結局うまくいきません。さらに、Fooをグローバルで使えるように、foo.jsに以下の一文を追加します。

class Foo {
  bar() {
    alert('bar');
  }
}
export default Foo;
window.Foo = Foo; // 追加
foo.js

上記の一文を追加したfoo.jsをbabelしてfoo2.jsに変換します。

babel foo.js -o foo2.js

このfoo2.jsは、ついに自分の求めていた状態となりました。「HTMLのscript要素」でも「ES6のimport」でも読み込んで使える状態になっています。一応これで目的達成です。

ちなみにbabel-plugin-transform-es2015-modules-umdを使って吐き出されるコードは以下のようになっています。exportsがbabelでコンパイルされたコードに渡されるようになっています。

(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.foo = mod.exports;
  }
})(this, function (exports) {
  'use strict';

// 通常のbabelでコンパイルされたものがここに入る  

});

Mochaでエラーが出た時の対策

一件落着だと思ったら、テストのためにmochaした時に、またまた以下のようなエラーが出ました。上記で設定したfoo.jsのwindowの部分が邪魔しているようです。

$ mocha
ReferenceError: window is not defined

ということで、foo.jsのwindowの部分を以下のように書き直してみました。

class Foo {
  bar() {
    alert('bar');
  }
}
export default Foo;
if (typeof window != "undefined") {
  !window.Foo && (window.Foo = Foo);
}
foo.js

これでmochaした時もエラーが出なくなりました。

まとめ

今回はなんとかtransform-es2015-modules-umdというBabelのプラグインがあったので解決できましたが、このプラグインがなければどうしていたでしょうか。自分にとっては、ES6(ES2015)のimport/exportはまだまだわからないことが多いです。もっともっと勉強が必要ですね。

ところで、今回いろいろ調べている中で、実際に試してはいませんが、ネット上の情報などを読んで他にも使えそうだなと思ったものがあったので紹介しておきます。

それから、今回は結構長い間解決できなかったことだったので、WebエンジニアのためのQ&Aサイトteratailでも相談させていただきました。他にも良い方法などがありましたら、ぜひお知らせいただけますと幸いです。

コメント一覧

  • 必須

コメント