你知道webpack怎么配置polyfill才是最优的吗?——webpack的常用优化(1)

2,613 阅读2分钟

为什么需要polyfill

我们写代码时可能会使用很多较新的API,但是由于用户的浏览器版本可能比较低,无法支持新的API或者语法,那么就需要Webpack将ES6+的代码降级,转换成在低版本浏览器上也可以正常运行的代码。

Babel插件就是用来干这个的。但是@babel/preset-env默认只转换ES6+的语法,比如:

letconst、箭头函数等等

但是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

image.png


如何设置polyfill?

那么如何设置polyfill呢?

设置polyfill有两种办法:

  1. 还是使用@babel/preset-env, 但是要对useBuiltIns进行配置.
  2. 新的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造成全局污染。