🔨

@babel/preset-env (Babel 7)をハンズオンでちゃんと理解する

17 min read

バージョン情報

version
node 12.16.3
@babel/core 7.9.0
@babel/preset-env 7.9.5
@babel/cli 7.8.4
browserslist 4.12.0

@babel/preset-env とは

@babel/preset-env は、 babel プラグインのプリセットのことです。

そもそも babel の本体(@babel/core)は、「JSコードを入力として受け取り、ASTに変換後、何らかの変換処理を行い、変換後のASTから再度JSコードを出力すること」が責務であり、 何らかの変換処理を指定しなければ、何のトランスパイルも行われません。

この 何らかの処理 に該当する実装がbabelプラグインのことで、開発者は実現したいトランスパイルに応じて適切な babel プラグインを適用する設定を記述する必要があります。

...でも面倒ですよね?

単純にIEを含む主要なブラウザで一通りのES6コードを動かしたいだけというのがだいたいのユースケースです。

そんな時、対応ブウラウザなどの大雑把な情報を元に、必要なプラグインを自動で選択して、最新の ES6+ を動く状態にしてくれる超便利なプラグインが @babel/preset-env です。

なんとなくイメージが湧いたでしょうか?

インストール

動作確認用の cli も含めて入れます。

$ yarn add -D @babel/cli @babel/preset-env

トランスパイル対象のコードを用意

本記事では、実際に @babel/cli@babel/preset-env を使って、Javascriptのコードが変換される様子をハンズオン形式で見ていきます。

まずは以下のような script.js を作成します。

class User {
  constructor(name) {
    this.name = name
  }
  sayHello() {
    return `Hello, ${this.name}`
  }
  async fetch() {
    return await new Promise(resolve => {
      setTimeout(() => {
        resolve('complete!!')
      }, 1000)
    })
  }
}

const user = new User('sasaki')
console.log(user.sayHello())

user.fetch().then(result => {
  console.log(result)
})

上記コードは

  • クラス構文
  • テンプレート文字列
  • Promise
  • async/await

と言った、現代のフロントエンドでは定番だけど、依然としてブラウザ互換に悩まされる要素を詰め込んだコードになっています。

babelを実行してみる

まずは何も考えずに script.js を babel にかけてみましょう。

$ babel script.js 

すると以下のような出力が得られます。

class User {
  constructor(name) {
    this.name = name;
  }

  sayHello() {
    return `Hello, ${this.name}`;
  }

  async fetch() {
    return await new Promise(resolve => {
      setTimeout(() => {
        resolve('complete!!');
      }, 1000);
    });
  }

}

const user = new User('sasaki');
console.log(user.sayHello());
user.fetch().then(result => {
  console.log(result);
});

なんということでしょう、何一つ変わっていません

それもそのはず、まだ babel の設定を何も定義していなく、当然 @babel/preset-env も使われていないので、何のトランスパイルもされずに、入力をそのまま出力しているからです。

ではここから、 .babelrc に babel の変換ルールを記述していきましょう。

シェア率2%以上のブラウザで動くコードに変換する

.babelrc を作成し、以下のようにプリセットとして @babel/preset-env を使うように指定します。

{
  "presets": [
    [
      "@babel/preset-env"
    ]
  ]
}

また、package.jsonbrowserslist というフィールドを以下のように追加します。これはこのパッケージがどのブラウザをサポートしているかを明記する方法になります。

"browserslist": [
  "> 2%"
]

(.babelrc にまとめて書いたり、 .browserslistrc を定義する方法もありますが、本記事では便宜上 package.json に分離して書きます)

まずは上記のように、 シェア率2%以上のブラウザ を対象とする設定にして、もう一度トランスパイルしてみましょう。

$ babel script.js 
"use strict";

class User {
  constructor(name) {
    this.name = name;
  }

  sayHello() {
    return `Hello, ${this.name}`;
  }

  async fetch() {
    return await new Promise(resolve => {
      setTimeout(() => {
        resolve('complete!!');
      }, 1000);
    });
  }

}

const user = new User('sasaki');
console.log(user.sayHello());
user.fetch().then(result => {
  console.log(result);
});

おやおや? まだ結果が変わっていないようです(先頭に use strict は付与されましたが)

というのも、 シェア率2%以上のブラウザならコレぐらいそのまま動くので変換不要 なようです。(2020/05/07現在)

ではシェア率2%以上とはどんなブラウザでしょうか?例のあのブラウザは何%なんでしょうか。

対象ブラウザを確認する

@babel/preset-env では、トランスパイルの対象とするブラウザを、browserslist に従って記述することができます。

browserslist は、異なるツール間でブラウザの対象範囲を共有するための仕組みで、 browserslit パッケージを導入すると、 package.json に記述したようなルールを元に、実際の対象ブラウザがどんなものかを確認することができます。

さっそく入れてみましょう。

$ yarn add -D browserslist

一応ブラウザ情報を最新化しておくと良さそうです

$ yarn browserslist --update-db

さて、この状態で、先程同様に package.json にシェア率2%以上の旨が記載された状態で browserslist を実行してみます。

"browserslist": [
  "> 2%"
]
$ yarn browserslist
and_chr 81
chrome 80
ios_saf 13.3
safari 13
samsung 11.1

なるほどなるほど。Chromeがダントツなのは予想できますが、他にはsafariと、各種モバイルブラウザが2%以上を維持しているようですね。

例の I で始まって E で終わるブラウザが入っていないので、トランスパイルが不要というもの納得できます。

シェア率1%以上のブラウザで動くコードに変換する

ではシェア率を1%以上に引き下げてみましょう。

"browserslist": [
  "> 1%"
]
$ browserslist
and_chr 81
and_uc 12.12
chrome 80
chrome 79
edge 18
firefox 74
firefox 73
ie 11
ios_saf 13.3
ios_saf 12.2-12.4
safari 13
samsung 11.1

2%以上を1%以上に引き下げただけですが、 IE edge firefox が入ってきたり、旧バージョンのiOS safariが食い込んできたりもします。

これは流石に元コードのままじゃ実行できないはずなので、babelをかけてみましょう。

$ babel script.js 
"use strict";

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }

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; }

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

    this.name = name;
  }

  _createClass(User, [{
    key: "sayHello",
    value: function sayHello() {
      return "Hello, ".concat(this.name);
    }
  }, {
    key: "fetch",
    value: function () {
      var _fetch = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
        return regeneratorRuntime.wrap(function _callee$(_context) {
          while (1) {
            switch (_context.prev = _context.next) {
              case 0:
                _context.next = 2;
                return new Promise(function (resolve) {
                  setTimeout(function () {
                    resolve('complete!!');
                  }, 1000);
                });

              case 2:
                return _context.abrupt("return", _context.sent);

              case 3:
              case "end":
                return _context.stop();
            }
          }
        }, _callee);
      }));

      function fetch() {
        return _fetch.apply(this, arguments);
      }

      return fetch;
    }()
  }]);

  return User;
}();

var user = new User('sasaki');
console.log(user.sayHello());
user.fetch().then(function (result) {
  console.log(result);
});

見事にそれらしいコードにトランスパイルされました!

@babel/preset-env の力によって、 browserslist に則って対象ブラウザを定義しただけで良い感じに変換できることがわかりました。

シェア率とか関係なくIEをサポートする

さて、先程はシェア率1%以上と指定することで、 IE さえもサポートするトランスパイルを実現しました。

しかし、この先 IE のシェア率が1%を下回った場合はどうなるでしょうか?同じ設定を使い続けていると、シェア率が下回った瞬間から、 IE がサポートの対象から外れてしまいます。

どんなにIEのシェア率が低まってもIEサポートを続けなければならないという呪われたサービス にとっては厄介な問題です。

そこで、 browserslit の定義を以下のように変えてみましょう。

  "browserslist": [
    "> 2%",
    "IE 11"
  ]

上記設定は、 シェア率2%以上、またはIE11 という、悲しみの溢れる設定になります。

$ yarn browserslist
and_chr 81
chrome 80
ie 11
ios_saf 13.3
safari 13
samsung 11.1

なんということでしょう、シェア率2%以上の人気ブラウザの中にIE11が紛れてしまっているではありませんか。

$ babel script.js
"use strict";

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }

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; }

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

    this.name = name;
  }

  _createClass(User, [{
    key: "sayHello",
    value: function sayHello() {
      return "Hello, ".concat(this.name);
    }
  }, {
    key: "fetch",
    value: function () {
      var _fetch = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
        return regeneratorRuntime.wrap(function _callee$(_context) {
          while (1) {
            switch (_context.prev = _context.next) {
              case 0:
                _context.next = 2;
                return new Promise(function (resolve) {
                  setTimeout(function () {
                    resolve('complete!!');
                  }, 1000);
                });

              case 2:
                return _context.abrupt("return", _context.sent);

              case 3:
              case "end":
                return _context.stop();
            }
          }
        }, _callee);
      }));

      function fetch() {
        return _fetch.apply(this, arguments);
      }

      return fetch;
    }()
  }]);

  return User;
}();

var user = new User('sasaki');
console.log(user.sayHello());
user.fetch().then(function (result) {
  console.log(result);
});

トランスパイルの結果も、シェア率1%以上のときとまったく同じです。つまるところ、ここまで実行してきたトランスパイルは全てIE11のためのものだったのです。

IEでPromiseが動くわけ無いだろ

ここまでトランスパイルされた結果の中に、以下のようなコードが混じっていました。

switch (_context.prev = _context.next) {
  case 0:
    _context.next = 2;
    return new Promise(function (resolve) {
      setTimeout(function () {
        resolve('complete!!');
      }, 1000);
    });

new Promise ...??

以下の Can I use... を見て分かる通り、 IE11 は依然として Promise を一切サポートしていません。

なのにIE11を対象ブラウザとしたトランスパイルをしたままなのに、このようなコードが生まれてしまうのは何故でしょうか…??

そう、 @babel/preset-env は、デフォルトだと Polyfill は行ってくれません。というよりそもそも babel の責務は構文の話であって、Promise のような追加機能に対しては別途対応が必要なのです。

ポリフィルとは、最近の機能をサポートしていない古いブラウザーで、その機能を使えるようにするためのコードです。大抵はウェブ上の JavaScript です。

とはいえ、現代の @babel/preset-env では、Polyfillもどうにかするオプションが用意されています。

Polyfill用のcore-jsをインストールする

Polyfill には一般的に core-js を使います。これはJavascriptの新機能を旧ブラウザでも使えるようにする互換コードのライブラリで、現在は 2.x系 と 3.x系 が主流です。

@babel/preset-env では一応どちらのバージョンも使えるようになっていますが、本記事では 3.x を使います。

$ yarn add -D core-js@3

@babel/preset-env で Polyfill する

まず開き直ってIEだけをサポートする設定にしちゃいます。

  "browserslist": [
    "IE 11"
  ]
$ yarn browserslist
ie 11

トランスパイル対象のコードも、最高にシンプルにします。

Promise.resolve()

.babelrc は変更せずにそのままトランスパイルするとセミコロンがついた程度で、Polyfillは行われていません。

$ yarn babel script.js
"use strict";

Promise.resolve();

ここで、 .babelrc を以下のように、 useBuiltIns オプションを指定し、 corejs3.x を使うようにします。

.babelrc
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}
$ yarn babel script.js
"use strict";

require("core-js/modules/es.object.to-string");

require("core-js/modules/es.promise");

Promise.resolve();

require("core-js/modules/es.promise"); のような互換コードを参照するコードが追加され、無事に Polyfill されていることがわかります。

useBuiltInsを理解する

前項では "useBuiltIns": "usage" のように、思考停止でuseBuiltIns オプションを指定することで、PromiseのPolyfillを実現していました。

この useBuiltIns とは、 @babel/preset-env でどのようにPolyfillを扱うかを指定するオプションで、 "usage" | "entry" | "false" のいずれかを取ります。

効果
"usage" 各ファイルで必要になったPolyfillのみ適用する
"entry" core-jsのimport文を、対象ブラウザが必要とするモジュールに置き換える
false Polyfillしない(デフォルト)

本記事序盤ではこれを指定していなかったので、デフォルトのfalseが設定され、Polyfillが行われなかったんですね。

"usage" "entry" ともに少しややこしいので、もう少し深堀りしていきましょう。

※ 以前は @babel/polyfill を使ってPolyfillするのが主流でしたが、Babel7.4.0からはこれが非推奨になっているので、本記事の内容で対応するのが望ましいです

useBuiltIns: "usage"

"usage" オプションは、ファイルの内容と対象ブラウザを見て、必要なPolyfillのみを良い感じに適用してくれるスグレモノです。

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}

最新の Chrome のみを対象とした場合は Polyfill しませんが

"use strict";

Promise.resolve();

IE11 を対象にした場合は Polyfill してくれます。

"use strict";

require("core-js/modules/es.object.to-string");

require("core-js/modules/es.promise");

Promise.resolve();

ただし、機械的にチェックしているという都合上、以下のようなトリッキーな場合などは上手く拾ってくれないので、注意が必要です。

eval('Promise.resolve()')
IEを対象にしてるのにPolyfillされない
"use strict";

eval('Promise.resolve()');

eval は極端にしても、コードの書き方によってはしっかり拾えてもらえるか確証がないので、使用する際は注意が必要です。心配であれば "entry" を使うほうが望ましいとも言えます。

useBuiltIns: "entry"

"entry" を使う場合は、アプリケーション全体で一度core-jsをimportします。

import 'core-js/stable'
Promise.resolve()

useBuiltIns: "entry" の場合、対象ブラウザの対応状況に応じて、上記のimportを、必要最低限のモジュールのimportに置き換えてくれます。

"use strict";

require("core-js/modules/web.immediate");

Promise.resolve();

(web.immediateはこれ のことっぽいけど、見た感じstableに入ってなさそうなのに何でだろ)

safari13だと18個
"use strict";

require("core-js/modules/es.promise.finally");

require("core-js/modules/es.string.replace");

require("core-js/modules/es.typed-array.float32-array");

require("core-js/modules/es.typed-array.float64-array");

require("core-js/modules/es.typed-array.int8-array");

require("core-js/modules/es.typed-array.int16-array");

require("core-js/modules/es.typed-array.int32-array");

require("core-js/modules/es.typed-array.uint8-array");

require("core-js/modules/es.typed-array.uint8-clamped-array");

require("core-js/modules/es.typed-array.uint16-array");

require("core-js/modules/es.typed-array.uint32-array");

require("core-js/modules/es.typed-array.from");

require("core-js/modules/es.typed-array.of");

require("core-js/modules/web.dom-collections.iterator");

require("core-js/modules/web.immediate");

require("core-js/modules/web.url");

require("core-js/modules/web.url.to-json");

require("core-js/modules/web.url-search-params");

Promise.resolve();
IE11だと188個
// (省略)

当たり前ですが、 core-js をimportしていなかったら何も起こらないのでご注意ください。

// import 'core-js/stable'
Promise.resolve()
"use strict";

Promise.resolve();

結論

IEのサポートを切れるなら切ろう

Discussion

ログインするとコメントできます