babel的基本用法、按需polyfill

2,112 阅读6分钟

搭建测试环境

在根目录下npm init -y,生成基本的package.json,创建src,并在src创建index.js文件,用于等会balel要编译的文件。然后在根目录创建.babelrc文件,作为babel的配置文件。

安装依赖

按照官网的流程,安装babel的一些依赖模块

npm install --save-dev @babel/core @babel/cli @babel/preset-env
npm install --save @babel/polyfill

配置.babelrc

babel编译会自动读取更目录下的.babelrc文件去读取配置

{
  "presets": [
    [
      "@babel/env",
      {
        "targets": {
          "chrome": "43"
        },
        "useBuiltIns": "usage",
        "corejs": {
          "version": 2
        }
      }
    ]
  ],
  "plugins": []
}

配置文件里可能会有preset、plugins这两个字段,简单介绍下:

  • plugins:用于将代码由高版本语法转为低版本插件集合,如:@babel/plugin-transform-arrow-functions,可以将箭头函数转换为es5。

  • preset:由于我们一个项目中可能会使用到很多bable插件,如果每次都要一个一个添加,很是麻烦。为了解决这个问题,babel提供了插件组合,也就是perset。

测试es6语法

let a = 1;
const foo = () => {
    console.log("箭头函数")
}

运行下面命令

// 编译src下的文件并输出到lib文件夹下
npx babel src --out-dir lib

// 编译结果
"use strict";

var a = 1;

var foo = function foo() {
  console.log("箭头函数");
};

可以看出已经被打包成了es5语法。

测试高版本api

includes是es7新出的api,测试下会被打包成什么?

let arr = [1, 2, 3];
console.log(arr.includes(1))

打包完后

"use strict";

require("core-js/modules/es7.array.includes");

var arr = [1, 2, 3];
console.log(arr.includes(1));

打包完成发现会新增一个require,这个文件会重写Array的includes方法,使用低版本语法实现es7新增的includes方法。

babel-polyfill

其实上面的高版本语法并不是由babel进行转换的,而是通过babel-polyfill来进行转,说到这我们有必要好好讲讲babel-polyfill这个包了。先放上官网地址

先简单解释一下:babel默认只会转义js语法,但对于一些新的API是不会做转换的, 像includeArray.from等方法。babel-polyfill做的事情就是帮你兼容这些高版本语法。

组成babel-polyfill包含了core-jsregenerator-runtime这两个包。

core-js: 一些高版本语法转低版本就是由这个库来实现的。

regenerator-runtime:对async await提供转换的库。

但很奇怪🤔,我们并没有在项目中的任何地方引入babel-polyfill,怎么就生效了呢😳😳。其实这得益于我们在.babelrc中配置的预设@babel/env,这个具体是干嘛用的呢。

@babel/env

env是我们在babel中最常用的,env 的核心目的是通过配置得知目标环境的特点,然后只做必要的转换。例如目标浏览器支持 es2015,那么 es2015 这个 preset 其实是不需要的,于是代码就可以小一点(一般转化后的代码总是更长),构建时间也可以缩短一些。

如果不写任何配置项,env 等价于 latest,也等价于 es2015 + es2016 + es2017 三个相加(不包含 stage-x 中的插件)。env 包含的插件列表维护在这里

那我们现在来看下,为什么上面没有引入polyfill,它就直接生效了呢?不用想肯定也能猜到,是env这个预设去引入了。我们可以看下关于babel/env的里关于useBuiltIns的介绍

"usage" | "entry" | false, defaults to false.

This option configures how @babel/preset-env handles polyfills.

When either the usage or entry options are used, @babel-preset-env will add direct references to core-js modules as bare imports (or requires). This means core-js will be resolved relative to the file itself and needs to be accessible.

useBuiltIns = entry,需要在代码顶部去引一下polyfill

// 输入
import "@babel/polyfill"

let arr = [1, 2, 3];
console.log(arr.includes(1))

// 编译结果
"use strict";
...

require("core-js/modules/es6.array.copy-within.js");

require("core-js/modules/es6.array.fill.js");

require("core-js/modules/es7.array.includes.js");

...

var arr = [1, 2, 3];
console.log(arr.includes(1));

可以看到,这种模式下,babel 会将chrome 43不支持的所有的内容全部引入,这样会导致结果的包大小非常大,而我们这里仅仅需要 includes 一个方法而已。

useBuiltIns = usage,会帮我们按需加载,且不需要我们手动引入

// 输入
let arr = [1, 2, 3];
console.log(arr.includes(1))

// 编译结果
"use strict";

require("core-js/modules/es7.array.includes.js");

var arr = [1, 2, 3];
console.log(arr.includes(1));

这个好像就牛逼了,能分析到是否调用了Array.includs方法再去引用。但真的是这样的吗?

// 输入
const Foo = function () {};
Foo.prototype.includes = function () { };
new Foo().includes();

// 编译结果
"use strict";
require("core-js/modules/es7.array.includes.js");

var Foo = function Foo() {};
Foo.prototype.includes = function () {};
new Foo().includes();

可以看到我并没有调用Array.includes,但是它却还是引入了,所以它应该通过方法名来判断是否需要引入。

useBuiltIns = false,即代表不处理api。

弊端

但是上面两种方式有两个的弊端,

  1. 它们是通过直接修改全局构造函数的原型对象上的方法来实现polyfill,这样会造成全局污染,有可能会照成意想不到的bug。

  2. babel 转译时,有时候会使用一些辅助的函数来帮忙,比如:

    // 输入
    class Test {}
    
    // 编译结果
    "use strict";
    
    function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
    
    var Test = function Test() {
      _classCallCheck(this, Test);
    };
    

class 语法中,babel 自定义了 _classCallCheck这个函数来辅助,如果一个项目中有多个文件,其中每个文件都写了一个 class,那么这个项目最终打包的产物里就会存在100个 _classCallCheck 函数,这显然不合理。

如果想避免这两种情况,我们可以使用@babel/plugin-transform-runtime插件。

@babel/plugin-transform-runtime

该插件需要依赖@babel/runtime-corejs2@babel/runtime-corejs3这两个包,这两个包你可以理解成是对polyfill的实现

npm install @babel/runtime-corejs2 --save
# or
npm install @babel/runtime-corejs3 --save

假设有如下代码:

class Test {}
const set = new Set();
console.log([].includes)

.babelrc配置

{
  "presets": [
    [
      "@babel/env",
      {
        "targets": {
          "chrome": "43"
        },
        "useBuiltIns": "usage",
        "corejs": {
          "version": 2
        }
      }
    ]
  ],
  "plugins": []
}

打包结果:

"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");
var _set = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/set"));
class Test {}
const set = new _set.default();
console.log([].includes);

发现core-js2只会对Set进行了处理,但是includes没有被处理,因为core-js2会对代码中用到的类/静态方法进行处理,对原型链上的方法不会做处理。

现在把.babelrccorejs改成3,进行编译

"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _set = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/set"));
var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));
var Test = function Test() {
  (0, _classCallCheck2.default)(this, Test);
};
var set = new _set.default();
console.log((0, _includes.default)([]));

core-js3处理了Setincludes了,可以看出core-js3也会处理原型链上的方法。

通过上面,可以看出使用plugin-transform-runtime插件,解决了我们上面的两个问题

  1. 不是直接通过修改对象原型对象上的方法,而是从一个统一模块中引入,避免了对全局变量的污染。
  2. 辅助函数从一个统一模块引入,避免了代码中存在多份辅助函数代码。