面试官:如何使用 babel 进行项目优化

256 阅读7分钟

简介

Babel 是一个广泛使用的 JavaScript 编译器,Babel 是一个强大的 JavaScript 编译器,主要用于将现代 JavaScript 代码转换为向后兼容的版本,并支持其他语言特性如 TypeScriptFlow是一个强大的工具链

我们日常接触到的用法一般也就分为三大类

转译代码以实现兼容性

ESNext(即最新的 ECMAScript 版本)、TypeScriptFlow 等语法转换为目标环境支持的 JavaScript 语法

特定用途的代码转换

利用 Babel 提供的 API 进行自定义代码转换,比如

  • 函数插桩:自动在函数中插入额外代码,例如用于性能监控或埋点
  • 小程序开发:工具如 Taro 使用 Babel API 实现跨平台的小程序开发
  • 国际化:自动处理代码中的国际化字符串。
  • 代码优化:进行各种编译时优化,如删除死代码、变量名混淆等。

代码静态分析

通过解析代码生成抽象语法树(AST),并基于 AST 进行代码分析,比如

  • 代码规范检查linter 工具(如 ESLint)使用 AST 检查代码风格和潜在问题
  • API 文档生成:提取源码中的注释并生成文档。
  • 类型检查:根据 AST 中的类型信息进行静态类型检查,减少运行时错误。
  • 压缩和混淆:通过 AST 分析进行代码优化,生成体积更小、性能更优的代码。
  • 解释执行:直接解释执行 AST,无需生成中间代码

本文主要研究第一种日常用法及优化

  1. 语法降级问题 转译 esnext
    • 将新的 JavaScript 语法(如箭头函数、解构赋值、类等)转换为旧版浏览器和环境可以理解的语法。
    • 示例:ES6 箭头函数 () => {} 转换为 ES5 函数表达式 function() {}
  2. 新特性支持( 提供 Polyfill
    • 提供对尚未被所有浏览器或环境完全支持的新 一些 JS API 的实现代码。
    • 例如,异步函数 (async/await)、装饰器、类属性等。

安装

核心依赖

1. @babel/cli

命令行工具,用于从命令行运行 Babel

2. @babel/core

babel 核心编译库

3. @babel/preset-env

根据目标环境自动选择并应用所需的 Babel 插件,确保只对确实需要的特性进行转换,将新的 JavaScript 语法(如箭头函数、类、解构赋值等)转换为旧版本的 JavaScript 代码
@babel/preset-env 出现之前,如果你想要支持特定的新特性(例如箭头函数),你需要显式地安装相应的 Babel 插件,并在 .babelrc 或其他配置文件中声明它们。例如:

{ "plugins": ["transform-arrow-functions"] }

有了 @babel/preset-env 会根据这个配置自动决定哪些特性需要转换,并加载必要的插件

4. core-js

提供新 API 的 polyfills,如 Array.prototype.includes、Promise 等,这个包不需要单独安装,由 @babel/preset-env 自动引入

5. regenerator-runtime

是一个用于支持异步函数 (async/await) 和生成器函数 (function*) 的 polyfill,这个包也不需要单独安装,core-js里面有,编译的时候会自己引入进来,这样引入同时这也是个代码冗余问题后面我们会研究怎么优化

image.png

依赖安装

 pnpm i @babel/cli @babel/core @babel/preset-env -D

使用

  • 新建 src 文件夹,里面新建 demo.js 文件,输入下内容
const fn = async ()=>{
    console.log("fn");
}

Promise.resolve().then(() => {
    console.log("Promise.resolve().then");
});
  • 新建 .babelrc.json 配置文件
{
  "presets": [
    [
      "@babel/preset-env", 
      {
        // 指定兼容的浏览器版本
        "targets": {
          "chrome": "58",
          "ie": "11"
        },
        // 基础库 core-js 的版本,一般指定为最新的大版本
        "corejs": 3,
        // Polyfill 注入策略,后文详细介绍
        "useBuiltIns": "entry",
        // 不将 ES 模块语法转换为其他模块语法
        "modules": false
      }
    ]
  ]
}

比较重要的是 useBuiltIns,它决定了添加 Polyfill 策略,默认是 false,即不添加任何的 Polyfill

  1. "false" :完全禁用 polyfills 的引入
  2. "entry" :适合不需要细粒度控制的情况,但会增加打包体积
  3. "usage" :推荐用于大多数项目,因为它能按需引入 polyfills,减少打包体积并优化性能
    我们先看下 entry 属性下的打包结果,entry 需要在 入口文件 引入下 core-js 这个依赖
// demo.js 开头加上
import 'core-js';
  • 执行编译命令
npx babel src --out-dir dist

image.png

可以看到所有的 polyfills 都会被引入到这个文件中,而不论它们是否在其他地方使用,没办法做到按需导入,接下来我们试试useBuiltIns: usage,demo.js 里面的 import 'core-js' 去掉,再编译下代码

image.png

编译完代码少了许多,对于 core-js里面的 Polyfill 代码和工具函数实现了按需导入

到现在我们就解决了解决了开头提出的两个主要问题,语法降级问题(通过@babel/preset-env) 和 新特性支持(通过core-js + regenerator-runtime 两个核心的运行时库) ,但是这些方案也会有一些局限性:

  1. 全局污染
  • 使用 core-js 和 regenerator-runtime 时,默认情况下它们会向全局环境添加 polyfill,这在开发应用时通常没有问题,但在开发第三方工具库时可能会导致全局命名空间污染
  1. 代码冗余
  • Babel 默认会在每个使用到辅助函数(如 _defineProperty)的文件中重复生成这些函数的实现代码,像前面提到的 _regeneratorRuntime 函数,可以看一下都是全量引入实现的,如果是多个文件会每个文件都引入实现一遍,这些都会导致打包体积过大

在 src 下面新建 demo1.js 把 demo2.js 的内容复制过去,执行编译命令

image.png

可以看到非常冗余的代码,那么有没有更优雅的 Polyfill 注入方案呢

使用 @babel/plugin-transform-runtime

@babel/plugin-transform-runtime 是一个专门设计来解决上述问题的 Babel 插件。它通过引入 @babel/runtime 库来提供运行时支持,而不是直接将辅助函数和 polyfill 内联到编译后的代码中。这样不仅可以避免全局污染,还可以减少代码冗余

主要功能
  • 非全局版本的 Polyfill@babel/plugin-transform-runtime 不会向全局环境添加 polyfill,而是通过模块化的方式引入所需的运行时支持。
  • 减少代码冗余:将常见的辅助函数提取到 @babel/runtime 中,避免每次编译时都重新生成相同的代码。
  • 支持异步编程:引入 regenerator-runtime 来支持 async/await 和生成器函数。
新增依赖
pnpm i @babel/plugin-transform-runtime -D
pnpm i @babel/runtime-corejs3 -S

@babel/plugin-transform-runtime 是一个编译时的工具
@babel/runtime-corejs3 是运行时的,是一个特定版本的 @babel/runtime

core-js 的三种产物

  1. core-js
  • 全局 Polyfill:就是我们一开始使用的版本,它会将 polyfill 注入到全局环境中(如 window 或 global)。这种方式简单直接,适合开发应用时使用
  1. core-js-pure
  • 按需引入,不污染全局环境:这种形式不会将 polyfill 注入到全局环境中,而是以模块化的方式提供 polyfill。你可以按需引入特定的 polyfill,从而避免全局命名空间污染
  • 适用场景:当你开发第三方库或工具时,这种方式可以确保你的库不会影响全局环境,并且可以更好地控制打包体积。
  • @babel/runtime-corejs3 使用的就是这种产物,它允许你通过模块化方式引入 polyfill,而不会污染全局命名空间
  1. core-js-bundle
  • 打包好的版本:这是一个包含了所有 polyfill 的预打包版本,适合那些不需要按需加载 polyfill 的场景。由于它包含了所有的 polyfill,因此体积较大,不太常用。
  • 适用场景:当你需要一个包含所有 polyfill 的单一文件时,例如某些特定的构建流程或受限环境中
修改配置文件
{
    "plugins": [
        // 添加 transform-runtime 插件
        [
            "@babel/plugin-transform-runtime",
            {
                "corejs": 3
            }
        ]
    ],
    "presets": [
        [
            "@babel/preset-env",
            {
                "targets": {
                    "ie": "11"
                },
                "corejs": 3,
                // 关闭 @babel/preset-env 默认的 Polyfill 注入
                "useBuiltIns": false,
                "modules": false
            }
        ]
    ]
}

因为我们使用了 transform-runtime 插件,所以要 把useBuiltIns属性设为 false ,并且依赖的基础库也发生了变化,不再直接依赖,core-jsregenerator-runtime,而是引入@babel/runtime-corejs3
现在我们再次执行编译命令查看 image.png

左边是之前的 useBuiltIns 方案,右边是 @babel/plugin-transform-runtime 的方案,对比发现

transform-runtime 方案编译后,Babel 会将 async/await/Promise 转换为对 @babel/runtime_asyncToGenerator/_regeneratorRuntime/_Promise 的引用,而不是将完整的 regenerator-runtime 实现嵌入到每个文件中

transform-runtime,提供了一种高效且模块化的方式来处理 polyfills 和工具函数,避免了全局污染并显著减小了打包后的文件体积。