Babel7.x時代のpolyfillの設定方法とuseBuiltInsの仕組み
ES2015+で実装するためにBabelのpolyfillを利用する場面は多いと思いますが、Babel6.xまでと7.xではその導入方法が変わっているので注意が必要です。今回はBabel7.xでの用途別polyfillの設定方法と、キモとなるuseBuiltIns
オプションの挙動についてまとめてみます(執筆時点でのBabelのバージョンは7.1.0です)。
なお、6.xまでの設定方法は「Babelの設定を見直すための逆引きガイド」にまとめてあります。polyfillのことだけでなく、Babelとは何か、どのように利用するのか、といったことも併せてまとめてありますので良ければご参考にどうぞ。
2019/06/21 追記
Babel7.4.0から @babel/polyfill が非推奨となっています。変更点や新しい設定方法は「Babel7.4で非推奨になったbabel/polyfillの代替手段と設定方法」の記事をご確認ください。用途別polyfillの入れ方
polyfillの入れ方には大きく3種類あります。
用途 | 方法 |
---|---|
必要なpolyfillだけ入れる | useBuiltIns オプション usage を使う |
全てのpolyfillを入れる | @babel/polyfillをimport/requireする |
グローバル汚染せずにpolyfillを適用する | @babel/runtimeと @babel/plugin-transform-runtimeを使う |
大幅に変更されたのがuseBuiltIns
オプションの挙動です。6.xではまずbabel-polyfillを入れた上で「それをどのようにcore-jsに置き換えるか」をuseBuiltInsオプションで指定する形でした。
対して7.xではuseBuiltIns
オプションに応じて@babel/polyfillを入れたり入れなかったりします。
ではそれぞれの方法の詳細を見ていきます。
1. 必要なpolyfillだけを入れる方法
@babel/preset-envのuseBuiltIns
オプションをusage
とすると、@babel/polyfillをソース内でimportせずとも必要なpolyfillだけを自動で選別して入れてくれます。ただし、@babel/polyfillをnpm installしておく必要はあります。
@babel/polyfill, @babel/preset-envをインストールする
$ npm install -D @babel/preset-env
$ npm install -S @babel/polyfill
.babelrcを記述する
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage"
}
]
]
}
この方法はメリットが大きいですが、公式ドキュメントにexperimentalと記されている点を忘れてはいけません。
試しにpolyfillが必要な実装例をいくつか試してみたところ、polyfillが入らないケースがありました。分かりやすかった例をピックアップしてみます。
obj = {};
let letObj = {};
const constObj = {};
obj['includes']; // array.includes と string.includes のpolyfillが入る
letObj['includes']; // polyfillが入らない
constObj['includes']; // polyfillが入らない
// --------
const getName = () => 'includes';
'str'[getName()]; // string.includes のpolyfillが入らない
静的解析してimportするpolyfillを特定している感じでしょうか。
スコープが不明確だと中身を特定しきれないので、使われる可能性のあるpolyfillを入れていますね。また、関数は実行しないと結果を判断できないようです。この感じだと、個人的にはproduction利用はまだちょっと怖い…と思ってしまいます。
ちなみに「importされているか怪しいpolyfillを個別に手動でimportする」案も考えたのですが、手動で入れたものとの重複判定はしてくれないようでしたので、この案もダメそうですね😳
2. 全てのpolyfillを入れる方法
Babel6.xまでと同じように@babel/polyfillをimport/requireする方法です。合わせてuseBuiltIns
オプションにentry
かfalse(default)
を指定することができます。
@babel/polyfill, @babel/preset-envをインストールする
$ npm install -D @babel/preset-env
$ npm install -S @babel/polyfill
エントリーポイントでpolyfillを読み込む
// index.js
import '@babel/polyfill';
Promise.resolve(); // polyfillが必要な実装
.babelrcを記述する
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "entry" // 任意
}
]
]
}
useBuiltIns
にentry
を指定すると、@babel/polyfillのimportを生のcore-jsのimportに置換してくれます。また、今まで通り同じpolyfillを複数回importするのはNGなのでその点もお忘れなく。
3. グローバル汚染せずにpolyfillを適用する方法
Babel6.xまでではbabel-plugin-transform-runtime
を使っていましたが、7.xからは2つのmoduleを使います。
- @babel/runtime … ソースにバンドルされるpolyfill本体
- @babel/plugin-transform-runtime … polyfillが必要な箇所を@babel/runtimeに置き換えてくれるもの
@babel/runtime, @babel/plugin-transform-runtimeをインストールする
$ npm install -S @babel/runtime
$ npm install -D @babel/plugin-transform-runtime
.babelrcを記述する
{
"plugins": ["@babel/plugin-transform-runtime"]
}
利用の際は、Babel6.xまでと同様にインスタンスメソッドは使えないということに注意する必要があります。
useBuiltInsのコードリーディング
ここからは余談です。
useBuiltInsオプションがどのように実装されているか気になったので、軽くコードリーディングしてみました。そのときのメモをまとめておきます。
babel-preset-env/src/index.js#L285
useBuiltIns
の値に応じて利用するプラグインを分岐しています。
- entry設定時 babel/use-built-ins-entry-plugin.js
- usage設定時 babel/use-built-ins-plugin.js
これらのプラグインは、どちらも以下のようなPluginオブジェクトを返す関数を実装しています。
export default function({ types: t }: { types: Object }): Plugin;
type Plugin = {
visitor: Object, // polyfill の import を解決するための処理群
pre: Function, // メイン処理前に実行すること
name: string // pluginの識別子のようなもの
}
Plugin.visitorが大きく差が出る部分ですね。
babel-preset-env/src/use-built-ins-entry-plugin.js#L31
Plugin.visitorに渡されるisPolyfillImport
オブジェクトを見ると、ImportDeclaration
関数に「@babel/polyfillがimportされてるならフラグをたてて、それを必要なmoduleのimportにreplaceしていく」といった実装があります。
babel-preset-env/src/use-built-ins-plugin.js#L68
Plugin.visitorに渡されるaddAndRemovePolyfillImports
オブジェクトを見ると、「@babel/polyfillがimportされていないことを確認するImportDeclaration
関数」と、「個々のpolyfillのimportするための関数」が存在します。後者については、ソースを解析して実装方法に応じて呼び出す関数を使い分けている感じですかね。
個々のpolyfillをimportするための関数は、最終的にutilsのcreateImport
関数を呼び出し、その中で更に@babel/helper-module-importsに処理を委譲しています。この@babel/helper-module-importsがpolyfillを個別にimportする本体ですね。
@babel/helper-module-imports
src以下には3つのファイルがあります。
index.js
外部から呼び出されるpublicな関数。処理の実態は以下の2つに委譲。
import-injector.js
polyfillの注入を担うclass。1つのpolyfillにつき1インスタンスを生成し、polyfillの性質(クラスメソッドなのか、インスタンスメソッドなのか、等)に応じた方法で注入する処理を呼び出す。
import-bulder.js
上述の「注入する処理」の本体。慎ましいBuilderパターンで実装されている。
以上、ざっくりとしたコードリーディングでした。@babel/coreのほうまではちゃんと読んでないので誤りがあるかもしれませんが、なんとなくuseBuiltIns
による挙動の違いを想定できたので良しとします。
あとがき
今回の記事はBabel7.1.0のドキュメントとソースを参考にしています。今後また仕様が変わることもあると思うので、記事内に古い情報や誤りを見つけた際は@aloerina_までご連絡いただければと思います。