Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9232

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

$
0
0

バージョン情報

version
node12.16.3
@babel/core7.9.0
@babel/preset-env7.9.5
@babel/cli7.8.4
browserslist4.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を作成します。

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を使うように指定します。

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

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

package.json
"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を実行してみます。

package.json
"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%以上に引き下げてみましょう。

package.json
"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%以上に引き下げただけですが、IEedgefirefoxが入ってきたり、旧バージョンの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の定義を以下のように変えてみましょう。

package.json
"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を一切サポートしていません。
スクリーンショット 2020-05-07 23.18.19.png
(引用)

なのに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だけをサポートする設定にしちゃいます。

package.json
"browserslist":["IE 11"]
$ yarn browserslist
ie 11

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

script.js
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文を、対象ブラウザが必要とするモジュールに置き換える
falsePolyfillしない(デフォルト)

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

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

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

useBuiltIns: "usage"

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

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

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

Chromeが対象の場合
"use strict";Promise.resolve();

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

IEが対象の場合
"use strict";require("core-js/modules/es.object.to-string");require("core-js/modules/es.promise");Promise.resolve();

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

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

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

useBuiltIns: "entry"

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

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

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

chrome81の場合1個のみ
"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していなかったら何も起こらないのでご注意ください。

script.js
// import 'core-js/stable'Promise.resolve()
IE11を対象にしても何もPolyfillされない
"use strict";Promise.resolve();

結論

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


Viewing all articles
Browse latest Browse all 9232