为什么需要polyfill
我们写代码时可能会使用很多较新的API,但是由于用户的浏览器版本可能比较低,无法支持新的API或者语法,那么就需要Webpack将ES6+的代码降级,转换成在低版本浏览器上也可以正常运行的代码。
Babel插件就是用来干这个的。但是@babel/preset-env默认只转换ES6+的语法,比如:
let、const、箭头函数等等
但是Api层面是babel/preset-env无法转换的:
内置对象(Promise,Class,Map,Symbol...)、实例方法(Array.find, Object.assign...),
我们来写个代码看一下。
待转换的代码index.js如下:
const a = 1;
let b = 2;
const c = new Promise();
const d = new Map();
const e = [1, 2, 3, 4, 5].some((num) => num === 4);
console.log(a, b, c, d, e);
package.json如下:
{
"name": "0.test",
"mode": "development",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.17.5",
"@babel/preset-env": "^7.16.4",
"babel-loader": "^8.2.3",
"html-webpack-plugin": "^5.5.0",
"webpack": "^5.64.2",
"webpack-cli": "^4.9.1"
},
"dependencies": {
"core-js": "^3.19.1"
}
}
webpack.config.js:
module.exports = {
mode: 'development',
devtool: false,
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
//配置需要支持的浏览器版本
targets: {
chrome: '90',
ie: '6',
},
},
],
],
},
},
],
},
],
},
};
转换后的代码
(() => {
var __webpack_exports__ = {};
var a = 1;
var b = 2;
var c = new Promise();
var d = new Map();
var e = [1, 2, 3, 4, 5].some(function (num) {
return num === 4;
});
console.log(a, b, c, d, e);
})();
我们可以看到,
- let,const都转换成了var
- Promise Map并没有转换
- Array.some方法也没有转换
所以,仅仅依靠@babel/preset-env并不能完全的将代码降级。那么是什么来完成对内置对象、某些实例方法的转换呢?
答案是 polyfill
什么是polyfill?
那么polyfill是什么呢?你可以理解为降级/替代方案,在这里我们指可以将ES6+的API转换为,在低版本浏览器上可以实现相同功能的替代实现。比如如果浏览器不支持Promise,那我们可以用setTimeout来替代实现。
Babel中的polyfill,是通过corejs这个库来实现的。
⬇️node_modules中的corejs
如何设置polyfill?
那么如何设置polyfill呢?
设置polyfill有两种办法:
- 还是使用@babel/preset-env, 但是要对useBuiltIns进行配置.
- 新的babel插件:@babel/plugin-transform-runtime
useBuiltIns
配置了useBuiltIns后,可以让@babel/preset-env引入corejs的能力,对 ES6+的API实现降级替代。useBuiltIns有三个可配置的值:
- false:不论要兼容的浏览器版本,默认引入全量的polyfill,引入量很大,有几百Kb
- 'entry':根据配置的要兼容的浏览器的版本,引入对应的polyfill.但是不会根据代码使用情况来引入。
- ‘usage’:根据配置的要兼容的浏览器版本和代码使用情况,使用哪些引入哪些polyfill。比如使用了Promise,没有使用Map,那么就只引入Promise,不引入Map。
@babel/proset-env实现polyfill的原理是,增加全局对象(Promise)或者在原型链上(Array.prototype.some)增加方法,这就造成了全局污染。
在我们的项目中,除了Babel外,可能引入多个第三方模块,如果其他模块也实现了同样的方法,那么就会造成冲突。比如两个第三方模块都实现了 Array.prototype.some,那么就不知道该用谁的,造成了冲突。
如果想要避免冲突,我们就要用导入的方法来引入polyfill,而不是修改原型链或者全局对象的方法。
//如果useBuiltIns实现polyfill
//全局定义Promise,再使用
window.Promise = function(){
...
}
//手动引入
//某个文件内引入,不会影响到其他地方
var Promise = require("./node_modules/@babel/runtime-corejs3/core-js-stable/promise.js")
使用@babel/plugin-transform-runtime插件实现polyfill
但是如果每个都要手动引入的话,又会非常麻烦,所以@babel/plugin-transform-runtime插件就出现了。它可以帮你引入当前文件需要的polyfill.
index.js:
Promise.resolve(1).then((data) => {
let a = data;
console.log(a);
});
webpack.config.js:
module.exports = {
mode: 'development',
devtool: false,
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
targets: {
browsers: ['IE 10'],
},
presets: [
[
'@babel/preset-env',
{
useBuiltIns: false,
},
],
],
plugins: [
[
'@babel/plugin-transform-runtime',
{
corejs: 3,
helpers: true,
regenerator: true, //不污染全局作用域
},
],
],
},
},
],
},
],
},
};
打包出来的内容大概是:
(() => {
"use strict";
__webpack_require__.r(__webpack_exports__);
var _babel_runtime_corejs3_core_js_stable_promise__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( "./node_modules/@babel/runtime-corejs3/core-js-stable/promise.js");
var _babel_runtime_corejs3_core_js_stable_promise__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_corejs3_core_js_stable_promise__WEBPACK_IMPORTED_MODULE_0__);
_babel_runtime_corejs3_core_js_stable_promise__WEBPACK_IMPORTED_MODULE_0___default().resolve(1).then(function (data) {
var a = data;
console.log(a);
});
})()
大意是:引用corejs中 Promise的polyfill( ( "./node_modules/@babel/runtime-corejs3/core-js-stable/promise.js"); ),然后调用该Promise去执行代码。
最佳实践
如果是写项目,推荐以下配置
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
devtool: false,
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
targets: {
"browsers": ["IE 10"]
},
presets: [
["@babel/preset-env", {
useBuiltIns: 'usage',
corejs: { version: 3 }
}]
],
plugins: [
["@babel/plugin-transform-runtime", {
corejs: false,
helpers: true,
regenerator: false
}]
]
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
};
// useBuiltIns: false 400 KiB 把polyfill全量引入,不考虑浏览器兼容性
如果是业务项目开发,不会有别人引用该项目,所以可以useBuiltIns:‘usage’方式,按照浏览器兼容需求和实际使用情况来按需引入polyfill,污染了全局,但是节省了空间,同时借用"@babel/plugin-transform-runtime"的helper辅助函数,进一步减少体积。
如果是写类库代码,推荐如下配置:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
devtool: false,
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
targets: {
"browsers": ["IE 10"]
},
presets: [
//@babel/preset-env只转换语法,不要提供polyfill
["@babel/preset-env", {
useBuiltIns: false
}]
],
plugins: [
["@babel/plugin-transform-runtime", {
corejs: { version: 3 },//不污染全局作用域
helpers: true,
regenerator: true //不污染全局作用域
}]
]
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
};
// useBuiltIns: false 400 KiB 把polyfill全量引入,不考虑浏览器兼容性
如果是类库代码,比如vue,moment,那么必然会给别人引用,所以不能污染全局,采用useBuiltIns: false的方式,让preset-e只转换语法,不转换API,不通过污染全局引入polyfill。使用"@babel/plugin-transform-runtime"插件,在每个文件内按需引入需要的polyfill,使用helpers辅助函数减少体积,同时重新生成generator,防止生成window.generator造成全局污染。