@babel/plugin-transform-runtime を理解する(Babel 7)
概要
Babelの主要プラグインの一つである、@babel/plugin-transform-runtime に関して、雰囲気で使わずに理解を深めるためにまとめた内容です。
概ね上記公式ドキュメントに書いてある内容を要約しただけになります。
バージョン情報
以下で動作確認済み
-
Node
v12.16.3 -
@babel/core
v7.4.5 -
@babel/plugin-transform-runtime
v7.4.5 -
@babel/runtime@
v7.9.2 -
core-js
v3.x
@babel/plugin-transform-runtime
is 何
本来はBabelによって注入されるヘルパーコードを、再利用可能なヘルパーをインポートする形に変換するプラグインです。これによってグローバル汚染を避けながら、成果物のコードサイズを小さくすることができます。
ES6+のインスタンスメソッドの使用には
core-js@3
が別途必要です。@babel/preset-env
のuseBuiltIns
を設定するのが手っ取り早いですがここでは割愛します
インストール
@babel/plugin-transform-runtime
はコードビルド時に必要なプラグインになるので、devDependencyで充分です。
$ yarn add -D @babel/plugin-transform-runtime
ビルド後のコードから参照されるヘルパー本体が別途必要になります。こちらは実行用なのでproductionDependencyになります。
$ yarn add @babel/runtime
設定
.babelrc
などの設定ファイル内に使用するプラグインを追加するだけ。オプションも豊富ですが、基本的にはデフォルトで問題無さそう。
{
"plugins": ["@babel/plugin-transform-runtime"]
}
なぜ必要なのか
Babelは通常、一般的によく使われる関数(Class生成など)に対して、都度ヘルパー関数を生成しますが、アプリケーションのファイルが分散している場合、ヘルパーを必要とするそれぞれに対して同様のコードを複製してしまいます。
そうすると複製したコードが溜まっていき、全体のファイルサイズが大きくなってしまい、アプリケーションの初期読み込み時間が長くなる問題に繋がってしまいます。
そこで @babel/plugin-transform-runtime
を用いると、ヘルパーを必要とするそれぞれのファイルがヘルパー関数を定義するのでなく、@babel/runtime
を参照するように変更されます。必要な関数は全て @babel/runtime
に配置してあるので、コードがどれだけ分散していても全体のファイルサイズに影響を与えません。
また、@babel/plugin-transform-runtime
を使わずに、babel/polyfill
などを直接インポートして変換すると、グローバル汚染が発生し、そのコードを外部ライブラリとした他のコードに影響を与える可能性があります。 (逆に言えばアプリケーションやコマンドラインツールであれば大した問題にはならない)
CLIで挙動を見る
実際の挙動をCLIで確認するとイメージが湧くので、まずbabel-cli
と、preset-env
、core-js
も用意しておきます。
$ yarn add @babel/core
$ yarn add -D @babel/cli @babel/preset-env core-js@3
babelの設定は必要最低限に。とりあえず IE11 をサポートするようにしておけばだいたいのコードは polyfill されます。
{
"presets": [
["@babel/preset-env", {
"modules": false,
"targets": {
"browsers": "> 1%",
"ie": 11
},
"corejs": {
"version": 3,
"proposals": false
},
"useBuiltIns": "usage"
}]
],
"plugins": [
]
}
検証対象のスクリプトは以下のような、クラスを通じて文字列を出力する程度のものになります。
class Human {
constructor(name) {
this.name = name
}
sayHello() {
return `Hello, ${this.name}`
}
}
const human = new Human('sasaki')
console.log(human.sayHello())
babel-cliを実行すると、es5に変換されます。
$ yarn babel script.js
import "core-js/modules/es.function.name";
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 Human = /*#__PURE__*/function () {
function Human(name) {
_classCallCheck(this, Human);
this.name = name;
}
_createClass(Human, [{
key: "sayHello",
value: function sayHello() {
return "Hello, ".concat(this.name);
}
}]);
return Human;
}();
var human = new Human('sasaki');
console.log(human.sayHello());
特筆すべきは、 _classCallCheck
_defineProperties
_createClass
と言った、ES5でES6+相当と同等の仕組みを提供するためにbabel(正確にはbabelプラグイン)が生成したヘルパー関数です。
では次に、 @babel/plugin-transform-runtime
を有効にしてみます。
$ yarn babel --plugins @babel/plugin-transform-runtime script.js
import "core-js/modules/es.function.name";
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import _createClass from "@babel/runtime/helpers/createClass";
var Human = /*#__PURE__*/function () {
function Human(name) {
_classCallCheck(this, Human);
this.name = name;
}
_createClass(Human, [{
key: "sayHello",
value: function sayHello() {
return "Hello, ".concat(this.name);
}
}]);
return Human;
}();
var human = new Human('sasaki');
console.log(human.sayHello());
おや? 先程まで直接生成されていたヘルパーコードが、
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";`
のように、他のモジュールを参照するように振る舞いが変わっていますね。
ではこの @babel/runtime/helpers/classCallCheck
を見てみましょう。
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
module.exports = _classCallCheck;
本来は動的に生成されていたコードがこちらに配置されてることが確認できました。
これが @babel/plugin-transform-runtime
のほかに @babel/runtime
を、それもProductionに入れる必要があるのはこういうワケでした。
Discussion