babel 相关知识

作者: shaokang 时间: January 11, 2022字数:10191

前言

项目中都会用到 babel 插件,日常开发想必也会遇到一些编译相关的问题,所以这里借此时间整理一下 babel 相关的知识。

babel 是什么?

官方解释:Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

简而言之就是将 ES 代码转换编译保证能在旧版本浏览器上可运行。语言的发展速度相当快,现在发布了 ECMAScript 2022,然后对于浏览器来说没办法兼容这么高版本的语法,所以 babel 应运而生.

babel 工作原理

babel 的转译过程也分为三个阶段:parsing(解析)、transforming(转化)、generating(生成)。想必学过编译原理的同学都知道,语言的编译过程基本都是这样。

  • parsing 阶段 babel 内部的 babylon 负责将代码进行语法分析和词法分析后转换成抽象语法树 (AST)。
  • transforming 阶段内部的 babel-traverse 插件负责对抽象语法树(AST)进行遍历转移,得到新的语法树。
  • printing 阶段内部的 babel-generator 负责生成对应的代码

babel 工作内容

babel 将 es6+(指 es6 及以上版本)按语法层和 API 能力层来进行专一。
语法层包括 let、const、class、箭头函数等这些 ESNEXT 中的语法,这些语法需要在构建时进行转译,比如将 let、const 转义成 var 的过程。
API 层包括 Promise、includes、map 等,这些在 全局对象、Object、Array 等的原型上新增的方法,这就需要以 es5 的形式重新构建补充这些方法。

preset-env

preset-env 所做的就是语法层面的转义工作,主要的作用是用来转换那些已经被正式纳入TC39 中的语法。所以它无法对那些还在提案中的语法进行处理,对于处在 stage 中的语法,需要安装对应的 plugin 进行处理。

  • npm i @babel/preset-env -D 下载该插件。
  • 在 .babelrc 文件或者 babel.config.js 文件中添加配置。想用哪种配置文件根据需求自行判断,可参考 官方配置 Babel
{
	"presets": ["@babel/preset-env"]
}

polyfill

语法层面已经做了降级,但 API 层面怎么处理呢,这里就要用到 polyfill 了。

@babel/polyfill

@babel/polyfill 能够对 API 层面进行处理:

  • npm install –save @babel/polyfill 下载插件
  • @babel/polyfill 模块包括 core-js 和一个自定义的 regenerator runtime 模块,可以模拟完整的 ES2015+ 环境。只需要在入口文件 index.js 中引入即可。
// index.js
import '@babel/polyfill';
......;

转译结果:

"use strict";

require("@babel/polyfill");

......;

@babel/polyfill 是一个运行时包,主要是通过核心依赖 core-js@2 来完成对应浏览器不支持的新的全局和实例 API 的添加。在升级到 core-js@3 后,如果还要保留 @babel/polyfill 的使用,就要在 @babel/polyfill 中添加 core-js@2 和 core-js@3 切换的选项,这样 @babel/polyfill 中将包含 core-js@2 和 core-js@3 两个包,出于这个原因官方决定弃用 @babel/polyfill。

@babel/preset-env & polyfill

@babel/preset-env 其实也可以对 API 的进行处理,不过得在代码中引入 polyfill。

@babel/preset-env 主要是依赖 core-js 来处理 API 的兼容性,所以需要事先安装 core-js(regenerator-runtime 会在安装 @babel/preset-env 的时候自动安装)。这样就不需要安装 @babel/polyfill 了。

useBuiltIns 选项

@babel/preset-env 可以通过 useBuiltIns 字段来支持如何引入 polyfill。

1.usage

按需引入,babel 会检测代码中 ES6/7/8 等的使用情况,仅仅加载代码中用到的且 browserslist 环境不支持的 polyfills。

//.babelrc
{
   "presets": [
       ["@babel/preset-env", {
           "useBuiltIns": "usage",
           // 不指定 corejs 版本会有警告,推荐版本 corejs@3
           "corejs": 3
       }]
   ]
}

一般来说项目中都会配置 exclude 将 node_modules 给忽略掉,useBuiltIns: ‘usage’ 这种用法的话有个风险点就是 node_modules 当中的第三方包在实际的编译打包处理流程当中没有被处理。

2.entry

需要在代码运行之前导入,会将 browserslist 环境不支持的所有 polyfill 都导入。

import "core-js/stable";
import "regenerator-runtime/runtime";
......;
//.babelrc
{
   "presets": [
       ["@babel/preset-env", {
           "useBuiltIns": "entry",
           "targets": 'Chrome >= 54, iOS >= 10',
       }]
   ]
}

3.false

只做了语法转换,不会导入 polyfill。

@babel/runtime 和 @babel/plugin-transform-runtime

在使用 @babel/polyfill 或者 @babel/preset-env 提供的语法转换和全局 api 添加的功能时,难免会造成文件的体积增加以及 api 的全局污染。为了解决这类问题,引入了 runtime 的概念,runtime 的核心思想是以引入替换的方式来解决兼容性问题。

runtime 包其有三个:

  • @babel/runtime
  • @babel/runtime-corejs2
  • @babel/runtime-corejs3

@babel/runtime 与 @babel/runtime-corejs2 类似,区别只是 @babel/runtime-corejs2 使用了 core-js@2 来处理全局 api @babel/runtime-corejs2 和 @babel/runtime-corejs3 前者是从 core-js 中的 library 模块去加载对应的 runtime,后者从 core-js-pure 加载。@babel/runtime-corejs3 还能模拟实例上的 API。

解决代码冗余

看个例子

npm init -y 初始化一个项目 下载依赖: npm i @babel/cli @babel/core @babel/preset-env -D npm i core-js@3 -S

// .babelrc
{
   "presets": [
       ["@babel/preset-env", {
           "useBuiltIns": "usage",
           "corejs": 3
       }]
   ]
}
// index.js
class Student {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
}

执行 npx babel index.js –out-file compiled.js 编译后的文件

'use strict';

require('core-js/modules/es.object.define-property.js');

require('core-js/modules/es.function.name.js');

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);
    Object.defineProperty(Constructor, 'prototype', { writable: false });
    return Constructor;
}

function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
        throw new TypeError('Cannot call a class as a function');
    }
}

//index.js
var Student = /*#__PURE__*/ _createClass(function Student(name, age) {
    _classCallCheck(this, Student);

    this.name = name;
    this.age = age;
});

从编译后的文件能看出多了一些 _defineProperties、_createClass、_classCallCheck 辅助函数,如果每个文件都存在这些辅助函数的话,体积也会相应得增加,@babel/plugin-transform-runtime 能够将这些辅助函数进行复用以节省代码体积。

全局污染

polyfill 直接修改现有的全局对象,比如修改 Array、String 的原型链等。

new Promise(function (resolve, reject) {
    resolve(100);
});

[1, 3, 4].includes(1);

编译后

'use strict';

require('core-js/modules/es.object.to-string.js');

require('core-js/modules/es.promise.js');

require('core-js/modules/es.array.includes.js');

//index.js
new Promise(function (resolve, reject) {
    resolve(100);
});
[1, 2, 3].includes(1);

编译处理这种 API 时会引入了 core-js 中的相关的 js 库,这些库重新定义了某个类,如 Promise,然后将其挂载到了全局。直接修改全局对象的原型。如 Object、Array。

@babel/plugin-transform-runtime

上面提到的一些辅助函数在 @babel/runtime 中会有定义,所以安装 @babel/plugin-transform-runtime 之前还得安装 @babel/runtime。

npm i @babel/plugin-transform-runtime @babel/runtime -S 这两个库是项目生产依赖,而不是开发依赖,所以要用 -S。

{
    "presets": [
        ["@babel/preset-env"]
    ],
    "plugins": [
        ["@babel/plugin-transform-runtime",{
            "corejs":3
        }]
    ]
}

corejs 选项可以配置使用的是 @babel/runtime-corejs2 还是 @babel/runtime-corejs3。 corejs: 2 仅支持全局变量(例如 Promise)和静态属性(例如 Array.from),corejs: 3 还支持实例属性(例如[].includes)。项目中一般都选 corejs3

若是不开启 corejs 选项,在转换时,Babel 使用的一些 helper 会假设已经存在全局的 polyfill;开启之后,Babel 会认为全局的 polyfill 不存在,并会引入 corejs 来完成原来需要 polyfill 才能完成的工作。

指向选项的值为数字,即选择哪个版本的@babel-runtime-corejs: 配置 corejs 为 3,需要预先安装@babel/runtime-corejs3 配置 corejs 为 2,需要预先安装@babel/runtime-corejs2 配置 corejs 为 false,需要预先安装@babel/runtime

#

class Student {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
}

new Promise(function (resolve, reject) {
    resolve(100);
});

[1, 2, 3, 4, 5].copyWithin(0, 3);

编译后

'use strict';

var _interopRequireDefault = require('@babel/runtime-corejs3/helpers/interopRequireDefault');

var _promise = _interopRequireDefault(
    require('@babel/runtime-corejs3/core-js-stable/promise')
);

var _copyWithin = _interopRequireDefault(
    require('@babel/runtime-corejs3/core-js-stable/instance/copy-within')
);

var _createClass2 = _interopRequireDefault(
    require('@babel/runtime-corejs3/helpers/createClass')
);

var _classCallCheck2 = _interopRequireDefault(
    require('@babel/runtime-corejs3/helpers/classCallCheck')
);

var _context;

//index.js
var Student = /*#__PURE__*/ (0, _createClass2['default'])(function Student(
    name,
    age
) {
    (0, _classCallCheck2['default'])(this, Student);
    this.name = name;
    this.age = age;
});
new _promise['default'](function (resolve, reject) {
    resolve(100);
});
(0, _copyWithin['default'])((_context = [1, 2, 3, 4, 5])).call(_context, 0, 3);

可以看到编译后的代码中辅助函数都是引入的 runtime-corejs3 中定义好的函数,并且 copyWithin 也是定义在当前作用域下。 因此我们可以得出 @babel/plugin-transform-runtime 插件的作用:

  • 实现对辅助函数的复用,解决转译语法层时出现的代码冗余
  • 解决转译 api 层出现的全局变量污染

transform-runtime 转换器插件会做三件事:

  • 当你使用 generators/async 函数时,自动引入@babel/runtime/regenerator(可通过 regenerator 选项切换)(它会对将 generator 函数转换为使用不污染全局作用的 regenerator 运行时,可以理解为做了一层兼容,因为 generator 是 es6 的语法)(所以当我们引入它之后项目可以自动支持 generators/async 语法。当然我们也可以用 generator 的插件来支持这种语法,如 @babel/plugin-transform-runtime @babel/plugin-proposal-async-generator-functions
  • 若是需要,将使用 core-js 作为 helpers,而不是假定用户已经使用了 polyfill(可通过 corejs 选项切换)
  • 自动移除内联的 Babel helpers 并取而代之使用@babel/runtime/helpers 模块(可通过 helpers 选项切换)

总结

目前,babel 处理兼容性问题有两种方案:

1.@babel/preset-env + corejs@3 实现语法转换 + 在全局和实例上添加 api,支持全量加载和按需加载,我们简称 polyfill 方案;

// .babelrc
{
   "presets": [
       ["@babel/preset-env", {
           "useBuiltIns": "usage",
           "corejs": 3
       }]
   ]
}

这里也可以引入 @babel/plugin-transform-runtime

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false, // 对ES6的模块文件不做转化,以便使用tree shaking、sideEffects等
        "useBuiltIns": "entry", // browserslist环境不支持的所有垫片都导入
        // https://babeljs.io/docs/en/babel-preset-env#usebuiltins
        // https://github.com/zloirock/core-js/blob/master/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md
        "corejs": {
          "version": 3, // 使用core-js@3
          "proposals": true,
        }
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
        {
          "corejs": false // 解决 helper 函数重复引入
        }
    ]
  ]
}

2.@babel/preset-env + @babel/runtime-corejs3 + @babel/plugin-transform-runtime 实现语法转换 + 模拟替换 api,只支持按需加载,我们简称 runtime 方案。

{
    "presets": [
        ["@babel/preset-env"]
    ],
    "plugins": [
        ["@babel/plugin-transform-runtime",{
            "corejs":3
        }]
    ]
}

两种方案一个依赖核心包 core-js,一个依赖核心包 core-js-pure,两种方案各有优缺点:
polyfill 方案很明显的缺点就是会造成全局污染,而且会注入冗余的工具代码;优点是可以根据浏览器对新特性的支持度来选择性的进行兼容性处理;
runtime 方案虽然解决了 polyfill 方案的那些缺点,但是不能根据浏览器对新特性的支持度来选择性的进行兼容性处理,也就是说只要在代码中识别到的 api,并且该 api 也存在 core-js-pure 包中,就会自动替换,这样一来就会造成一些不必要的转换,从而增加代码体积。

所以,polyfill 方案比较适合单独运行的业务项目,如果你是想开发一些供别人使用的第三方工具库,则建议你使用 runtime 方案来处理兼容性方案,以免影响使用者的运行环境。

webpack babel-loader

在 webpack 中用到 babel 需要先下载 babel-loader

module: {
    rules: [
        {
            test: /\.js$/,
            exclude: /node_modules/,
            use: {
                loader: 'babel-loader',
                options: {
                    presets: ['@babel/preset-env'],
                    plugins: [
                        [
                            '@babel/plugin-transform-runtime',
                            {
                                corejs: 3,
                            },
                        ],
                    ],
                },
            },
        },
    ];
}

因为 babel-loader 很慢,所以 webpack 官方推荐转译尽可能少的文件,配置 exclude 选项将 node_modules 中的文件排除了。但有时候我们引入的库可能并没有经过 babel 编译,所以还得视情况配置。

参考资料

https://juejin.cn/post/6976501655302832159 https://juejin.cn/post/6844904199554072583#heading-12 https://juejin.cn/post/6845166891015602190#heading-5 https://segmentfault.com/a/1190000020237817