非常深入的理解一下babel

632 阅读9分钟

什么是babel

这里借用一下官方的定义

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

简单的理解就是,对较高版本的语法做一次向下兼容,使得较低版本的浏览器能够识别并运行这些代码

为什么会出现babel

先来看一下这个词babel(bāb(ə)l,音译为 掰bou,中文译为巴别塔。

我的理解是:

  • es6之类的只是语言规范,而浏览器要实现这一规范可能就需要相当漫长的一个时间了。
  • 各种前端技术造成了各种js方言,尤其是react的jsx 所以急需一个工具去解决这些问题,然后babel就应运而生。

再来看一下百度上对巴别塔的解释

巴别塔;是《圣经·旧约·创世记》第11章故事中人们建造的塔。根据篇章记载,当时人类联合起来兴建希望能通往天堂的高塔;为了阻止人类的计划,上帝让人类说不同的语言,使人类相互之间不能沟通,计划因此失败,人类自此各散东西。此事件,为世上出现不同语言和种族提供解释。

是不是感觉非常🐂🍺

这里想到了我司的一个项目,这个项目的主要作用就是使用函数计算在移动端截图,我司名字是moka,所以这个项目起名为mokapture,我个人感觉还是挺有意思的

重点:presets

babel会通过具体的某个插件对相应的代码进行转码,比如箭头函数对应的插件就是@babel/plugin-transform-arrow-functions,链式调用对应的插件就是@babel/plugin-proposal-optional-chaining,我们不可能一个一个的安装并一个一个引入使用这些插件,那么presets就为我们提供了一组插件的集合。

官方 Preset,包括:env,flow,react,typescript等

这里重点介绍一下env,这也是我们平时用的最多的

env 的核心目的是通过配置(browserslist, compat-table, and electron-to-chromium)得知目标环境的特点,然后只做必要的转换。例如目标浏览器支持 es2015,那么 es2015 这个 preset 其实是不需要的,于是代码就可以小一点(一般转化后的代码总是更长),构建时间也可以缩短一些。如果不写任何配置项,env默认使用最新的JS语法(不包括Stage-X阶段)。

基本的配置以及本人认为比较重要的options如下:

module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        targets: "> 0.25%, not dead",
        modules: false,
        useBuiltIns: "entry",
        corejs: "3",
      },
    ],
  ],
};
  • targets: 支持babel转换的浏览器环境,示例代码的意思是仅包含浏览器市场份额超过0.25%的用户所需的polyfill和代码转换(忽略没有安全更新的浏览器,如IE10和BlackBerry)。具体的语法可以参考browserslist
  • modules:让 babel 以特定的模块化格式来输出代码,可选值为"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false。默认为auto
    1. auto会根据caller的配置数据决定是否模块化处理,一般情况下不需要我们配置。
    2. false不进行模块化处理。
  • useBuiltIns: "usage" | "entry" | false, 默认为false,如果使用此配置项,需要指定corejs版本,不然会有WARNING
    1. usage会自动根据我们的环境自动引入对应的core-jsregenerator-runtime插件对我们的代码进行模块化解析
    2. entry确保我们的每个文件只引入一次polyfill代码
    3. false则不自动给文件引入polyfill,也不会给core-js或者polyfill做按需加载处理

Stage-X (实验性质的 Presets)

TC39 将提案分为以下几个阶段:

  • Stage 0 - 设想(Strawman):只是一个想法,可能有 Babel插件。
  • Stage 1 - 建议(Proposal):这是值得跟进的。
  • Stage 2 - 草案(Draft):初始规范。
  • Stage 3 - 候选(Candidate):完成规范并在浏览器上初步实现。
  • Stage 4 - 完成(Finished):将添加到下一个年度版本发布中。 注意:这些提案可能会有变化,因此,特别是处于 stage-3 之前的任何提案,请务必谨慎使用。我们计划在每次 TC39 会议之后,如果有可能,在提案变更时更新 stage-x 的 preset。

注意Preset的排列顺序

Preset 是逆序排列的(从后往前)。

{
  "presets": ["a", "b", "c"]
}

将按如下顺序执行: c、b 然后是 a。

这主要是为了确保向后兼容,由于大多数用户将 "es2015" 放在 "stage-0" 之前,这样必须先执行 stage-0 才能确保 babel 不报错。另外:Plugin 会运行在 Preset 之前,从前向后执行。

重点:babel插件

Babel 是一个编译器(输入源码 => 输出编译后的代码)。就像其他编译器一样,编译过程分为三个阶段:解析、转换和打印输出。现在,Babel 虽然开箱即用,但是什么动作都不做。它基本上类似于 const babel = code => code; ,将代码解析之后再输出同样的代码。如果想要 Babel 做一些实际的工作,就需要为其添加插件。

我们可以看到babel主要还是依靠各种插件来对我们的代码进行编译。下面我们来看一下babel的插件到底有多强大吧。先来看一段代码:

// index.js
const study = (a, b) => a + b;

这是一段很常见的使用箭头函数的代码,那么我们如何使用babel去编译这段代码呢,让我们先依次来执行下列代码

npm install --save-dev @babel/core
npm install --save-dev @babel/cli
npm install --save-dev @babel/plugin-transform-arrow-functions

安装完这几个模块之后我们再配置一下babel

// babel.config.js
module.exports = {
  presets: [],
  plugins: ["@babel/plugin-transform-arrow-functions"],
};

然后在终端里执行

npx babel index.js --out-file index-compiled.js

我们打开index-compiled.js看一下

// index-compiled.js
const study = function (a, b) {
  return a + b;
};

babel把我们的箭头函数给转换成了function形式

babel-core

All transformations will use your local configuration files.

所有的转换都将用本地的配置文件(.babelrc、babel.config.js或者package.json),core即使核心嘛,我们也可以看到core的仓库里的目录结构 babel-core 主要是将代码转成ast,方便各个插件分析语法进行相应的处理

babel-cli

用于命令行使用babel,比如我们上面👆代码的npx babel index.js --out-file index-compiled.js

babel-node

babel-node is a CLI that works exactly the same as the Node.js CLI, with the added benefit of compiling with Babel presets and plugins before running it. babel-nodebabel附带的第二个CLI,工作原理与Node.js的CLI完全相同,只是它会在运行之前编译ES6代码,node环境下可以直接运行代码,而不需要转码。babel-node一般不用于生产环境,因为运行前动态编译,所以内存的开销非常的大。babel-node相当于babel-polyfill + babel-register

babel-register

babel-register会在node环境下,给require绑定一个钩子函数,这个钩子会讲所有以.es6, .es, .jsx, .mjs, and .js为后缀的文件都将用babel进行转码。所以babel-register只会对require的文件转码,并不会对自身文件转码,因为是实时转码,所以不适用于生产环境。并且如果你想用babel-register的话,还要一并引入babel-polyfill

babel-polyfill

babel 默认只转换 js 语法,而不转换新的 API,比如 IteratorGeneratorSetMapsProxyReflectPromise 这种全局Api或者对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。所以在使用这些方法时就必须要使用babel-polyfill(包含core-jsregenerator-runtime)。

这里我们改造一下代码,npm install --save-dev @babel/preset-env ,然后修改一下babel.config.js

// babel.config.js
module.exports = {
  presets: ["@babel/preset-env"],
  plugins: [],
};

这里再看一下babel-polyfill

// index.js
import "@babel/polyfill";
new Promise((resolve, reject) => {
  resolve(1);
});
// index-compiled.js
"use strict";

require("@babel/polyfill");

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

我们注意到@babel/polyfill的引用方式变成require了,怎么变成import呢, 在babel.config.js中,将preset-env的配置项中添加modules: false即可。所以此时的babel.config.js文件变成了:

// babel.config.js
module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        modules: false,
        useBuiltIns: "entry",
        corejs: "3",
      },
    ],
  ],
  plugins: [],
};

我们再来执行一下,会发现@babel/polyfill的引用方式变回import了。

一般情况下我们在使用polyfill时都是通过webpack的入口文件配置,比如:

module.exports = {
  entry: ["babel-polyfill", "./app/js"]
};

所以,babel-polyfill要安装在生产环境dependencies中。

因为babel-polyfill会把所有的方法都加在原型上,比如Array.isArray这个方法,babel-polyfill会在Arrayprotorype上挂载这个方法,即Array.prototype.isArray,这将导致:

  • babel-polyfill打包出来的体积非常大,因为所有的原型上都挂载的有兼容的方法,我只想用Array.isArray,但是Object.assign也会被挂载兼容方法。当然也有优化的方法,配置useBuiltIns: usage
  • 在原型上挂载方法污染全局变量,如果我们是在维护一个公共组件库,就十分的危险了 所以plugin-transform-runtime会是一个比较不错的选择

babel-runtime 和 babel-plugin-transform-runtime

当我们在使用一些稍复杂的语法时,babel会借用一些函数进行转换,比如说es6的class

// index.js
class Circle {}

经过babel的默认转换后:

// index-compiled.js
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Circle = function Circle() {
  _classCallCheck(this, Circle);
};

我们可以看到,babel会使用一些函数来帮助处理这些复杂的转换,那我们可以预想到,每个文件都这么转换之后代码量将会是多么的庞大,并且还重复了很多的这些helper函数,所以babel-plugin-transform-runtime其实就是把这些helper函数统一收集起来,下次直接在文件中引用即可,我们安装一下babel-plugin-transform-runtime

npm install --save-dev @babel/plugin-transform-runtime

配置babel.config.js

// babel.config.js
module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        modules: false,
        useBuiltIns: "entry",
        corejs: "3",
      },
    ],
  ],
  plugins: ["@babel/plugin-transform-runtime"],
};

此时运行再看我们编译后的class

import _classCallCheck from "@babel/runtime/helpers/classCallCheck";

var Circle = function Circle() {
  _classCallCheck(this, Circle);
};

已经变成了引入的方式。我们注意到@babel/plugin-transform-runtime其实是引用的@babel/runtime的代码。

此外@babel/plugin-transform-runtime还有一个最重要的作用就是为我们提供了一个配置项corejs,他可以给babel-polyfill提供一个沙箱环境,这样就不会污染到全局变量,而且无副作用。但是这项要开启@babel/plugin-transform-runtime的沙箱模式,必须要配置corejs corejs 需要注意的是corejs2只支持全局变量(promise。。。)和静态属性(Array.from。。。),而不支持实例属性(includes。。。),如果要用实例属性,就要使用corejs3。但是corejs3目前还是提案阶段,所以还需配置proposals: true,所以此时babel.config.js为:

module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        modules: false,
        useBuiltIns: "entry",
        corejs: "3",
      },
    ],
  ],
  plugins: [
    [
      "@babel/plugin-transform-runtime",
      {
        corejs: { version: 3, proposals: true },
      },
    ],
  ],
};

参考链接