Skip to content

Babel

第一章:初识

一、简介

1. 是什么

Babel 是一个通用的多功能的 Javascript Transpiler(JS 转译器)。主要用于将采用 ES6 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在旧版本的浏览器或其他环境中。

转译器:高级语言到高级语言;编译器:高级语言到低级语言;转译器是特殊的编译器。

常见的用途:

  • 语法转换(降级)

  • 通过 Polyfill 方式在目标环境中添加缺失的功能(通过引入第三方 polyfill 模块,例如 core-js

  • 源码转换(codemods)

    例如我们在 React 经常使用 JSX 语法,由于这不是 JS 原生语法,所以不能直接被 JS 引擎编译执行。在代码被执行之前,需要使用编译器进行转译,转换成 JS 代码。

中文文档:

2. 原理

babel 是 source to source 的转换,整体编译流程分为三步:

  • parse:通过 parser 把源码转成抽象语法树(AST)。
  • transform:遍历 AST,调用各种 transform 插件对 AST 进行增删改。
  • generate:把转换后的 AST 打印成目标代码,并生成 sourcemap。

对于不了解编译原理的前端开发人员,推荐一个 github 上面的 mini 级别的项目:jamiebuilds/the-super-tiny-compiler

二、快速入门

1. 基本使用

Babel 是作为一个独立的工具(和 postcss 一样),可以不和 webpack 等构建工具配置来单独使用。

如果希望在命令行尝试使用 babel,需要安装如下库:

  • @babel/core:babel 的核心代码,必须安装;

  • @babel/cli:可以让我们在命令行使用 babel;

bash
npm install @babel/cli @babel/core

使用 babel 来处理我们的源代码:

bash
# 按文件编译
babel 要编译的文件 -o 编辑结果文件

# 按目录编译
babel 要编译的整个目录 -d 编译结果放置的目录

例子:

bash
# src: 是源文件的目录
# --out-dir: 指定要输出的文件夹 dist
npx babel src --out-dir dist

重点:Babel 本身不做任何处理,只是会解析源代码转为 AST 抽象语法树,如果需要对代码降级等,一切需要插件来完成。

2. 结合插件的使用

比如我们需要转换箭头函数,那么我们就可以使用箭头函数转换相关的插件:

bash
npm install @babel/plugin-transform-arrow-functions -D
bash
npx babel src --out-dir dist --plugins=@babel/plugin-transform-arrow-functions

查看转换后的结果:发现只有箭头函数转换为了普通函数,但 const 并没有转成 var。这是因为 plugin-transform-arrow-functions,并没有提供这样的功能,需要使用 plugin-transform-block-scoping 来完成这样的功能。

bash
npm install @babel/plugin-transform-block-scoping -D
bash
npx babel src --out-dir dist --plugins=@babel/plugin-transform-block-scoping,@babel/plugin-transform-arrow-functions

问题:一个插件一个功能,让我们自己来一个个配置,好麻烦,怎么办?使用预设。

3. 使用预设 preset

安装 @babel/preset-env 预设:

bash
npm install @babel/preset-env -D

执行如下命令:

bash
npx babel src --out-dir dist --presets=@babel/preset-env

三、扩展知识

1. babel 中常见的库

@babel/runtime 的作用:作为一个“公共且集中的函数库”,用来提取和复用 Babel 编译时产生的辅助代码(Helpers),从而大大减小最终打包的体积。

2. babel 的配置

文档:配置 Babel

babel 本身没有做任何事情,真正的编译要依托于 babel 插件和 babel 预设来完成。babel 预设和 postcss 预设含义一样,是多个插件的集合体,用于解决一系列常见的兼容问题。

如何告诉 babel 要使用哪些插件或预设呢?需要通过一个配置文件 babel.config.json 或者 .babelrc.json 或者 package.json 中增加一个 babel 键名,值为对象。

例如:在项目的根目录(package.json 所在的位置)创建一个名为 babel.config.json 的文件,其中包含以下内容。

json
{
  "presets": [],
  "plugins": []
}

3. JS Parser 的历史

在前端工程化(比如代码编译、压缩、代码检查)中,工具必须先“读懂”你的代码,这就需要一个叫 Parser(解析器)的工具,把代码转换成计算机能看懂的树状结构(AST,抽象语法树)。

1)制定武林规矩:SpiderMonkey 与 ESTree

最早的时候,火狐浏览器的引擎 SpiderMonkey 公布了它是怎么把 JS 代码拆解成 AST 的。因为它是权威,所以大家把它当成了行业标准,这个标准后来被称为 ESTree 标准

2)开荒大弟子:Esprima

既然有了规矩,当时就诞生了最早的一批解析器工具,最著名的就是 Esprima。早期的前端工具基本都在用它来解析代码。

3)大弟子没跟上时代,自立门户:Espree (ESLint)

2015 年之后(ES6 发布),JavaScript 标准开启了每年更新的时代。但 Esprima 的作者更新太慢了,跟不上新语法。

而需要检查代码语法的工具 ESLint 实在等不及了,于是把 Esprima 的代码 Fork 下来自己改,起名叫 Espree。这也是现在 ESLint 默认在用的老底子。

4)新王登基:Acorn

后来,江湖上出现了一个新秀叫 Acorn。它不仅严格遵守 ESTree 标准,而且运行速度非常快,最厉害的是它支持插件扩展。也就是说,有了新语法,不用去改核心代码,写个插件装上去就行了。

5)天下归一:大家都改用 Acorn

因为 Acorn 实在太好用了,于是:

  • ESLint 把自己那个叫 Espree 的工具的核心,偷偷换成了基于 Acorn 的重新实现。
  • Babel 的解析器(以前叫 babylon,现在叫 @babel/parser)也决定基于 Acorn 来开发。

Babel 借用了 Acorn 速度快、底子好的优势,然后在它上面加了一堆自己的黑科技插件(比如让它能认识 TypeScript、JSX 语法等),最终演变成了我们今天用的 Babel Parser。

第二章:babel 预设

一、前提知识

1. 官方预设

babel 官方为常见环境组装了一些预设:

2. 预设顺序

预设顺序是相反的(从最后一个到第一个)。

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

将按照 c、b,然后是 a 的顺序运行。

3. 预设选项

插件和预设都可以通过将名称和选项对象封装在配置中的数组中来指定选项。

对于不指定选项,这些都是等效的:

json
{
  "presets": [
    "presetA", // string
    ["presetA"], // array 包裹
    ["presetA", {}] // 第二个参数是一个空对象
  ]
}

要指定选项,请将带有键的对象作为选项名称传递。

json
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "loose": true, // ES6+ 代码降级时使用的模式。Babel 8 中删除了此配置。
        "modules": false // 是否将 ES 模块语法转换为另一种模块类型。设置为 false 表示保留 ES 模块。
      }
    ]
  ]
}

二、@babel/preset-env

babel 有多种预设,最常见的预设是 @babel/preset-env

@babel/preset-env 可以让你使用最新的 JS 语法,而无需针对每种语法转换设置具体的插件。

配置

json
{
  "presets": [
    "@babel/preset-env"
  ]
}

兼容的浏览器

@babel/preset-env 需要根据兼容的浏览器范围来确定如何编译,和 postcss 一样,可以使用文件 .browserslistrc 来描述浏览器的兼容范围。

last 3 version  # 每个浏览器的最后 3 个版本
> 1%            # 全球使用率大于 1% 的浏览器
not ie <= 8     # 不需要兼容 ≤ IE 8

为什么都使用 .browserslistrc 配置文件?因为都使用了 browserslist 这个库。这个库的目标是让你只写一次“我的项目需要兼容哪些浏览器”,然后所有的前端编译工具都会统一去读取这个配置,并据此来处理你的代码。这个库通过查询 Can I use 网站来得到需要兼容具体的哪些浏览器。

自身的配置

postcss-preset-env 一样,@babel/preset-env 自身也有一些配置。

具体的配置见:https://www.babeljs.cn/docs/babel-preset-env#options

配置方式是:

json
{
  "presets": [
    ["@babel/preset-env", {
      "配置项1": "配置值",
      "配置项2": "配置值",
      "配置项3": "配置值"
    }]
  ]
}

其中一个比较常见的配置项是 usebuiltins,该配置的默认值是 false。

它有什么用呢?由于该预设仅转换新的语法,并不对新的 API 进行任何处理。

例如:

javascript
new Promise(resolve => {
  resolve()
})

转换的结果为:

javascript
new Promise(function (resolve) {
  resolve();
});

如果遇到没有 Promise 构造函数的旧版本浏览器,该代码就会报错。

而配置 usebuiltins 可以在编译结果中注入这些新的 API,它的值默认为 false,表示不注入任何新的 API。可以将其设置为 usage,表示根据 API 的使用情况,按需导入 API。

json
{
  "presets": [
    ["@babel/preset-env", {
      "useBuiltIns": "usage",
      "corejs": 3 // 默认使用的 corejs 这个库的版本为 2, 此配置告诉使用 3 版本
    }]
  ]
}

注意:需要安装 core-jsregenerator-runtime 包。
core-js 仅负责填补缺失的新 API(内置对象和方法),比如 Promise 构造函数;regenerator-runtime 保证异步代码 await/async 关键字在低版本浏览器还能正常运行;@babel/preset-env 仅处理 JS 新语法转换为兼容语法,比如 2**3 会转为 Math.pow(2, 3),let 关键字转为 var。
@babel/polyfill 已过时,目前被 core-jsgenerator-runtime 所取代。

第三章:babel 插件

除了预设可以转换代码之外,插件也可以转换代码,它们的顺序是:

  • 插件在 Presets 前运行。
  • 插件顺序从前往后运行。
  • Preset 运行顺序是从后往前。

通常情况下,@babel/preset-env 只转换那些已经形成正式标准的语法,对于某些处于早期阶段、还没有确定的语法不做转换。

如果要转换这些语法,就要单独使用插件。

一、@babel/plugin-proposal-class-properties

该插件可以让你在类中书写初始化字段。

javascript
class A {
  a = 1;
  constructor(){
    this.b = 3;
  }
}

二、@babel/plugin-proposal-function-bind

该插件可以让你轻松的为某个方法绑定 this。

javascript
function Print() {
  console.log(this.loginId);
}

const obj = {
  loginId: "abc"
};

obj::Print(); //相当于:Print.call(obj);

遗憾的是,目前 VSCode 无法识别该语法,会在代码中报错,虽然并不会有什么实际性的危害,但是影响观感。

三、@babel/plugin-proposal-optional-chaining

javascript
const obj = {
  foo: {
    bar: {
      baz: 42,
    },
  },
};

const baz = obj?.foo?.bar?.baz; // 42

const safe = obj?.qux?.baz; // undefined

四、babel-plugin-transform-remove-console

该插件会移除源码中的控制台输出语句。

javascript
console.log("foo");
console.error("bar");

编译后:(发现该插件会把打印语句删除)

javascript

五、@babel/plugin-transform-runtime

babel 最重要的作用就是对 JS 代码进行转换。比如把 ES6+ 代码转为 ES5 代码时,babel 自身会生成一些辅助函数,比如代码中有 class 关键字的代码,那么每个 JS 模块都需要生成关于 class 的辅助代码,这样就造成多个文件代码重复问题。

babel 考虑到了这个问题,把这些代码封装到了 @babel/runtime 库中。只要配合 babel 的 @babel/plugin-transform-runtime 插件就能避免 babel 添加的代码重复问题。

备注:@babel/plugin-transform-runtime 插件作用就是按需导入 @babel/runtime 库中的代码。

第四章:与构建工具结合

一、与 Webpack 结合

在实际开发中,通常会在构建工具中通过配置 babel 来对其进行使用的,比如在 webpack 中。

1)安装相关的依赖。

bash
npm install --save-dev babel-loader @babel/core @babel/preset-env

2)可以设置一个规则,在加载 js 文件时,使用 babel 对 JS 代码进行转换。

javascript
{
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: { // 可选,也可以单独创建一个 babel.config.json 配置文件来配置 babel
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  }
}

3)创建 babel.config.json 文件,在项目根目录下。

json
{
  "presets": ["@babel/preset-env"]
}
preview
图片加载中
预览

Released under the MIT License.