WebDesign Dackel

rollup.jsでES6なモジュールをブラウザとNodeの両対応させる

Hatena0
Google+0
Pocket0
Feedly0

まだES6 Modulesの仕様が確定せず、実際に使うのってどうなの?みたいなところについては、様々な議論があると思います。

ただ、「せっかくES6だしモジュール周りもそれっぽく書きたい!!」ということで、rollup.jsを使って、ブラウザとNodeの両方で実行可能なモジュールを書いてみました。

そもそもrollup.jsって何者?

browserifywebpackみたいなバンドルツールで、ES6に特化したもののようです。(勘違いあったらごめんなさい…)

吐き出すコードのフォーマットも、CommonJSをはじめ、AMDやグローバル変数、後述しますがUMD(Universal Module Definition)を選択出来るなど柔軟性に富んでいます。しかも最終的なコードがかなりすっきりとした形で出てきます。

そのおかげで、先日作ったcake-hash.jsでは、最初browserifyでバンドル+minifyしていたものが24KBrollup.jsに切り替えてから12KBと、丁度半分のファイルサイズになりました。

まだ一度しか使ってないので、詳しいことは分かりませんが簡単なモジュールであれば移行するのは楽そうでした!

対象のモジュール

四則演算を行うためのCalculatorモジュールをテーマにしてみました。

addition.js

export default function addition(a, b) {
  return a + b;
}

subtraction.js

export default function subtraction(a, b) {
  return a - b;
}

multiplication.js

export default function multiplication(a, b) {
  return a * b;
}

division.js

export default function division(a, b) {
  return a / b;
}

calculator.js

import addition from "./addition"
import subtraction from "./subtraction"
import multiplication from "./multiplication"
import division from "./division"

export default class Calculator {
  constructor(a) {
    this.setValue(a);
  }

  setValue(a) {
    this.a = a;
  }

  getValue() {
    return this.a;
  }

  addition(b) {
    this.a = addition(this.a, b);
  }

  subtraction(b) {
    this.a = subtraction(this.a, b);
  }

  multiplication(b) {
    this.a = multiplication(this.a, b);
  }

  division(b) {
    this.a = division(this.a, b);
  }
}

Node上では以下のような感じで動くことを想定します。

var Calculator = require("./calculator")
var calc = new Calculator(10);

calc.addition(5);
console.log(calc.getValue()); //15

ライブラリのインストール

ES6のコードはBabelを使ってES5に変換したいので、rollup含めて幾つかライブラリをインストールしておきます。

$ npm install --save-dev rollup rollup-plugin-babel babel-preset-es2015-rollup

ここでインストールしたものは以下。

まだまだ問題ありな感じがしますが、Babel6にチャレンジです。
そのため、babel用のプラグインと、rollup用のpresetsを使用します。

rollupとbabelの設定

rollupCLIで使えますが、pluginの指定が見当たらなかったので設定ファイルを用意、その設定ファイルを読みこませるような形で実行したいと思います。

rollup.config.js

import babel from "rollup-plugin-babel"

export default {
  entry: "src/calculator.js",
  dest: "calculator.js",
  format: "umd",
  moduleName: "Calculator",
  plugins: [babel()]
}

ポイントは、ブラウザ、Nodeの両対応にするためにformatオプションにumdを渡します。
あとは、ブラウザでwindowオブジェクトに割り当てるモジュールの名前をmoduleNameに指定する箇所です。
entrydestらへんは他のバンドルツールと同じです。

babelの設定はいつも通り.babelrcに書いておきます。

.babelrc

{
  "presets": ["es2015-rollup"]
}

ここまでのファイル構成は以下。

.
├── node_modules
│   ├── babel-preset-es2015-rollup
│   ├── rollup
│   └── rollup-plugin-babel
├── package.json
├── rollup.config.js
└── src
    ├── addition.js
    ├── calculator.js
    ├── division.js
    ├── multiplication.js
    └── subtraction.js

ビルドを実行!

package.jsonscriptsを以下のように変更します。

"scripts": {
  "build": "rollup -c rollup.config.js"
}

-cオプションで、設定ファイルへのパスを指定しているだけです。

これで、以下のコマンドを実行します。

$ npm run build

実行が完了して、同階層にcalculator.jsが出来ていたらOKです!

calculator.jsの中身

(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  typeof define === 'function' && define.amd ? define(factory) :
  global.Calculator = factory();
}(this, function () { 'use strict';

  var babelHelpers = {};

  babelHelpers.classCallCheck = function (instance, Constructor) {
    if (!(instance instanceof Constructor)) {
      throw new TypeError("Cannot call a class as a function");
    }
  };

  babelHelpers.createClass = (function () {
    function defineProperties(target, props) {
      for (var i = 0; i < props.length; i++) {
        var descriptor = props[i];
        descriptor.enumerable = descriptor.enumerable || false;
        descriptor.configurable = true;
        if ("value" in descriptor) descriptor.writable = true;
        Object.defineProperty(target, descriptor.key, descriptor);
      }
    }

    return function (Constructor, protoProps, staticProps) {
      if (protoProps) defineProperties(Constructor.prototype, protoProps);
      if (staticProps) defineProperties(Constructor, staticProps);
      return Constructor;
    };
  })();

  babelHelpers;
  function division(a, b) {
    return a / b;
  }

  function multiplication(a, b) {
    return a * b;
  }

  function subtraction(a, b) {
    return a - b;
  }

  function addition(a, b) {
    return a + b;
  }

  var Calculator = (function () {
    function Calculator(a) {
      babelHelpers.classCallCheck(this, Calculator);

      this.setValue(a);
    }

    babelHelpers.createClass(Calculator, [{
      key: "setValue",
      value: function setValue(a) {
        this.a = a;
      }
    }, {
      key: "getValue",
      value: function getValue() {
        return this.a;
      }
    }, {
      key: "addition",
      value: function addition$$(b) {
        this.a = addition(this.a, b);
      }
    }, {
      key: "subtraction",
      value: function subtraction$$(b) {
        this.a = subtraction(this.a, b);
      }
    }, {
      key: "multiplication",
      value: function multiplication$$(b) {
        this.a = multiplication(this.a, b);
      }
    }, {
      key: "division",
      value: function division$$(b) {
        this.a = division(this.a, b);
      }
    }]);
    return Calculator;
  })();

  return Calculator;

}));

上5行がUMDな出力になっています。

ブラウザではmoduleNameに指定した名前で、window(this)に直接モジュールを生やして、AMDCommonJS環境下であればそちらに渡す、といった感じです。

詳しくは以下が参考になると思います。

umdjs/umd


以上でES6で書いたモジュールをブラウザとNodeに対応させることが出来ました!
間違いなどありましたらご指摘いただけると嬉しいです〜