はじめに
JavaScriptにおけるpolyfill(ポリフィル)とは、例えば Promise
オブジェクトが使えないIE11でPromiseを使えるようにするなど、使いたいAPI(オブジェクト, メソッド等)がない環境にそれらを使えるようにする役目を果たすライブラリ及びその機構のことです。
polyfillをしたことの無い人にとっては、どのライブラリがどんな役目を持っていて、自分が望むpolyfillを実現するためのconfigの構築は難しいものと思っています。
そこで、polyfillを実現する複数の方法において、どのようなケースでどのような機構を使ってどのようなconfigを作れば良いのか、以下書き連ねておきます。
想定読者
- babelを噛ませてpolyfillさせたい人
- core-jsを直接読み込ませてpolyfillさせたい人
- swcを使ってpolyfillさせたい人
polyfillを実現するためのconfig一覧
以下の私のリポジトリで検証したものをピックアップしていきます。
babelを使ってpolyfillを実現する
まずはbabel(ES2015以降のJavaScriptをES5に変換するツールおよびそのライブラリ群)を通してpolyfillを実現する方法です。
ES2015以降のJavaScriptをメインのコードベースとして構築している環境には、babelを使っている環境もそれなりにあるのではないでしょうか。
babelを通してpolyfillを注入する方法として、主に三つの方法があります。
- @babel/preset-env を使う方法
- @babel/plugin-transform-runtime を使う方法
- babel-plugin-polyfill-corejs3 を使う方法
どれを選べば良いのか。私の考えとしては以下のとおりです。
- ライブラリ作者であれば、
babel-plugin-polyfill-corejs3
を使って、usage-pure
の設定でpolyfillする - ライブラリではないアプリケーションの開発者であれば、
@babel/preset-env
を使って、entry
もしくはusage
の設定でpolyfillする
ライブラリ作者であれば、 babel-plugin-polyfill-corejs3
を使って、usage-pure
の設定でpolyfillする
babel-plugin-polyfill-corejs3
というbabelプラグインを使ってpolyfillする方法です。
webpackを利用している場合の、babel-loader込みの設定例は次の通りです。
(babel-loader
の他、@babel/core
、babel-plugin-polyfill-corejs3
、core-js
がpackage.jsonに追加されている必要があります)
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist/usage-pure-corejs3"),
},
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
targets: {
ie: "11",
},
plugins: [
["polyfill-corejs3", { method: "usage-pure", version: "3.23" }],
],
},
},
},
],
},
};
@babel/plugin-transform-runtime
を使ってpolyfillする場合には、targets(どの実行環境に向けてトランスパイルするか)に関わらず全てのpolyfill可能な対象がpolyfillされてしまいますが、この babel-plugin-polyfill-corejs3
プラグインを利用した場合には、targetsを見てpolyfill可能です。
上のリンクを見れば分かる通り、 babel-plugin-polyfill-corejs3
には複数のpolyfill設定が可能となっています。
-
entry-global
- targetsを見て、targetsにおいて不足しているpolyfillを注入するモード。
- polyfillの副作用がglobalに及ぶ。
-
usage-global
- targetsだけじゃなく、実際に使用しているコードを見て、必要なだけのpolyfillを注入するモード。
- polyfillの副作用がglobalに及ぶ。
-
usage-pure
- targetsだけじゃなく、実際に使用しているコードを見て、必要なだけのpolyfillを注入するモード。
- polyfillの副作用がglobalに及ばず、
Promise
などの呼び出しが独自の関数呼び出しに置き換えることでpolyfillを実現している。
先のwebpackのコード例は、先述の usage-pure
の例です。
ライブラリという様式上、その副作用をライブラリ使用側に及ぼさないためにはusage-pure
を利用するのが適しているのではないでしょうか。
なお、babel-plugin-polyfill-corejs3
プラグインのコアはcore-js(JavaScriptでpolyfillを行うコアライブラリの一つ。)に依存しているので、core-jsをいい感じにハンドリングしてコードが動く環境の設定に応じてpolyfillしてくれるやつ・・・という認識で良いと思います。
core-jsについて詳しく知りたければ次のリンクからどうぞ。
ライブラリではないアプリケーションの開発者であれば、 @babel/preset-env
を使って、 entry
もしくは usage
の設定でpolyfillする
@babel/preset-env
というbabelプリセットを使ってpolyfillする方法です。
こちらも先のplugin同様、core-jsをいい感じにハンドリングしてpolyfillを行ってくれる機能を備えています。
webpackを利用している場合の、babel-loader込みの設定例は次の通りです。(usage
の場合)
(babel-loader
の他、@babel/core
、@babel/preset-env
、core-js
がpackage.jsonに追加されている必要があります)
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist/usage"),
},
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
targets: {
ie: "11",
},
presets: [
[
"@babel/preset-env",
{
useBuiltIns: "usage",
corejs: "3.21",
},
],
],
},
},
},
],
},
};
useBuiltIns: "usage"
となっていますが、これはアプリケーションの利用箇所に応じてpolyfillの注入をハンドリングしてくれる設定です。
webpack等を使い、polyfillの注入対象をアプリケーションコードと別のチャンクに分離するなど、polyfillによるコードサイズの最適化を図る場合には、entry
を使ってまとめて分離すれば良いですが、とりあえずpolyfillできればいいや、という場合は、usage
の設定で十分事足りると思います。
先述のいずれのpolyfillプラグインを使う場合でも、babelがpolyfillとは関係なく生成するヘルパー関数は各コードに分散したままになるので、それらのヘルパー関数をまとめたい場合には別途@babel/plugin-transform-runtime
を使う必要がありますが、そこまで最適化を考えていない場合には先述のwebpack.config.jsの設定だけでも問題ないでしょう。
(@babel/plugin-transform-runtimeは、polyfillの文脈とヘルパー関数の集約の文脈の2つの文脈で出てくるので注意が必要です。今言及したのは後者の文脈です。)
core-jsを直接使ってpolyfillを実現する
次にご紹介するのは、core-jsを直接使ってpolyfillを実現する方法です。
tsc等、babelを通さない環境で使うこともあるでしょうか。
直接使う場合のコード例は以下の通り、core-jsをどこかのファイルでインポートするだけです。
これだけでglobalに副作用をもたらす形としてpolyfillが注入されます。
import "core-js/stable";
もし、上記のように1ファイルにcore-jsの関心を分離させる場合には、その後webpack等のバンドラで結合させるか、もしくは独立したチャンクとして実行環境側で読み込めばいいかと思います。
webpackで結合させる場合のコード例も載せておきます。
const path = require("path");
module.exports = {
entry: [
"./src/polyfill-stable.js",
"./src/index.js"
],
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist/polyfill-core-js-stable"),
}
};
この場合、stableなcore-jsの機能が副作用を起こして注入されるので、babelのようにtargetsに応じて・・・というものはありません。
いたってシンプルです。
したがって、末端のアプリケーションで利用する & JavaScriptコードのサイズを気にしないのであればこれでもいいかもしれません。
ライブラリ作者であれば、core-jsを直接バンドルして配布するのはサイズが大きくなるし副作用ももたらすので適していないのではないでしょうか。
なお、バンドラすら使いたくないという人のために、CDNからも直接利用できるようです。
(CDN経由では使ったことないですが)
swcを使ってpolyfillを実現する
最後にご紹介するのは、swcを通してバンドルする際にpolyfillも行うやり方です。babelと似たようなものと思ってくれていいのではないでしょうか。
webpackを利用している場合の、swc-loader込みの設定例は次の通りです。
(swc-loader
の他、@swc/core
、core-js
がpackage.jsonに追加されている必要があります)
import "core-js/stable";
const path = require("path");
module.exports = {
entry: [
"./src/polyfill.js",
"./src/index.js"
],
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist/polyfill-swc-with-ie-11"),
},
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules)/,
use: {
loader: "swc-loader",
options: {
"env": {
"targets": {
"ie": "11"
},
"mode": "entry",
"coreJs": 3
}
}
}
}
]
}
};
2022年8月現在、babelのpreset-env同様に、usage
と entry
の二通りが使えるようですが、
The usage mode is currently not as efficient as Babel, yet.
の記載があるので、こちらは entry
モードでのみ使用すべきでしょう。
usage
と entry
の使い分け・違いについてはbabelの欄をご覧ください。
babelよりも高速と謳われているので、babelと同様の機能性・安定性があると判断できたらこちらを使ってもいいのではないでしょうか。
他参考にすべきサイト
もっとpolyfillに関して詳しく知りたければ、
ライブラリ作者におすすめしたいBabelの新機能 babel-plugin-polyfill-corejs3 を一読してみてはどうでしょうか。すごく詳しく解説されています。(書いているのは僕ではないのでご注意を)
おわりに
IEがサポート切れたからといって、JavaScriptのproposalな機能を使ったりする場合にはpolyfillのことを考えなければいけません。
今作っているアプリケーションはどのような実行環境であれば動かせることができるのか、そしてpolyfillは必要なのかどうか・・・アプリケーションを構築する必要は今までもこれからも気にしなければいけないでしょう。