【前端进阶】一文彻底读懂Babel 7

492 阅读12分钟

前言

本教程基于目前最新的Babel7.26.0版本,后续Babel版本如果有大的变动,我会另写一篇介绍变动点。

1. Babel是什么

Babel是一个工具链,主要用于将es6+代码转换为当前和旧版浏览器或环境中向后兼容的JavaScript版本

2. Babel编译流程

Babel的编译流程分为三步:parsing(解析)、transforming(转化)、generating(生成)

  1. 解析:@babel/parser 负责将ES6代码进行语法分析和词法分析后转换成抽象语法树AST

  2. 转换:@babel/traverse 负责遍历AST并进行节点的操作

  3. 生成:@babel/generator 负责通过AST树生成ES5代码

而整个过程,由@babel/core负责编译过程的控制和管理。它会调用其他模块来解析、转换和生成代码

3. Babel编译流程示例

情景:

将函数中的 let 转为 const,访问AST转换平台:astexplorer.net/,将以下函数输入,右侧将出现转换结果

function example() {
  let a = 1;
  let b = 2;
  return a + b;
}

右侧部分截图:

1731053414710.jpg

body是函数体部分,函数体内部又包含两个变量声明VariableDeclaration以及一个return语句ReturnStatement

思路:

  1. 将源码转成 ast

  2. 遍历树节点,当遇到 type === 'VariableDeclaration' && kind === 'let' 时,将其 kind 转为 'const'

  3. 将 ast 转为源码

步骤:

  1. 新建一个文件夹,运行npm init初始化一个package.json

  2. 分别安装@babel/parser,@babel/traverse,@babel/generator

  3. 新建index.js

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;

const code = `function example() {
  let a = 1;
  let b = 2;
  return a + b;
}`;

// 1.解析:使用babel解析器解析源码为AST
const ast = parser.parse(code);

// 定义一个遍历AST的访问器对象,也就是访问到目标节点【这里是VariableDeclaration】的时候会做什么处理
const visitor = {
  VariableDeclaration(path) { // 这里的 path 是指当前的上下文,而不是路径
    if (path.node.kind === 'let') {
      path.node.kind = 'const';
    }
  }
};

// 2. 转换:使用traverse遍历AST并应用访问器,也就是遍历并应用刚才那个 visitor 规则
traverse(ast, visitor);

// 3. 生成:使用generate根据修改后的AST生成新的代码
const output = generate(ast, {});

// 打印修改后的代码
console.log(output.code);

使用VS Code的插件Code Runner直接运行index.js查看打印结果,可以看到输出的函数中let已全部被替换为const

1731053471181.jpg

4. Babel重要组成

plugins 插件

Babel 插件是一段代码,用于对 Babel 在编译过程中生成的抽象语法树(AST)进行操作。Babel 强调功能的单一性,即每个插件应专注于实现一个特定的功能。例如,如果你想使用 ES6 的箭头函数,就需要使用 @babel/plugin-transform-arrow-functions 插件。想要使用React的jsx语法,就需要使用@babel/plugin-transform-react-jsx。

然而,ES6、ES7 等版本引入了许多新语法特性,难道要一个个单独安装这些插件吗?这时,预设(presets)就派上用场了。

presets 预设

presets预设实际上是一组插件的集合,它们被预先配置好,以解决特定的需求。例如,@babel/preset-env 是一个常用的预设,它可以根据你的目标环境自动选择需要的插件,以便将代码转换为兼容的版本。

查看Babel文档,可以看到@babel/preset-env中有许多plugins。例如,ES6的箭头函数使用arrow-functions处理,也就是@babel/plugin-transform-arrow-functions这个插件。

1731053516817.jpg

plugins 和 presets 的运行顺序

Babel 配置可以通过 .babelrc 文件、package.json 文件中的 babel 字段或者通过配置文件 babel.config.js 来指定。plugins字段指定插件的集合,Presets字段指定预设的集合。

"plugins": ["pluginA", "pluginB"],
"presets": ["a", "b", "c"]

Plugins 执行顺序从前往后:pluginA,pluginB

presets 执行顺序从后往前: c , b ,a

Plugins 在 Presets 前运行

5. polyfill 介绍

垫片的主要作用就是支持旧版浏览器。对于支持新语法的浏览器不进行代码兜底兼容,对于旧版浏览器使用旧语法来兼容。

访问 browsersl.ist/ 查看浏览器的使用情况,例如在右侧输入dead,查看哪些浏览器处于不再维护状态

1731053594949.jpg

可以看到dead状态的浏览器,全球还有0.62%的人们在使用。IE浏览器11版本,全球范围还有0.45%的人在用。未来使用人数只会更低。

新建一个文件夹,运行npm init初始化一个package.json,修改package.json,配置browserslist,指定最低的浏览器版本

  "browserslist": [
      "Chrome >= 69",
      "Firefox >= 95",
      "last 2 safari version",
      "ie 8"
    ]

browserslist 配置会告诉 Babel 需要支持哪些浏览器。Babel 会根据这些浏览器的特性支持情况,自动选择需要转换的语法特性。 例如,如果某个现代 JavaScript 特性在 Chrome 69 及以上版本中已经支持,Babel 就不会对其进行转换。

6. 安装

yarn add @babel/core @babel/cli @babel/preset-env @babel/plugin-transform-runtime  -D
yarn add core-js@3 @babel/runtime-corejs3 -S

@babel/core:负责编译过程的控制和管理。它会调用其他模块来解析、转换和生成代码

@babel/cli:一个内置的CLI命令行工具,可通过命令行编译文件

@babel/preset-env:根据目标环境自动选择需要的转义插件,转换现代 JavaScript 代码。

@babel/runtime:包含一些辅助(helpers)函数,实现polyfill时函数的复用。@babel/runtime不需要额外安装,安装@babel/preset-env时,会作为依赖包一同被安装。

regenerator-runtime:负责处理异步函数(如generator、async/await)。不需要额外安装,作为@babel/runtime的依赖,会一同被安装。

@babel/plugin-transform-runtime:在编译过程中,自动使用 @babel/runtime 里的辅助函数。

core-js:它提供了广泛的老版本浏览器兼容的 JavaScript 功能实现,包括但不限于 Promise、Map、Set、Array.from、String.prototype.includes 等。

@babel/runtime-corejs3:专门为与 @babel/plugin-transform-runtime 一起使用而设计,提供 core-js-pure 的 polyfills 和一些辅助函数。

提示:@babel/polyfill由于不支持按需引入和会全局污染window对象等缺点,从Babel7.4开始,@babel/polyfill就不再推荐使用。

7. polyfill 三种方式

babel将ES6+版本的代码分为了两种情况,语法层和api层面:

语法层: let、const、class、箭头函数等,这些默认会被Babel转义

api层面:Promise、includes、map等,这些是在全局或者Object、Array等原型上新增的方法,需要借助polyfill来完成转义

1. @babel/preset-env entry模式

  1. 新建babel/input.js
const fn = () => {
    console.log("测试箭头函数");
  };
  new Promise((resolve, reject) => {
    resolve("测试Promise");
  }).then((res) => {
    console.log(res);
  });
  1. 修改package.json的scripts,新增启动命令
"babel": "babel babel/input.js --out-file babel/output.js"
  1. 新建babel.config.json
{
    "presets": ["@babel/preset-env"],
    "plugins": []
  }

运行 npm run babel 进行打包,生成的output.js

"use strict";
var fn = function fn() {
  console.log("测试箭头函数");
};
new Promise(function (resolve, reject) {
  resolve("测试Promise");
}).then(function (res) {
  console.log(res);
});

可以看到语法层面的箭头函数降级了。但是api层面的Promise并未做处理,对于不支持Promise语法的浏览器,需要polyfill垫片处理。

  1. 修改babel.config.json,配置@babel/preset-env做polyfill垫片处理
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry", 
        "corejs": {
          "version": 3
        }
      }
    ]
  ],
  "plugins": []
}

entry模式,并且指定corejs的版本。

  1. 修改input.js,手动导入core-js/stable
import "core-js/stable";
const fn = () => {
  console.log("测试箭头函数");
};
new Promise((resolve, reject) => {
  resolve("测试Promise");
}).then((res) => {
  console.log(res);
});

运行 npm run babel 进行打包,生成的output.js的部分截图,光是require就有2百多行。

1731053755561.jpg

useBuiltIns 设置为 'entry'时,会注入目标环境不支持的所有api层面方法。但大部分情况下肯定是希望按需引入,减小打包体积的,这时可使用usage模式。

2. @babel/preset-env usage模式

  1. 修改babel.config.json,配置@babel/preset-env做polyfill垫片处理
{
    "presets": [
      [
        "@babel/preset-env",
        {
          // 实现按需加载
          "useBuiltIns": "usage", 
          "corejs": {
            "version": 3
          }
        }
      ]
    ],
    "plugins": []
  }
  1. 修改input.js,去掉import 'core-js/stable'
const fn = () => {
    console.log("测试箭头函数");
  };
  new Promise((resolve, reject) => {
    resolve("测试Promise");
  }).then((res) => {
    console.log(res);
  });

运行 npm run babel 进行打包,生成的output.js

"use strict";
require("core-js/modules/es.object.to-string.js");
require("core-js/modules/es.promise.js");
var fn = function fn() {
  console.log("测试箭头函数");
};
new Promise(function (resolve, reject) {
  resolve("测试Promise");
}).then(function (res) {
  console.log(res);
});

可以看到只导入了相应api层面的降级处理,例如require("core-js/modules/es.promise.js");处理了Promise的降级处理。

使用@babel/preset-env进行polyfill的问题

问题1:会造成全局污染

core-js是通过require("core-js/modules/es.promise.js");方式导入了Promise的垫片用法,使得你即使在不支持Promise的浏览器中,直接使用 Promise。但如果第三方库也可能定义了相同名称的方法或属性,会造成冲突

问题2. 会生成重复的辅助函数,增大打包体积

修改input.js,增加async,await语法

const fn = () => {
    console.log("测试箭头函数");
  };
  new Promise((resolve, reject) => {
    resolve("测试Promise");
  }).then((res) => {
    console.log(res);
  });
  async function ge(){
    await console.log("测试async,await");
  }

运行 npm run babel 进行打包,生成的output.js部分截图

1731053878378.jpg

除了require的部分,还多了好多定义的函数,这些是辅助函数(比如上边的_regeneratorRuntime函数),是在编译阶段辅助 Babel 的函数;问题来了,现在只有一个JS文件需要转换,然而实际项目开发中会有大量的需要转换的文件,如果每一个转换后的文件中都存在相同的函数,那代码总体积肯定会无意义的增大。

而@babel/runtime中包含了常用的辅助函数,可以减少打包体积,这也涉及到第3种polyfill方式:@babel/plugin-transform-runtime。

3. @babel/plugin-transform-runtime

辅助函数被统一封装在@babel/runtime中提供的helper模块中,编译时,@babel/plugin-transform-runtime自动使用 @babel/runtime 里的辅助函数

  1. 修改babel.config.json
{
    "presets": ["@babel/preset-env"],
    "plugins": [
      [
        "@babel/plugin-transform-runtime",
        {
          "corejs": { "version": 3 }
        }
      ]
    ]
  }

这里指定版本3,对应着上面下载的@babel/runtime-corejs3。如果指定版本2,则下载@babel/runtime-corejs2。

运行 npm run babel 进行打包,生成的output.js部分截图

1731053930682.jpg

可以看到定义的函数消失了,而是通过导入辅助函数,导入了所需的函数。例如使用@babel/preset-env垫片时生成的_regeneratorRuntime,在@babel/plugin-transform-runtime中直接导入了_regenerator。

当然,如果因为某些原因,就是要在文件中定义辅助函数,而不是从外部引入,此时可通过设置helpers为false

  1. 修改babel.config.json
{
    "presets": ["@babel/preset-env"],
    "plugins": [
      [
        "@babel/plugin-transform-runtime",
        {
          "corejs": { "version": 3 },
          "helpers": false
        }
      ]
    ]
  }

运行 npm run babel 进行打包,生成的output.js部分截图

image.png

可以看到用于垫片异步函数的_regeneratorRuntime又重新内联在文件内。此使用场景很少,并且helpers选项将在未来的 Babel 8 中删除。

提示:

使用@babel/plugin-transform-runtime进行垫片时,不再需要core-js,因为@babel/plugin-transform-runtime的依赖包core-js-pure,会在安装@babel/plugin-transform-runtime时一同安装。

core-js-pure是core-js的一个纯净版本,它的设计目标是不对全局作用域进行任何修改,防止全局污染。可以看到它是通过定义一个变量的方式来使用,例如_promise。

总结:

语法层面,Babel会使用@babel/preset-env默认进行转义;api方法层面,需要额外的polyfill来兼容旧环境。目前共有三种方式:

  1. 使用 @babel/preset-env ,useBuiltIns 设置为 'entry':会注入目标环境不支持的所有api层面方法,需在代码中主动使用import 'core-js/stable'

  2. 使用 @babel/preset-env ,useBuiltIns 设置为 'usage':会注入目标环境不支持的所有被用到的api层面方法

  3. 使用 @babel/plugin-transform-runtime:通过局部变量的方式实现了所有被用到的api层面方法,不会污染全局

8. 案例分析

分析create-react-app搭建的React项目是如何做polyfill的

1. 使用create-react-app搭建一个react项目

npm config set registry https://registry.npmmirror.com
npx create-react-app my-app

2. 运行npm run eject 将配置暴露出去,方便查看配置

3. 查看配置config/webpack.config.js,找到babel-loader

babel-loader 是一个 Webpack 加载器(loader),用于将 JavaScript 文件通过 Babel 编译器进行转换。

1731054112313.jpg

presets预设中通过require.resolve('babel-preset-react-app')引入了babel-preset-react-app。

4. 从node_modules找到babel-preset-react-app

babel-preset-react-app/index.js,内容如下所示

'use strict';
const create = require('./create');
module.exports = function (api, opts) {
  const env = process.env.BABEL_ENV || process.env.NODE_ENV;
  return create(api, opts, env);
};

可以看到主要还是用了create.js文件内容

5. 查看create.js文件return语句,找到presets配置

1731054180084.jpg

可以看到预设presets中主要配置了三个预设,@babel/preset-env用于转换现代 JavaScript 代码,@babel/preset-react专门用于处理React代码,@babel/preset-typescript专门用于处理TypeScript。

@babel/preset-env开启了entry模式,意味着在代码中,需手动导入core-js/stable时,才启动polyfill。一般情况下使用不到这个功能。

6. 继续看return语句,找到plugins配置

plugins中的配置较多,只展示@babel/plugin-transform-runtime相关代码

1731054195081.jpg

@babel/plugin-transform-runtime的corejs为false,说明没有开启相关polyfills。

为什么不需要polyfills?查看package.json的browserslist配置

7. browserslist

  "browserslist": {
      "production": [
        ">0.2%",
        "not dead",
        "not op_mini all"
      ],
      "development": [
        "last 1 chrome version",
        "last 1 firefox version",
        "last 1 safari version"
      ]
    },

开发环境不用说,用的都是最新的浏览器,生产环境也进行了相应的浏览器配置。访问 browsersl.ist/ 查看浏览器的使用情况,例如在右侧输入> 0.2% and not dead and not op_mini all,查看浏览器使用情况。

1731054284712.jpg

Chrome目前最低的支持版本为109,此配置可以说的上是只支持新版浏览器了。

polyfills是什么:polyfills垫片的主要作用就是支持旧版浏览器。对于支持新语法的浏览器不进行代码兜底兼容,对于旧版浏览器使用旧语法来兼容。

既然只支持新版浏览器,那么就没有使用polyfills的需要了。

8. 为什么只支持新版浏览器?

因为React 18 引入了许多新特性,如并发模式、自动批处理等,这些特性依赖于现代浏览器提供的 API 和性能优化。React 团队选择不再支持 IE11等旧版浏览器。

结尾

本文若存在不准确或不完整之处,欢迎各位读者批评指正

参考文章:juejin.cn/post/728414…