バージョン情報
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
を作成します。
classUser{constructor(name){this.name=name}sayHello(){return`Hello, ${this.name}`}asyncfetch(){returnawaitnewPromise(resolve=>{setTimeout(()=>{resolve('complete!!')},1000)})}}constuser=newUser('sasaki')console.log(user.sayHello())user.fetch().then(result=>{console.log(result)})
上記コードは
- クラス構文
- テンプレート文字列
- Promise
- async/await
と言った、現代のフロントエンドでは定番だけど、依然としてブラウザ互換に悩まされる要素を詰め込んだコードになっています。
babelを実行してみる
まずは何も考えずにscript.js
をbabelにかけてみましょう。
$ babel script.js
すると以下のような出力が得られます。
classUser{constructor(name){this.name=name;}sayHello(){return`Hello, ${this.name}`;}asyncfetch(){returnawaitnewPromise(resolve=>{setTimeout(()=>{resolve('complete!!');},1000);});}}constuser=newUser('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.json
に browserslist
というフィールドを以下のように追加します。これはこのパッケージがどのブラウザをサポートしているかを明記する方法になります。
"browserslist":["> 2%"]
(.babelrc
にまとめて書いたり、.browserslistrc
を定義する方法もありますが、本記事では便宜上 package.json
に分離して書きます)
まずは上記のように、シェア率2%以上のブラウザを対象とする設定にして、もう一度トランスパイルしてみましょう。
$ babel script.js
"use strict";classUser{constructor(name){this.name=name;}sayHello(){return`Hello, ${this.name}`;}asyncfetch(){returnawaitnewPromise(resolve=>{setTimeout(()=>{resolve('complete!!');},1000);});}}constuser=newUser('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";functionasyncGeneratorStep(gen,resolve,reject,_next,_throw,key,arg){try{varinfo=gen[key](arg);varvalue=info.value;}catch(error){reject(error);return;}if(info.done){resolve(value);}else{Promise.resolve(value).then(_next,_throw);}}function_asyncToGenerator(fn){returnfunction(){varself=this,args=arguments;returnnewPromise(function(resolve,reject){vargen=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(!(instanceinstanceofConstructor)){thrownewTypeError("Cannot call a class as a function");}}function_defineProperties(target,props){for(vari=0;i<props.length;i++){vardescriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if("value"indescriptor)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);returnConstructor;}varUser=/*#__PURE__*/function(){functionUser(name){_classCallCheck(this,User);this.name=name;}_createClass(User,[{key:"sayHello",value:functionsayHello(){return"Hello, ".concat(this.name);}},{key:"fetch",value:function(){var_fetch=_asyncToGenerator(/*#__PURE__*/regeneratorRuntime.mark(function_callee(){returnregeneratorRuntime.wrap(function_callee$(_context){while(1){switch(_context.prev=_context.next){case0:_context.next=2;returnnewPromise(function(resolve){setTimeout(function(){resolve('complete!!');},1000);});case2:return_context.abrupt("return",_context.sent);case3:case"end":return_context.stop();}}},_callee);}));functionfetch(){return_fetch.apply(this,arguments);}returnfetch;}()}]);returnUser;}();varuser=newUser('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";functionasyncGeneratorStep(gen,resolve,reject,_next,_throw,key,arg){try{varinfo=gen[key](arg);varvalue=info.value;}catch(error){reject(error);return;}if(info.done){resolve(value);}else{Promise.resolve(value).then(_next,_throw);}}function_asyncToGenerator(fn){returnfunction(){varself=this,args=arguments;returnnewPromise(function(resolve,reject){vargen=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(!(instanceinstanceofConstructor)){thrownewTypeError("Cannot call a class as a function");}}function_defineProperties(target,props){for(vari=0;i<props.length;i++){vardescriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if("value"indescriptor)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);returnConstructor;}varUser=/*#__PURE__*/function(){functionUser(name){_classCallCheck(this,User);this.name=name;}_createClass(User,[{key:"sayHello",value:functionsayHello(){return"Hello, ".concat(this.name);}},{key:"fetch",value:function(){var_fetch=_asyncToGenerator(/*#__PURE__*/regeneratorRuntime.mark(function_callee(){returnregeneratorRuntime.wrap(function_callee$(_context){while(1){switch(_context.prev=_context.next){case0:_context.next=2;returnnewPromise(function(resolve){setTimeout(function(){resolve('complete!!');},1000);});case2:return_context.abrupt("return",_context.sent);case3:case"end":return_context.stop();}}},_callee);}));functionfetch(){return_fetch.apply(this,arguments);}returnfetch;}()}]);returnUser;}();varuser=newUser('sasaki');console.log(user.sayHello());user.fetch().then(function(result){console.log(result);});
トランスパイルの結果も、シェア率1%以上のときとまったく同じです。つまるところ、ここまで実行してきたトランスパイルは全てIE11のためのものだったのです。
IEでPromiseが動くわけ無いだろ
ここまでトランスパイルされた結果の中に、以下のようなコードが混じっていました。
switch(_context.prev=_context.next){case0:_context.next=2;returnnewPromise(function(resolve){setTimeout(function(){resolve('complete!!');},1000);});
new Promise
...??
この美しき縦ビンゴを見ても明らかなように、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
オプションを指定し、corejs
は3.x
を使うようにします。
{"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()')
"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に入ってなさそうなのに何でだろ)
"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();
// (省略)
当たり前ですが、core-js
をimportしていなかったら何も起こらないのでご注意ください。
// import 'core-js/stable'Promise.resolve()
"use strict";Promise.resolve();
結論
IEのサポートを切れるなら切ろう