Babelって結局なんなんだ

定義

Babel is a JavaScript compiler.

と、公式にでっかく書いてありますね。

さらにドキュメントを読み進めていくと、

Now, out of the box Babel doesn’t do anything. It basically acts like const babel = code => code;

と書いてあります。

const babel = code => code;

基本は受け取ったコードをそのまま吐き出すだけ、どう変換させるかは設定次第ということですね。つまり設定に応じてJavaScriptをコンパイルしてくれるものというのがBabelの定義のようです。 (正確にはトランスパイルだろと言いたいところですが、この記事では公式にならってコンパイルと表現します)

構成要素

ドキュメントを見る限りBabelは大きく3要素から成ります。

babel-core

コンパイル処理を行うBabelの本体です。

babel-polyfill

Babelが用意してくれているpolyfill集で、core-jsregenerator runtime を内包しています。これら2つはそれぞれ独立した(Bebelとは関係ない外部の)poryfill集です。
要は世の中のイケてるporyfillをまとめて提供してくれるものがbabel-polyfillだという認識でいいと思います。

Plugins

「設定に応じてJavaScriptをコンパイルしてくれるもの」の「設定に応じて」の部分を担当するのがpluginです。「どうコンパイルするか」を定義しているもので、よく見かけるbabel-preset-es2015などがこれに該当します。

※ 正確には、複数のpluginを特定の用途に合わせてセットにしたものが「babel-preset-xxxx」です。よくつかうplugin集をpresetとして提供してくれているわけです。

最新の記法でJSを書くにはどうしたらいいんだ

Babelのドキュメントを読んだ結果感じたことはこれでした。結局どうすりゃいいんだと。

ここでおさらいしておきます。
最新の記法でJSを書く際には、ES2015〜で登場した新しい文法新しい関数を使うことになります。前者はアロー関数やクラス記法などで、後者はPromise, Object.assign, Array.prototype.findなどです。

「最新の記法でJSを書く」=「この2つを含むソースをBabelでコンパイルする」 ということになりますね。それには以下の2つのことが必要です。

  • babel-preset-envなどのplugin・presetを使って新しい文法をコンパイルできるようにする
  • babel-polyfillなどのpolyfillを入れて新しい関数が動くようにする

最低限の設定方法

ここまでの内容をCLIで実行するための最低限の手順は以下のようになります。

1. Installする
$ npm install -D babel-cli babel-preset-env 
$ npm install -S babel-polyfill
2. .babelrcに設定を書く
{
  "presets": ["env"]
}
3. polyfillをrequireする
// input.js
require('babel-polyfill');

// 以下実装
4. Babeる(Babelでコンパイルする)
$ npx babel ./input.js -o ./output.js

これが基本の使い方です。これを応用して、JSXをコンパイルさせたかったらbabel-preset-reactを利用したり、bundleさせたかったらWebpackなどのツールと組み合わせたり…といったカスタマイズをしていく感じになると思います。

Webpackと組み合わせた実践例

ここからは「ES02015+の記法で書いたJSをWebpackでbundleする」というよくあるパターンの実践例です。

キホンのキ

まず基礎ですが、WebpackでBabelを使うにはbabel-loaderbabel-coreを読み込みますよね。babel-loaderはWebpackに「受け取ったコードをBabeってね」と指示を出すもので、指示に基づいて実際にBabeるのがbabel-core、という感じです。

Install

$ npm install -D babel-loader babel-core

webpack.config.js

module: {
  rules: [
    { 
      test: /\.js$/, 
      exclude: /node_modules/, 
      loader: "babel-loader" 
    }
  ]
}

pluginの適用は上記の「最低限の設定」と同様に.babelrcに書きます。polyfillの適用の仕方は3通りあるので、やりたいことベースで整理します。

polyfillの機能を制限なく使いたい、でも無駄なものは入れたくない

polyfillの対象となる機能には3つのタイプがあります。

  • globals
    Promiseなど新しいグローバルオブジェクト
  • native methods
    Object.assignなどの既存のグローバルオブジェクトに追加されたメソッド
  • instance methods
    Array.prototype.findなどのインスタンスが持つメソッド

これらすべてを使うにはbabel-polyfillをbundleに含めることが必要です。それにはentry pointとなるファイルの1行目1度だけbabel-polyfillを読み込みます。

// CommonJS
require('babel-polyfill');

// ES6
import 'babel-polyfill';

「1度だけ」というのがとても重要で、複数回読み込むとpolyfillがぶっ壊れます。あるHTMLで2つのjsファイルを読み込んでいて、それぞれでbabel-polyfillを読み込んでいる場合もぶっ壊れます。とにかく2回読み込まれたらマズイのです。

この二重読み込み制約にさえ気をつけていれば、この方法ですべてのpolyfillを利用することができます。加えて、babel-preset-envと組み合わせて利用する場合は、useBuiltInsオプションを設定することで全部入りのbabel-polyfillを、必要なpolyfill(該当するcore-jsのモジュール)のみに変換してくれます

.babelrc

{
  "presets": [
    ["env", { "useBuiltIns": true }]
  ]
}

app.js (entry point)

// Babeる前
import 'babel-polyfill';

// Babeった後 使っているものだけimportするように変換される
import 'core-js/modules/es7.string.pad-start';
import 'core-js/modules/es7.string.pad-end';

まとめ

⭕:全polyfillを利用できる、必要な分だけ入れられる
❌:二重読み込み制約のリスクがある、グローバルが汚染される

「二重読み込み制約」のリスクを背負わずにpolyfillを使いたい

WebpackとBabelを組み合わせる場合、二重読み込み制約のリスクを回避する方法があります。それはソース内でbabel-polyfillを読み込むのではなく、bundle時にbabel-polyfillを結合する方法です。以下のように設定します。

webpack.config.js

module.exports = {
  entry: ["babel-polyfill", "./app.js"]
};

ただし、この方法は上述のuseBuiltInsオプションが効かなくなるので注意です。

まとめ

⭕:二重読み込みのリスクがない、全polyfillを利用できる
❌:useBuiltInsオプションが効かない、グローバルが汚染される

グローバルを汚染せずにpolyfillを使いたい、複数ファイルでpolyfillを使いたい

ライブラリを実装する場合などでグローバルを汚染せずにES2015+の関数を利用したいときは、transform-runtimepluginを使います。また、ひとつのアプリケーションで複数のJSファイルを読み込む場合など、polyfill読込が重複してしまうのを避けたいときにもtransform-runtimepluginが使えます。

これはES2015+の関数を、core-jsのエイリアスであるbabel-runtimeモジュールで書き換えるものです。本来polyfillとは「そのままでは動かないコードを補うためのコード」を埋め込むものなので、対象コードそのものを書き換えてしまうtrunsform-runtimeは正確にはpolyfillとは呼べないですが、おかげで上述の利点を得られるわけです。

そして、ここまでで勘の良い方はお気づきかもですが、instance methodsを利用できないという制約がつきます。

設定するには、変換処理を行うbabel-plugin-transform-runtimeと、core-jsのエイリアスであるbabel-runtimeを npm install します。後者はソースに含まれることになるので--saveオプションで。

Install

$ npm install -D babel-plugin-transform-runtime
$ npm install -S babel-runtime

.babelrc

{
  "plugins": ["transform-runtime"]
}

まとめ

⭕:グローバルを汚染しない、使うものだけ置き換えられる
❌:インスタンスメソッドは使えない

参考:Runtime transform · Babel

早見表

  メリット デメリット
require babel-polyfill 全polyfillを利用できる
必要な分だけ入れられる
二重読み込みのリスクがある
グローバルが汚染される
webpackで結合 二重読み込みのリスクがない
全polyfillを利用できる
useBuiltInsが効かない
グローバルが汚染される
runtime transform グローバルを汚染しない
必要な分だけ変換される
インスタンスメソッドのpolyfillは利用できない

細かい機能を上手く使いこなしたい

設定はショートハンドで書けるよ

babel-preset-hogeを npm install しているのに、設定ファイルには"presets": ["hoge"]って書くから分かりにくいな…と思ってました。これはpresetとpluginはbabel-preset- babel-plugin-の部分を省略して書くことができる故でした。
なので"presets": ["babel-preset-hoge"]と書いてもOK。ちなみにパッケージスコープの場合は"presets": ["@org/hoge"]という感じ。

babel-preset-esXXXX はいくつを使えばいいんだ

過去に書いたbabelrcを見てみたらbabel-preset-es2015を入れていることがほとんどだったのですが、ES2016、ES2017と仕様が増えている現在ではbabel-preset-envを入れておくのがベターのようです。

これは何のオプションもなしで利用するとbabel-preset-latestと同じように機能し、設定次第でbabel-preset-es2015と同様の動きをさせることもできます。

また、上述したuseBuiltInsのような便利なオプションを使うことができる利点があります。個人的にありがたいと思っているのがtargetsオプションです。ブラウザやNodeのバージョンを指定できるオプションで、指定されたバージョンに必要なpolyfillのみを入れてくれるようになります。

参考: Env preset # Options

ES2015+のソースをそのままminifyできるってよ

これはBabelの公式ブログ読んでて見つけて得た程度の知識なのですが、「古いブラウザで動かす必要がないならそのまま圧縮すれば?」ということでBabili(babel-minify)というものがあるそうです。

Install

$ npm install -D babel-core babel-loader babel-preset-babili

.babelrc

{
  "presets": ["babili"]
}

uglify-esでサポートされていない構文であっても圧縮可能なので、その点は利がありそうです。ただ執筆時点(2018/03/19)ではBeta版ということもあって、個人的にはまだ使うことはなさそうです。

おわりに

今までなんとなくの理解だった部分が整理されたおかげで、今後新しいプロジェクトを立ち上げるときに迷わなくて済みそうです。よかった。

それと公式ドキュメントがけっこう説明不足感が強くて、しかも日本語の資料もそんなに見当たらなくて整理に苦労したので、同じ苦労を誰かがせずに済むといいなと思います。 間違っているところを見つけた方は@aloerina_までご一報ください。