想弄懂Babel?你必须得先弄清楚这几个包

lxf2023-03-11 13:25:01

前言

相信很多人都知道Babel,知道它是用来编译ES6+的东西。但是再深入一点,大家都清楚我们平时项目Babel用到的那些包作用是什么吗?为什么要用那几个包?

另外,我们平时项目中Babel用到的包其实并不多,基本就是文章中讲解的这几个包,所以如果大家能把这几个包弄得很清楚,Babel的大部分知识也了解的差不多了。

由于Babel内容实在太多,加上配置讲的话,篇幅太长,大家阅读也累。为了让大家更好地理解,我们把Babel拆成两部分(Babel了解篇,Babel配置篇)来学习

Babel了解篇

就是本篇文章。脱离配置Babel这块,系统的梳理、介绍、讲解我们平时Babel用到的主要几个包,并且答疑官网中一些让人疑惑的地方。

Babel配置篇

当我们在清楚地理解了Babel的主要几个包后,我们就能更好地深入Babel怎么配置。

想了解配置篇的朋友,可以看这里—— Babel配置不要再“复制粘贴”了,带你自己配一个Babel

本篇文章稍微有点长,第一是因为Babel本身内容就特别多,第二是因为有些同学看官网的时候,觉得很难懂,所以文章写的比较详细,并且会答疑了一些我们平时看官网难懂的地方,所以希望大家可以静下心来,一个模块一个模块的看,每个模块都是循序渐进的。相信我,看完你一定会对Babel有一个更清晰的理解

备注:

  • 当前@babel/core最新版本是:7.20.12
  • 当前@babel/preset-env最新版本是:7.20.2

Babel

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

我们可以这么理解,Babel就是一个工具。它是一个可以将ES6+新特性,转换成低版本浏览器或其他环境能支持并正常运行的一个工具。

结构

很多人以为Babel只有pluginspresets等几个配置。其实不止,我们看看Babel配置文件大致架构:

// babel.config.js
module.exports = {
    ...,
    envName: "development",
    plugins: [],
    presets: [],
    passPerPreset: false,
    targets: {},
    browserslistConfigFile: true,
    browserslistEnv: undefined,
    inputSourceMap: true
    ...
}

我们一般主要用到的就是pluginspresets这两个

功能

从大体上看,Babel提供以下两个功能组成:

  • 编译ES6+最新语法(letclass() => {}等)
  • 实现旧版本浏览器不支持的ES6+APIPromiseSymbolArray.prototype.includes等)

参考文章:

  • What is Babel?
  • Options

@babel/core

core可以看出,它是Babel实现编译的核心。所以我们如果要使用Babel@babel/core这个包一定是必不可少的。另外我们平常说的Babel 6Babel 7指的就是@babele/core的版本

参考文章:@babel/core

@bable/cli

官网解释:Babel自带了一个内置的CLI命令行工具,可通过命令行编译文件

简单地说就是,让我们可以在终端里使用命令来编译(这样可以更好的调试打印信息):

npx babel index.js

安装的话,我们最好安装到我们项目的本地目录下,尽量不要安装到全局(影响全局的东西,都很可怕)

参考文章:@babel/cli

@bable/preset-env

官网解释:@babel/preset-env是一个智能预设,它允许您使用最新的JavaScript,而无需微观管理目标环境需要哪些语法转换(以及可选的浏览器polyfill)。这既让你的生活更轻松,也让JavaScript包更小!

理解

@bable/preset-env这个名字,我们可以拆开两部分来看,这样方便理解:

  • preset预设
  • env环境

preset

Babel编译ES6+语法,是通过一个个插件plugin去实现的。每年都会有不同新的提案、新的语法,但我们不可能一个个插件去配置,所以就有了preset这个东西。因此我们可以理解成preset就是一个语法插件集合包,这样我们只用安装这一个包,不需要一个个配插件,就可以很方便的编译最新的语法了。

我们通过一个不用预设的案例 no-preset ,感受一下如果不用preset有多麻烦。

//  入口文件 index.js
const senses = ['eye', 'nose', 'ear', 'mouth'];

const lMC = {
    senses,
    like: ['eat', 'drink', 'play', 'fun'],
    information: {
        sex: 'male',
        age: '18+'
    },
    play: (sport = 'badminton') => {
        console.log(`play ${sport}`);
    }
};

const { like, information } = lMC;

这段代码,我们用了几个ES6新语法:

  • const声明
  • 属性的简洁表示法
  • 箭头函数
  • 函数默认值
  • 模板字符串
  • 解构

如果不用preset我们Babel配置如下:

// Babel配置文件 babel.config.js
const plugins = [
    '@babel/plugin-transform-arrow-functions',
    '@babel/plugin-transform-block-scoping',
    '@babel/plugin-transform-destructuring',
    '@babel/plugin-transform-parameters',
    '@babel/plugin-transform-shorthand-properties',
    '@babel/plugin-transform-template-literals'
];
module.exports = {plugins};

编译后的文件:

// 编译后的文件 compile.js
var senses = ['eye', 'nose', 'ear', 'mouth'];
var lMC = {
  senses: senses,
  like: ['eat', 'drink', 'play', 'fun'],
  information: {
    sex: 'male',
    age: '18+'
  },
  play: function () {
    var sport = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'badminton';
    console.log("play ".concat(sport));
  }
};
var like = lMC.like,
  information = lMC.information;

在不用preset的情况下,实现上述编译的过程,我基本是用一个ES6新语法,我就要去查一个插件,首先我不记得那么多插件,其次一个个插件找真的很累。

ok,那我们再用一个使用了预设的案例 use-preset ,感受一下预设到底有多方便。 我们npm i @babel/preset-env -D,修改babel.config.js使用preset预设:

// 修改babel.config.js
const presets = [
    '@babel/preset-env'
];

module.exports = {presets};

编译后的文件:

// 编译后的文件 compile.js
"use strict";

var senses = ['eye', 'nose', 'ear', 'mouth'];
var lMC = {
  senses: senses,
  like: ['eat', 'drink', 'play', 'fun'],
  information: {
    sex: 'male',
    age: '18+'
  },
  play: function play() {
    var sport = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'badminton';
    console.log("play ".concat(sport));
  }
};
var like = lMC.like,
  information = lMC.information;

我们会发现,用preset(预设)方式输出的代码,跟plugins(不用预设)方式输出的代码是几乎是一模一样的。但是presetbabel.config.js更简洁,我也不需要一个个插件去找,也不需要安装那么多插件,只用安装@babel/preset-env这一个包,就可以很愉快的写ES6+

env

env指的是环境。因为@babel/preset-env还有一个配置功能,我们可以通过配置我们代码运行的目标环境,来控制polyfill(一个提供低版本浏览器缺失的ES6+新特性的方法与实现的集合 ,后面会有更详细的讲解)的导入跟语法编译,从而使ES6+新的特性可以在我们想要的目标环境中顺利运行。

备注:关于@babel/preset-env的配置功能,可查看这篇——Babel配置不要再“复制粘贴”了,带你自己配一个Babel

功能

通过上面对presetenv的理解跟案例感受,我们能总结出@babel/preset-env主要提供以下功能:

  • 只编译ES6+语法(上述案例只使用了ES6+的语法,并没有用ES6+API
  • 它并不提供polyfill,但是可以通过配置我们代码运行的目标环境,从而控制polyfill的导入跟语法编译,使ES6+的新特性可以在我们想要的目标环境中顺利运行

注意

我们先看看TC39提案分为几个阶段:

  • 阶段0  (stage-0)——草根(Strawman):只是一个想法,可能是Babel插件。
  • 第一阶段(stage-1)——提案(Proposal):这是值得研究的。
  • 第二阶段(stage-2)——草案(Draft):初步规范。
  • 第三阶段(stage-3)——候选(Candidate):完整的规范和最初的浏览器实现。
  • 第四阶段(stage-4)——完成(Finished):将被添加到下一年度的版本中。

再看看官网中这段话:

Note: @babel/preset-env won't include any JavaScript syntax proposals less than Stage 3 because at that stage in the TC39 process, it wouldn't be implemented by any browsers anyway. Those would need to be included manually.

大致意思是:

  • Babel 7以后,@bable/preset-env舍弃了Stage presets@babel/preset-stage-x)这种预设
  • @bable/preset-env只提供TC39大于stage-3的提案(即只包含stage-4阶段),因此如果要用小于stage 4的提案语法,则必须先安装再手动引入对应插件

第一点相信大家都很好理解,我们来理解一下第二点是什么意思。

意思是,如果我们想用一些小于stage-4阶段的语法的话,光安装@babel/preset-env这一个包是没有用的,因为这个包里只包含编译stage-4的预设,所以我们就得安装并配置相应的plugin去编译。

在写这篇文章的时候,有一个新的语法 do expressions ,它当前是处于stage-1阶段的语法,用插件@babel/plugin-proposal-do-expressions可以编译这个语法。

想弄懂Babel?你必须得先弄清楚这几个包

官网解释:do { .. } 表达式执行一个块(其中有一个或多个语句),块内的最终语句完成值成为 do 表达式的完成值。

我们借助官网,整理成这个案例 compile-stage-1 来看看怎么使用小于stage-4的语法。

我们先只用@babel/preset-env,看看能不能编译do {...}这个语法。

// do expressions stage-1语法
let x = 100;
let y = 20;

let a = do {
    if (x > 10) {
        if (y > 20) {
            ("big x, big y");
        } else {
            ("big x, small y");
        }
    } else {
        if (y > 10) {
            ("small x, big y");
        } else {
            ("small x, small y");
        }
    }
};

Babel.config.js配置:

const presets = [
    '@babel/preset-env'
];

// const plugins = [
    // '@babel/plugin-proposal-do-expressions'
// ];

// module.exports = {plugins, presets};

module.exports = {presets};

我们会发现终端报错

想弄懂Babel?你必须得先弄清楚这几个包

大致意思是:@babel/preset-env当前未启用对实验语法doExpressions的支持(因为doExpressions当前是stage-1的语法,@babel/preset-env只包含必编译stage-4的语法插件),需要我们加入@babel/plugin-proposal-do-expressions插件去编译。

那我们npm i @babel/plugin-proposal-do-expressions -D,修改一下babel.config.js

const presets = [
    '@babel/preset-env'
];

const plugins = [
    '@babel/plugin-proposal-do-expressions'
];

module.exports = {plugins, presets};

// module.exports = {presets};

我们可以看到,可以正常输出编译后的文件:

"use strict";

var x = 100;
var y = 20;
var a = x > 10 ? y > 20 ? "big x, big y" : "big x, small y" : y > 10 ? "small x, big y" : "small x, small y";

所以,当我们想使用小于stage-4阶段的语法时,我们要先找到其对应的编译插件安装,然后在plugins里面配置就好了。

参考文章:

  • TC39 proposals
  • proposal-do-expressions
  • @babel/plugin-proposal-do-expressions

补充

有时我们也可能需要知道我们当前的preset(预设)包含了哪些插件,那我们怎么查看当前@babel/preset-env包含了哪些预设呢?

我们可以通过查看@babel/preset-env --> package.json --> dependencies里面可以找到。我目前安装的@babel/preset-env版本为7.20.2,它包含了以下预设:

// @babel/preset-env@7.20.2预设
"dependencies": {
    "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6",
    "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9",
    ...,
    "@babel/plugin-transform-sticky-regex": "^7.18.6",
    "@babel/plugin-transform-template-literals": "^7.18.9",
    "@babel/plugin-transform-typeof-symbol": "^7.18.9",
    "@babel/plugin-transform-unicode-escapes": "^7.18.10",
    "@babel/plugin-transform-unicode-regex": "^7.18.6",
  },

polyfill

功能

ES6+除了提供很多简洁的语法(letclass() => {}等)外,还为我们提供了很多便捷的APIPromiseSymbolArray.prototype.includes等)。但旧版本浏览器是不支持这些API,而polyfill存放了这些API的方法与实现,所以它可以使得这些不支持的浏览器,支持这些API

理解

我们可以把所有这种存放了ES6+ API方法与实现的集合叫做polyfill,也就是我们经常说的垫片。(如果把我们的旧版本浏览器缺失的API当做一个个坑,polyfill就是用来把这些坑填平)

polyfill也分很多种,像core-js是会提供旧版本浏览器缺失的所有API;还有一些只提供缺失API某块,例如 promise-polyfill、proxy-polyfill 等。

Babel配置polyfill的过程,就是实现旧版本浏览器对这些API支持的过程。

@babel/polyfill

上面我们解释了polyfill是什么,从包名@babel/polyfill就知道,它就是一个polyfill(其核心是依靠core-js@2.x.x实现)。虽然这个包已经被废弃了,但我们还是稍微了解一下它。

官网解释: