JavaScript Babel7で出来ることを確かめる

webpackやrollupのようなバンドラーや、Vue CLIのような開発ツールとともにBabelが使われている。しかし、そもそもBabelは何なのか、そしてどう使えばよいのだろうか。
この記事では、Babel7の挙動を確認していく。

2019/5/22追記
Babel 7.4.0で非推奨になった@babel/polyfillからcore-js@3に移行する方法についてはこちらの記事に書いた。

Babelとは

Babelとは、ブラウザでサポートされていないJavaScriptの新機能を、現在のブラウザで使えるようにするツールだ。JavaScriptの新機能は毎年追加されるが、ブラウザでその機能が使えるようになるのは数年先だ。Babelを使うことで、開発者は最新の機能をつかってコードを読みやすく、メンテナンスしやすい形で書くことができ、現在のブラウザでも動くようにすることができる。

BabelでES2015のアロー関数を変換する

さっそく、BabelでES2015のアロー関数をどのブラウザでも動くようにしてみる。 コマンドラインでBabelを使えるようにするため@babel/cliと、@babel/coreをインストールする。

npm install --save-dev @babel/core @babel/cli

package.json

{
  "devDependencies": {
    "@babel/cli": "^7.2.3",
    "@babel/core": "^7.3.4"
  }
}

そして、アロー関数の書かれたindex.jsを用意する。

index.js

[1, 2, 3].map((n) => n + 1);

ディレクトリ

.
├── index.js
├── package-lock.json
└── package.json

babel [入力ファイル] --out-file [出力ファイル]として変換する。

$npx babel index.js --out-file script-compiled.js

出力されたファイルを確認すると、もとのままだ。

script-compiled.js

[1, 2, 3].map(n => n + 1);

どうやら@babel/coreだけでは何も変換してくれないようだ。
アロー関数を使えるようにするにはプラグイン、またはプリセットを使う。
まずはプラグインから見ていく。

プラグイン(@babel/plugin-transform-arrow-functions)で変換する

アロー関数を変換するために@babel/plugin-transform-arrow-functionsをインストールする。

npm install --save-dev @babel/plugin-transform-arrow-functions




 



{
  "devDependencies": {
    "@babel/cli": "^7.2.3",
    "@babel/core": "^7.3.4",
    "@babel/plugin-transform-arrow-functions": "^7.2.0"
  }
}

--pluginsオプションでプラグインを指定する。

npx babel index.js --out-file script-compiled.js --plugins=@babel/plugin-transform-arrow-functions

script-compiled.js

[1, 2, 3].map(function (n) {
  return n + 1;
});

今度は()=>{}で書いていた部分がfunction(){}に変換できた。
プラグインはアロー関数だけでなく、仕様ごとに個別に用意されている。
https://babeljs.io/docs/en/plugins

プリセット(@babel/preset-env)で変換する

プラグインでアロー関数を使えるようになったが、他にもletやconstを使ったブロックスコープや、クラス構文など策定済みの仕様をまとめて使えるようにしたい。
Babelにはプラグインをまとめて設定できるプリセットというものが用意されている。このプリセットを試してみる。

npm install --save-dev @babel/preset-env

package.json





 



{
  "devDependencies": {
    "@babel/cli": "^7.2.3",
    "@babel/core": "^7.3.4",
    "@babel/preset-env": "^7.3.4"
  }
}

--presetsオプションでプリセットを指定する。

npx babel index.js --out-file script-compiled.js --presets=@babel/env

出力されたscript-compiled.jsをみると、プラグインを使ったときと同じようにアロー関数がES5で動くように変換されている。

script-compiled.js

"use strict";

[1, 2, 3].map(function (n) {
  return n + 1;
});

また、アロー関数だけではなく、ブロックスコープやクラス構文をつかったコードも変換できるようになっている。

index.js

(() => {
  class Counter {
    constructor() {
      this.count = 0;
    }

    increment() {
      this.count++;
    }

  }

  const counter = new Counter();
  console.info(counter.count); // 0
  counter.increment()
  console.info(counter.count); // 1

})();

出力されたscript-compiled.jsでは、classconstのような記述がなくなり、ES5で動く形に変換されている。

script-compiled.js

"use strict";

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a 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); } }

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

(function () {
  var Counter =
  /*#__PURE__*/
  function () {
    function Counter() {
      _classCallCheck(this, Counter);

      this.count = 0;
    }

    _createClass(Counter, [{
      key: "increment",
      value: function increment() {
        this.count++;
      }
    }]);

    return Counter;
  }();

  var counter = new Counter();
  console.info(counter.count); // 0

  counter.increment();
  console.info(counter.count); // 1
})();

babel.config.jsでプラグインやプリセットを設定する

コマンドラインに引数を増やしていくと実行するのが手間になる。Babelではどのプラグインやプリセットを使うかを予めファイルで指定しておくことができる。
babel.config.jsという名前のファイルをpackage.jsonと同じ階層に配置しておくと、babelコマンドを実行した際にそのファイルの設定を参照するようになる。

babel.config.jsでプリセットを設定する。

babel.config.js

module.exports = function (api) {
  api.cache(true);
  const presets = [
    ['@babel/preset-env'],
  ];
  return {
    presets,
  };
}

ついでに入力ファイルと出力ファイルはnpm-scriptsで指定する。

package.json

  "scripts": {
    "build": "babel index.js --out-file script-compiled.js"
  },

これでnpm run buildと実行するだけで、先ほどまでと同じ結果が出力できるようになった。

.browserslistrcでサポートするブラウザを指定する

サポートするブラウザがすでにJavaScriptの新機能をサポートしていれば、Babelで変換する必要はない。Babelで必要な変換だけするためにサポートブラウザを指定できる。
指定する方法としてはbabel.config.jspackage.json.browserslistrcがある。
Babelでは.browserslistrcで設定することが推奨されている。
ただし、browserslistではpackage.jsonが推奨されていたので、好みの方を選ぶ。

For browser- or Electron-based projects, we recommend using a .browserslistrc file to specify targets. https://babeljs.io/docs/en/babel-preset-env


browserslist key in package.json file in current or parent directories. We recommend this way. https://github.com/browserslist/browserslist

アロー関数でbrowserslistの挙動を確かめてみる。
Chromeはバージョン44までは未対応、バージョン45からは対応している。つまり、バージョン44ならアロー関数はfunctionの形に変換されるし、バージョン45なら()=>{}のままのはずだが...

arrow functionのブラウザ対応状況

index.js

[1, 2, 3].map((n) => n + 1);

.browserslistrc

Chrome 44

バージョン44では予想どうり変換された。

script-compiled.js

"use strict";

[1, 2, 3].map(function (n) {
  return n + 1;
});

ところが、バージョン45ではブラウザがサポートしているから変換されないと思っていたのだけど、変換された。バージョン47までバージョンをあげると変換されなくなった。要確認。

.browserslistrc

Chrome 45

script-compiled.js

"use strict";

[1, 2, 3].map(function (n) {
  return n + 1;
});

なお、browserslistでサポート対象にしているブラウザのバージョンはnpx browserslistで確認できる。

提案中の仕様を使う

プライベートメソッドのような提案中(2019年の3月時点)の仕様を使うには、個別にプラグインを指定する。

npm install --save-dev @babel/plugin-proposal-private-methods

package.json




 



  "devDependencies": {
    "@babel/cli": "^7.2.3",
    "@babel/core": "^7.3.4",
    "@babel/plugin-proposal-private-methods": "^7.3.4",
    "@babel/preset-env": "^7.3.4"
  }

babel.config.js






 
 
 


 



module.exports = function (api) {
  api.cache(true);
  const presets = [
    ['@babel/preset-env'],
  ];
  const plugins = [
    ['@babel/plugin-proposal-private-methods'],
  ];
  return {
    presets,
    plugins,
  };
}

プラグインに@babel/plugin-proposal-private-methodsを追加することで、メソッド名の先頭に#のついたメソッドは外部からアクセスできなくなる。
つまり、次のCounterクラスのincrementは外部からアクセスできないが、クラス内のメソッドからはアクセスできる。

index.js






 




 







class Counter {
  constructor() {
    this.count = 0;
  }

  #increment() {
    this.count++;
  }

  publicIncrement() {
    return this.#increment();
  }
}

const counter = new Counter();
console.info(counter.increment);
console.info(counter.publicIncrement);

Babelで変換して実行すると、counter.incrementundefinedcounter.incrementOutside[Function: incrementOutside]となり#のついたメソッドは外からアクセスできないことがわかる。

script-compiled.js

"use strict";

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a 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); } }

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

function _classPrivateMethodGet(receiver, privateSet, fn) { if (!privateSet.has(receiver)) { throw new TypeError("attempted to get private field on non-instance"); } return fn; }

var Counter =
/*#__PURE__*/
function () {
  function Counter() {
    _classCallCheck(this, Counter);

    _increment.add(this);

    this.count = 0;
  }

  _createClass(Counter, [{
    key: "publicIncrement",
    value: function publicIncrement() {
      return _classPrivateMethodGet(this, _increment, _increment2).call(this);
    }
  }]);

  return Counter;
}();

var _increment = new WeakSet();

var _increment2 = function _increment2() {
  this.count++;
};

var counter = new Counter();
console.info(counter.increment);
console.info(counter.publicIncrement);

@babel/preset-envだけではできないこと

@babel/preset-envを使うことで、新しい構文を使うことができるようになった。
しかし、次のことは@babel/polyfillまたは@babel/plugin-transform-runtimeを使わないと動作しない。

  1. ブラウザで未対応のグローバルオブジェクト(PromiseやMapなど)を使う
  2. ブラウザで未対応のグローバルオブジェクトのメソッド(Array.prototype.includesやString.prototype.padStartなど)を使う

@babel/polyfillでポリフィルを使えるようにする

まずは@babel/polyfillを使うことでPromiseasync/awaitが使えるようになることを確かめる。

npm install --save-dev @babel/polyfill




 




{
    "devDependencies": {
        "@babel/cli": "^7.2.3",
        "@babel/core": "^7.3.4",
        "@babel/polyfill": "^7.2.5",
        "@babel/preset-env": "^7.3.4"
    }
}

index.js

 
















import "@babel/polyfill";
const resolveAfter2Seconds = () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('resolved');
    }, 2000);
  });
};

const asyncCall = async () => {
  console.log('calling');
  var result = await resolveAfter2Seconds();
  console.log(result);
};

asyncCall();

@babel/polyfillを使う場合は、ファイルの先頭にimport "@babel/polyfill";を書く。
しかし、JavaScriptをBabelで変換しても、出力されたJavaScriptにはrequire("@babel/polyfill");のようにrequire関数が必要な形で変換される。
そのため、ブラウザで実行するとUncaught ReferenceError: require is not definedというエラーが表示される。

ブラウザで使うだけなら、@babel/polyfillのdistディレクトリにあるpolyfill.jsをscriptタグで読み込めば動くようになる。しかし、大抵の場合はimport "@babel/polyfill";を記載した上でwebpackrollupを使って1ファイルにまとめてしまう。この記事では他のビルドツールに関しては記述しないので、polyfill.jsを読み込んだHTMLだけ記述しておく。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>polyfill</title>
</head>
<body>
<script src="./node_modules/@babel/polyfill/dist/polyfill.js"></script>
<script src="./script-compiled.js"></script>
</body>
</html>

@babel/plugin-transform-runtimeでポリフィルを使えるようにする

@babel/plugin-transform-runtimeを使う方法はrequireでruntime-corejs2を読み込む形でJavaScriptを出力するため、webpackrollupと一緒に使わないとブラウザでは動かない。


それでは、必要なモジュールをインストールしていく。
@babel/runtime-corejs2--saveになっているのは実行時にポリフィルを読み込むため。また、@babel/runtime-coreというものもあるが、こちらはポリフィルを含まないのでPromiseなどを使いたいときは使わない。

npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime-corejs2 

設定のポイントは"corejs": 2,としているところだ。
この設定がないとポリフィルが読み込まれない。

babel.config.js







 
 
 







module.exports = function (api) {
  api.cache(true);
  const presets = [
    ['@babel/preset-env']
  ];
  const plugins = [
    ['@babel/plugin-transform-runtime',{
     "corejs": 2,
    }]
  ];
  return {
    presets,
    plugins,
  };
}

次のJavaScriptを変換する。

index.js

const resolveAfter2Seconds = () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('resolved');
    }, 2000);
  });
};

const asyncCall = async () => {
  console.log('calling');
  var result = await resolveAfter2Seconds();
  console.log(result);
};

asyncCall();

出力されたJavaScriptの一部は次のようになる。
Promise_promise.defaultで置き換えられている。

script-compiled.js












 


 



"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");

var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs2/regenerator"));

var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/asyncToGenerator"));

var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise"));

var resolveAfter2Seconds = function resolveAfter2Seconds() {
  return new _promise.default(function (resolve) {
    setTimeout(function () {
      resolve('resolved');
    }, 2000);
  });
};

https://babeljs.io/docs/en/next/v7-migration#babel-runtime-babel-plugin-transform-runtime

@babel/polyfillと@babel/plugin-transform-runtimeどちらを使うべきか

WEBサービスをつくるなら@babel/polyfillを使えば良い。
ライブラリをつくるなら@babel/plugin-transform-runtimeを使えば良い。

@babel/polyfill @babel/plugin-transform-runtime
使えるようにするためのポイント ポリフィルを使うファイルの先頭でimport "@babel/polyfill";を記載する 1. @babel/runtimeではなく@babel/runtime-corejs2を使う 

2. '@babel/plugin-transform-runtimeプラグインのオプションで{"corejs": 2}を設定する
グローバル汚染 あり なし
使えないポリフィル なし ブラウザで未対応のグローバルオブジェクトのメソッド(Array.prototype.includesやString.prototype.padStartなど)が使えない

・参考
https://babeljs.io/
https://codersmind.com/babel-polyfill-babel-runtime-explained/