一文搞定前端Babel

1,099 阅读20分钟

Babel 核心详解:从代码到代码的魔法

1. Babel 是什么?为什么需要它?

Babel 是一个 JavaScript 编译器。更准确地说,它是一个源代码到源代码的转换器 (transpiler) 。它的主要用途是将 ECMAScript 2015+ (ES6+) 的代码转换为向后兼容的 JavaScript 版本,以便在当前和旧版本的浏览器或环境中运行。

为什么需要 Babel?

  • 使用最新 JavaScript 特性: JavaScript 语言标准 (ECMAScript) 每年都在发展,带来很多优秀的新语法和 API (如箭头函数、类、模块、Promise、async/await、可选链、空值合并等)。Babel 允许开发者立即使用这些特性,而不必等待所有浏览器都支持它们。
  • JSX 转换: 在 React 等框架中广泛使用的 JSX 语法,并不是标准的 JavaScript。Babel 可以将其转换为普通的 JavaScript 函数调用 (如 React.createElement)。
  • TypeScript/Flow 支持: Babel 也可以转换 TypeScript 或 Flow 这类带有类型注解的 JavaScript 超集。
  • Polyfills (腻子脚本): 对于新的 API (如 Promise, Array.from, Object.assign 等),Babel 可以配合 core-js 等库来提供这些缺失的功能实现。
  • 代码转换与优化: Babel 的插件系统非常强大,可以用于各种代码转换、优化、甚至代码分析。

2. Babel 的核心工作流程

Babel 的转换过程主要分为三个阶段:

  1. 解析 (Parsing): 将输入的源代码字符串解析成一种叫做抽象语法树 (Abstract Syntax Tree, AST) 的数据结构。这个树形结构代表了代码的语法结构。
  2. 转换 (Transformation): 遍历 AST,并对 AST 节点进行添加、删除、替换等操作。这个阶段是 Babel 插件和预设发挥作用的地方,它们定义了如何将新的语法转换为旧的语法,或者实现特定的代码转换逻辑。
  3. 生成 (Generation): 将经过转换的 AST 重新生成为 JavaScript 代码字符串,同时也可以生成对应的 Source Map,以便于调试。
graph LR
    A[源代码 (ESNext, JSX, etc.)] --> B{解析 (Parsing)};
    B --> C[抽象语法树 (AST)];
    C --> D{转换 (Transformation)};
    D -- 使用插件和预设 --> E[修改后的 AST];
    E --> F{生成 (Generation)};
    F --> G[目标代码 (ES5, etc.)];
    F --> H[Source Map];

3. Babel 核心模块详解

Babel 是一个高度模块化的项目,其核心功能由多个 NPM 包组成。

3.1. @babel/parser (曾用名 Babylon)

  • 作用: 负责将 JavaScript 源代码字符串转换为 AST。它是 Babel 工作流程的第一步。

  • AST 结构: Babel 的 AST 遵循 ESTree 规范 (一个社区驱动的 AST 格式标准),并在此基础上进行了一些扩展 (例如支持 JSX、Flow、TypeScript 等)。AST 是一个普通的 JavaScript 对象,描述了代码的结构。

    • 每个节点 (Node) 都有一个 type 属性,表示节点的类型 (如 Identifier, BinaryExpression, FunctionDeclaration 等)。
    • 节点还包含其他属性来描述其细节,如 name, operator, body, params 等。
    • 节点之间通过属性相互连接,形成树状结构。
  • 解析过程简述:

    1. 词法分析 (Lexical Analysis / Tokenizing): 将源代码字符串分解成一系列的词法单元 (Tokens) 。例如,const answer = 42; 会被分解成 const (Keyword), answer (Identifier), = (Punctuator), 42 (NumericLiteral), ; (Punctuator)。
    2. 语法分析 (Syntactic Analysis / Parsing): 基于词法单元流和语言的语法规则,构建出 AST。这个过程会检查语法错误。
  • 代码示例 (AST 概览):
    假设有如下代码:

    const greet = "Hello";
    

    其简化后的 AST 可能如下所示 (实际 AST 会更详细):

    {
      "type": "Program",
      "body": [
        {
          "type": "VariableDeclaration",
          "declarations": [
            {
              "type": "VariableDeclarator",
              "id": {
                "type": "Identifier",
                "name": "greet"
              },
              "init": {
                "type": "StringLiteral",
                "value": "Hello"
              }
            }
          ],
          "kind": "const"
        }
      ],
      "sourceType": "module" // 或 "script"
    }
    

    你可以使用 AST Explorer 这个在线工具来查看不同代码片段生成的 AST。选择 @babel/parser 作为解析器。

  • 源码解读思路: @babel/parser 的源码非常复杂,因为它需要处理 JavaScript 语言的完整语法,包括各种边界情况和最新的语言特性。其内部实现了状态机、递归下降等解析算法。对于大多数用户而言,无需深入其源码,只需了解其输入 (代码字符串) 和输出 (AST 对象) 即可。

3.2. @babel/traverse

  • 作用: 提供了遍历和操作 AST 的能力。这是 Babel 插件进行代码转换的核心。

  • 访问者模式 (Visitor Pattern): @babel/traverse 使用访问者模式来遍历 AST。你可以为 AST 中特定类型的节点定义“访问者”函数。当遍历器遇到相应类型的节点时,就会调用对应的访问者函数。

    • 访问者对象可以为每种节点类型定义 enter()exit() 方法。enter() 在进入节点时调用,exit() 在其所有子节点都处理完毕后离开该节点时调用。通常,我们直接定义节点类型名作为方法名 (如 Identifier(path) { ... }),这等同于定义了 enter 方法。
  • Path 对象: 当访问者函数被调用时,它会接收一个 path 对象作为参数。path 对象非常重要,它代表了当前节点与父节点之间的链接,并提供了大量关于节点上下文的信息以及操作节点的方法:

    • path.node: 当前访问到的 AST 节点。
    • path.parent: 父 AST 节点。
    • path.parentPath: 父节点的 Path 对象。
    • path.scope: 当前节点所处的作用域信息。
    • path.replaceWith(newNode): 用新节点替换当前节点。
    • path.remove(): 删除当前节点。
    • path.insertBefore(nodes): 在当前节点前插入节点。
    • path.insertAfter(nodes): 在当前节点后插入节点。
    • 以及更多用于节点查询和操作的方法。
  • 代码示例 (简单插件:将 var 转换为 let)

    // 这是一个非常简化的插件结构和 traverse 使用示例
    import { parse } from "@babel/parser";
    import traverse from "@babel/traverse";
    import generate from "@babel/generator";
    import * as t from "@babel/types"; // 引入 @babel/types
    
    const code = `var a = 1; function test() { var b = 2; }`;
    
    // 1. 解析
    const ast = parse(code);
    
    // 2. 转换
    // 定义一个插件 (实际插件格式更复杂,这里简化为访问者对象)
    const varToLetVisitor = {
        // 访问所有 VariableDeclaration 类型的节点
        VariableDeclaration(path) {
            // path.node 是当前的 VariableDeclaration 节点
            if (path.node.kind === "var") {
                // 创建一个新的 VariableDeclaration 节点,kind 为 "let"
                // 其他部分 (如 declarations) 保持不变
                // 注意:直接修改 path.node.kind 是更简洁的方式
                path.node.kind = "let";
    
                // 或者,如果你需要更复杂的替换,可以使用 t.variableDeclaration
                // const newDeclaration = t.variableDeclaration(
                //     "let",
                //     path.node.declarations
                // );
                // path.replaceWith(newDeclaration);
            }
        }
    };
    
    traverse(ast, varToLetVisitor); // 对 AST 应用访问者
    
    // 3. 生成
    const output = generate(ast, {}, code); // {} 是选项,code 是原始代码用于 source map
    
    console.log("Original Code:\n", code);
    console.log("\nTransformed Code:\n", output.code);
    // 输出:
    // Original Code:
    //  var a = 1; function test() { var b = 2; }
    //
    // Transformed Code:
    //  let a = 1;
    //  function test() {
    //    let b = 2;
    //  }
    

    在这个例子中:

    1. parse(code) 将代码字符串转换为 AST。
    2. varToLetVisitor 是一个访问者对象,它定义了当遇到 VariableDeclaration 类型的节点时应该执行的逻辑。
    3. traverse(ast, varToLetVisitor) 会遍历 ast。当遇到一个 VariableDeclaration 节点时,它会调用 VariableDeclaration(path) 函数。
    4. 函数内部通过 path.node.kind 检查声明类型,如果是 var,就将其修改为 let
    5. generate(ast, {}, code) 将修改后的 AST 转换回代码字符串。
  • 源码解读思路: @babel/traverse 内部维护了一个遍历上下文,递归地访问 AST 的每个节点。它会根据节点的 type 查找访问者对象中是否有对应的处理函数。它还需要管理作用域 (scope) 信息,以便插件能够进行作用域相关的分析和操作。

3.3. @babel/generator

  • 作用: 将最终的 (可能被修改过的) AST 转换回 JavaScript 代码字符串。它还会负责生成 Source Map,将转换后的代码映射回原始代码,这对于调试至关重要。

  • 选项: @babel/generator 可以接受一些选项来控制输出代码的格式,例如:

    • comments: 是否保留注释 (默认为 true)。
    • compact: 是否生成紧凑的代码 (移除不必要的空格和换行,默认为 auto,当代码大于 500KB 时为 true)。
    • minified: 是否生成压缩的代码 (移除注释、空格,缩短变量名等,默认为 false)。
    • sourceMaps: 是否生成 source map (默认为 false)。
    • sourceFileName, sourceRoot: 与 source map 相关。
  • 代码示例 (AST 片段到代码):
    假设有一个代表 const a = 1; 的 AST 片段:

    const astNode = {
      type: "VariableDeclaration",
      declarations: [
        {
          type: "VariableDeclarator",
          id: { type: "Identifier", name: "a" },
          init: { type: "NumericLiteral", value: 1 }
        }
      ],
      kind: "const"
    };
    
    // 简化版,实际使用需要完整的 Program 节点
    const programAst = {
        type: "Program",
        body: [astNode],
        directives: [],
        sourceType: "script" // 或 "module"
    };
    
    // const { code, map } = generate(programAst, { sourceMaps: true, sourceFileName: "source.js" }, originalCode);
    // console.log(code); // 输出: const a = 1;
    

    @babel/generator 会递归地处理 AST 节点,根据每个节点的 type 和属性,将其“打印”成相应的代码片段,并拼接起来。

  • 源码解读思路: 其内部为每种 AST 节点类型都定义了一个生成函数。例如,遇到 BinaryExpression 节点,它会先递归生成左操作数的代码,然后打印操作符,再递归生成右操作数的代码,并可能根据优先级添加括号。

3.4. @babel/types (通常简写为 ttypes)

  • 作用: 这是一个非常实用的工具包,提供了用于创建、验证和转换 AST 节点的辅助函数。它就像 AST 节点的 Lodash。

  • 主要功能:

    • 节点构造器 (Builders): 例如 t.identifier("myVar") 创建一个标识符节点,t.binaryExpression("+", leftNode, rightNode) 创建一个二元表达式节点。
    • 节点检查器 (Validators/Checkers): 例如 t.isIdentifier(node) 判断一个节点是否是标识符,t.isStatement(node) 判断是否是语句。
    • 断言 (Assertions): 例如 t.assertIdentifier(node) 如果节点不是标识符则抛出错误。
    • 别名 (Aliases): 某些节点类型有别名,例如 Function 可以指代 FunctionDeclaration, FunctionExpression, ArrowFunctionExpression 等。@babel/types 提供了检查这些别名的方法。
  • 代码示例:

    import * as t from "@babel/types";
    
    // 创建节点
    const idName = t.identifier("name");
    const stringVal = t.stringLiteral("Babel");
    const varDeclarator = t.variableDeclarator(idName, stringVal);
    const constDeclaration = t.variableDeclaration("const", [varDeclarator]);
    
    // 验证节点
    console.log(t.isIdentifier(idName)); // true
    console.log(t.isVariableDeclaration(constDeclaration)); // true
    console.log(t.isStringLiteral(idName)); // false
    
    // 使用断言 (如果不是,会抛错)
    try {
        t.assertStringLiteral(idName);
    } catch (e) {
        console.error("Assertion failed:", e.message);
    }
    
    // 将上面创建的 constDeclaration 转换为代码
    // (需要完整的 Program 结构和 @babel/generator)
    // import generate from "@babel/generator";
    // const program = t.program([constDeclaration]);
    // const { code } = generate(program);
    // console.log(code); // const name = "Babel";
    

    在编写 Babel 插件时,@babel/types 是不可或缺的,它使得操作 AST 更加方便和安全。

3.5. @babel/template

  • 作用: 允许你使用字符串模板来快速构建 AST 节点,特别是对于结构比较固定的代码片段。它会将包含占位符的字符串模板编译成一个函数,调用该函数并传入占位符的值,即可生成对应的 AST。

  • 占位符: 占位符通常是大写字母 (如 NAME, VALUE),它们会被替换为实际的 AST 节点。

  • 代码示例:

    import template from "@babel/template";
    import * as t from "@babel/types";
    import generate from "@babel/generator";
    
    // 定义一个模板,NAME 和 VALUE 是占位符
    const buildConstDeclaration = template(`
      const %%NAME%% = %%VALUE%%;
    `); // 使用 %%NAME%% 或 NAME 均可,取决于 template 版本和配置
    
    // 使用模板创建 AST 节点
    const astNodes = buildConstDeclaration({
      NAME: t.identifier("myVar"),       // 占位符 NAME 被替换为 Identifier 节点
      VALUE: t.numericLiteral(123)   // 占位符 VALUE 被替换为 NumericLiteral 节点
    });
    // astNodes 可能是一个节点,也可能是一个节点数组,取决于模板内容
    
    // 通常模板返回的是一个语句或表达式节点,或其数组
    // 我们需要将其放入 Program 节点中才能生成完整代码
    const program = t.program(Array.isArray(astNodes) ? astNodes : [astNodes]);
    const { code } = generate(program);
    console.log(code); // 输出: const myVar = 123;
    

    @babel/template 在需要生成固定模式的代码时非常有用,比手动用 t.* 函数逐个构建节点要简洁得多。

3.6. @babel/core

  • 作用: 这是 Babel 的核心 API 包,它将上述所有部分(解析、转换、生成)串联起来。当你通过 Babel CLI (@babel/cli) 或构建工具 (如 Webpack 的 babel-loader) 使用 Babel 时,它们内部通常都是调用 @babel/core 提供的 API。

  • 主要 API:

    • babel.transform(code, options, callback): 转换代码(异步)。
    • babel.transformSync(code, options): 同步转换代码。
    • babel.transformFile(filename, options, callback): 转换文件(异步)。
    • babel.transformFileSync(filename, options): 同步转换文件。
    • babel.parse(code, options): 仅解析代码到 AST(异步,但通常表现为同步,因为解析本身是同步的)。
    • babel.parseSync(code, options): 同步解析。
    • babel.transformFromAst(ast, code, options, callback): 从已有的 AST 开始转换。
    • babel.transformFromAstSync(ast, code, options): 同步从 AST 开始转换。
  • 选项 (options):

    • plugins: 一个插件数组。
    • presets: 一个预设数组。
    • filename: 用于错误信息和某些插件逻辑。
    • sourceMaps: 是否生成 source map。
    • ast: 是否在返回结果中包含 AST。
    • 等等...
  • 代码示例:

    import { transformSync } from '@babel/core';
    // 假设我们有一个简单的插件,将箭头函数转换为普通函数 (非常简化)
    const arrowFunctionToFunctionExpressionPlugin = ({ types: t }) => {
        return {
            visitor: {
                ArrowFunctionExpression(path) {
                    // 简化处理,不考虑 this 绑定等复杂情况
                    path.replaceWith(
                        t.functionExpression(
                            path.node.id,
                            path.node.params,
                            // 如果 body 不是 BlockStatement,需要包装一下
                            t.isBlockStatement(path.node.body)
                                ? path.node.body
                                : t.blockStatement([t.returnStatement(path.node.body)]),
                            path.node.generator,
                            path.node.async
                        )
                    );
                }
            }
        };
    };
    
    const es6Code = `const add = (a, b) => a + b;`;
    
    try {
        const result = transformSync(es6Code, {
            plugins: [
                arrowFunctionToFunctionExpressionPlugin
                // 也可以使用官方插件,如 '@babel/plugin-transform-arrow-functions'
            ],
            filename: 'example.js', // 可选,但推荐
            ast: true // 如果需要获取 AST
        });
    
        console.log("Transformed Code:\n", result.code);
        // console.log("\nAST:\n", JSON.stringify(result.ast, null, 2));
        // console.log("\nSource Map:\n", result.map);
    } catch (error) {
        console.error("Babel transformation error:", error);
    }
    // 输出 (大致):
    // Transformed Code:
    //  const add = function (a, b) {
    //    return a + b;
    //  };
    

3.7. 插件 (Plugins) 与预设 (Presets)

  • 插件 (Plugins):

    • Babel 本身不进行任何具体的代码转换,除非你告诉它怎么做。插件就是用来实现特定代码转换逻辑的。

    • 每个插件通常只负责一种语法或特性的转换。例如:

      • @babel/plugin-transform-arrow-functions: 转换箭头函数。
      • @babel/plugin-transform-classes: 转换 ES6 类。
      • @babel/plugin-proposal-optional-chaining: 转换可选链操作符 (?.)。
    • 插件是一个函数,它返回一个对象,该对象包含一个 visitor 属性,其值就是传递给 @babel/traverse 的访问者对象。

    • 插件可以有自己的选项。

  • 预设 (Presets):

    • 预设是一组预先配置好的插件的集合,用于简化配置。例如,你不需要手动列出转换 ES2015 所有特性所需的几十个插件,只需使用 @babel/preset-env 即可。

    • 常用预设:

      • @babel/preset-env: 根据你指定的目标环境,智能地加载转换所需特性的插件,以及处理 polyfill (配合 useBuiltInscorejs 选项)。它是最常用的预设。
      • @babel/preset-react: 包含转换 JSX 和其他 React 相关特性的插件。
      • @babel/preset-typescript: 包含转换 TypeScript 的插件。
      • @babel/preset-flow: 包含转换 Flow 的插件。
    • 预设也可以有自己的选项。

  • 执行顺序:

    1. 插件先于预设执行。
    2. 插件按数组顺序从前到后执行。
    3. 预设按数组顺序从后到前执行 (这看起来有点反直觉,但设计如此是为了确保某些预设可以覆盖或依赖于之前的预设行为,例如用户自定义的预设可以基于官方预设进行调整)。

4. 深入理解 @babel/plugin-transform-runtime@babel/runtime

这是 Babel 中一个非常重要且容易混淆的部分,主要用于优化编译输出和处理 Polyfill。

4.1. 背景:为什么需要它们?

当 Babel 转换新的 JavaScript 语法时,有时需要一些辅助函数 (helpers) 来模拟这些功能。例如,转换 ES6 的 class 语法时,Babel 可能会注入类似 _classCallCheck (检查构造函数是否通过 new 调用) 和 _createClass (辅助定义类属性和方法) 这样的函数。

问题点:

  1. 辅助函数重复: 如果不使用 @babel/plugin-transform-runtime,这些辅助函数会直接内联到每个需要它们的文件顶部。如果项目中有大量文件都使用了 class,那么 _classCallCheck 等函数就会在每个编译后的文件中都存在一份,造成代码冗余,增大打包体积。

  2. Polyfill 与全局污染:

    • 语法转换 vs Polyfill: Babel 默认只转换语法 (如箭头函数转普通函数,classfunction),它不处理新的全局对象 (如 Promise, Symbol) 或实例方法 (如 Array.prototype.includes, Object.assign)。这些需要 Polyfill (腻子脚本) 来提供实现。

    • 旧的 @babel/polyfill (已废弃): 这个包通过引入 core-js (提供标准库的 polyfill) 和 regenerator-runtime (提供 async/await 和生成器函数的运行时支持) 来完整模拟一个 ESNext 环境。但它的主要问题是会污染全局作用域 (例如,直接在 windowglobal 上添加 Promise,修改 Array.prototype)。这对于应用开发可能还好,但对于库的开发来说是灾难性的,因为它可能与使用该库的项目或其他库产生冲突。

    • @babel/preset-envuseBuiltIns 这个选项配合 corejs 配置,可以智能地按需引入 core-js 的 polyfill。

      • useBuiltIns: 'entry': 需要在项目入口手动 import "core-js"; import "regenerator-runtime/runtime";。Babel 会根据目标环境将其替换为实际需要的 core-js 模块导入。仍然会污染全局。
      • useBuiltIns: 'usage': Babel 会分析每个文件用到的新 API,并自动在文件顶部引入相应的 core-js 模块。仍然会污染全局。

@babel/plugin-transform-runtime@babel/runtime 就是为了解决这些问题而生的。

4.2. @babel/plugin-transform-runtime 的作用

这是一个 Babel 插件 (在 devDependencies 中),它在代码转换阶段工作。其核心功能是:

  1. 外部化辅助函数:

    • 当 Babel 插件 (如 @babel/plugin-transform-classes) 需要注入辅助函数时,@babel/plugin-transform-runtime 会拦截这个过程。

    • 它不会将辅助函数内联到当前文件,而是将其替换为从 @babel/runtime/helpers 模块的导入语句。

    • 例如,class A {} 转换后,可能不再是内联 _classCallCheck,而是:

      import _classCallCheck from "@babel/runtime/helpers/esm/classCallCheck"; // 如果 useESModules: true
      // 或者
      // var _classCallCheck = require("@babel/runtime/helpers/classCallCheck");
      
      var A = function A() {
        _classCallCheck(this, A);
      };
      
    • 这样,所有文件都从同一个地方 (@babel/runtime) 引用辅助函数,避免了重复。

  2. 模块化 Polyfill (不污染全局):

    • 通过配置 corejs 选项 (如 corejs: 3),此插件可以转换代码中对 ES6+ 内置对象和方法的引用,使其从 @babel/runtime-corejs3 (或 @babel/runtime-corejs2) 中导入这些功能的非污染版本。

    • 例如,new Promise(...) 可能会被转换为:

      import _Promise from "@babel/runtime-corejs3/core-js/promise";
      // ...
      new _Promise(...)
      
    • Object.assign({}, obj) 可能会被转换为:

      import _Object$assign from "@babel/runtime-corejs3/core-js/object/assign";
      // ...
      _Object$assign({}, obj)
      
    • 这些导入的 polyfill 是局部的,不会修改全局对象或其原型。这对于库的开发尤其重要。

  3. regenerator-runtime 的处理:

    • 如果代码中使用了 async/await 或生成器函数,它们依赖 regenerator-runtime。此插件也会将对 regeneratorRuntime 的引用转换为从 @babel/runtime/regenerator 导入。

配置选项:

.babelrc.jsbabel.config.js 中配置:

module.exports = {
  plugins: [
    [
      "@babel/plugin-transform-runtime",
      {
        // corejs: false, // 默认值,不处理 polyfill
        corejs: 3,     // 使用 core-js@3 提供 polyfill,需要安装 @babel/runtime-corejs3
        // corejs: { version: 3, proposals: true }, // 也可以包含提案阶段的 polyfill

        helpers: true, // 默认值,转换辅助函数
        // useESModules: true, // 默认根据调用者推断,可以强制使用 ES 模块导入辅助函数

        regenerator: true, // 默认值,转换 regenerator-runtime
      }
    ]
  ]
};

4.3. @babel/runtime (及 @babel/runtime-corejsX) 的作用

这是一个 NPM 包,它应该作为项目的 生产依赖 (dependencies) 安装,而不是开发依赖 (devDependencies)。因为编译后的代码在运行时会实际 requireimport 这个包里的模块。

  • @babel/runtime 包含:

    • helpers/ : 存放所有 Babel 辅助函数的实际实现。例如 classCallCheck.js, extends.js 等。这些辅助函数通常是纯粹的、独立的 JavaScript 函数。

      • 示例 (@babel/runtime/helpers/esm/classCallCheck.js 的简化概念):

        // 实际代码会更健壮
        export default function _classCallCheck(instance, Constructor) {
          if (!(instance instanceof Constructor)) {
            throw new TypeError("Cannot call a class as a function");
          }
        }
        
    • regenerator/ : 包含 regenerator-runtime/runtime.js 的一个副本或引用,用于 async/await 和生成器。

  • @babel/runtime-corejs2@babel/runtime-corejs3 包含:

    • 这些包是 @babel/runtime 的超集,除了包含 helpersregenerator 外,还包含了从 core-js@2core-js@3 提取出来的、用于模块化引入的 polyfill。
    • 例如,@babel/runtime-corejs3/core-js/promise.js 内部会 require("core-js-pure/features/promise") 或类似的代码,并将其导出。core-js-purecore-js 的一个版本,它提供的 polyfill 不会污染全局。

安装:
如果 @babel/plugin-transform-runtime 配置了 corejs: 3
npm install --save @babel/runtime-corejs3
如果 corejs: 2
npm install --save @babel/runtime-corejs2
如果 corejs: false (默认):
npm install --save @babel/runtime

4.4. @babel/helpers

这是一个 Babel 内部使用的包,它负责生成那些标准化的辅助函数代码字符串。当一个 Babel 插件(比如 @babel/plugin-transform-classes)需要一个辅助函数时,它可以向 @babel/core 请求这个辅助函数。

  • 如果没有使用 @babel/plugin-transform-runtime@babel/core 就会从 @babel/helpers 获取这个辅助函数的代码,并将其直接插入到输出文件的顶部。
  • 如果使用@babel/plugin-transform-runtime,那么这个插件会阻止内联,而是将引用指向 @babel/runtime 中的相应辅助函数。

所以,@babel/helpers 是辅助函数的“源码生成器”,而 @babel/runtime 是这些辅助函数的“预编译分发包”。

4.5. 与 @babel/preset-envuseBuiltIns 的对比

特性@babel/preset-env + useBuiltIns: 'usage' / 'entry'@babel/plugin-transform-runtime + corejs
Polyfill 方式全局 Polyfill (修改全局对象和原型)模块化 Polyfill (不污染全局)
辅助函数处理默认内联 (除非也用了 transform-runtime)@babel/runtime 导入 (避免重复)
适用场景应用开发 (App development)库开发 (Library development),或希望避免全局污染的应用
依赖core-js (devDependency 或 dependency)@babel/runtime-corejsX (dependency)
配置@babel/preset-env 中配置 useBuiltInscorejs单独配置 @babel/plugin-transform-runtime

总结:

  • 对于应用程序开发,如果不太在意全局污染,@babel/preset-envuseBuiltIns: 'usage' 配合 corejs: 3 是一个简单有效的方案,它可以根据代码实际用到的特性按需引入全局 polyfill,并且由于是全局的,对于实例方法 (如 [].includes()) 的 polyfill 会更直接。
  • 对于库 (library) 开发,强烈推荐使用 @babel/plugin-transform-runtime 并配置 corejs。这能确保你的库不会污染全局作用域,避免了与其他库或宿主应用产生冲突,并且辅助函数也能共享,减小体积。
  • 在一些大型应用中,为了严格控制全局作用域和优化共享,也可能会选择 @babel/plugin-transform-runtime

5. Babel 源码阅读的挑战与建议

阅读 Babel 这样的编译器源码是一项有挑战性的任务:

  • 代码库庞大: Babel 由许多包组成,每个包都有其特定的职责,代码量巨大。
  • 高度模块化和抽象: 为了灵活性和可维护性,代码被拆分成很多小模块和抽象层。
  • 性能优化: 编译器对性能要求很高,源码中会有很多为了性能而做的优化,可能使代码不易初学者理解。
  • 复杂的算法和数据结构: 解析器、遍历器等都涉及到复杂的计算机科学理论。
  • ESTree 规范的深入理解: 需要对 AST 结构有清晰的认识。

建议:

  1. 从高层理解: 先理解 Babel 的整体工作流程和各个核心模块的职责。
  2. 从小处着手: 不要试图一开始就理解整个 @babel/parser@babel/core。可以从一个简单的插件源码开始阅读,理解它是如何使用 @babel/traverse@babel/types 来修改 AST 的。
  3. 阅读文档和文章: Babel 官方文档、Babel Handbook 以及社区中有很多关于 Babel 原理和插件开发的文章,这些都是很好的学习资源。
  4. 使用调试工具: 在简单的 Babel 转换脚本中打断点,跟踪 @babel/core@babel/traverse 等模块的执行流程,观察 AST 的变化。
  5. 自己动手写插件: 实践是最好的学习方式。尝试编写一些简单的 Babel 插件,可以加深对 AST 操作和访问者模式的理解。
  6. 关注特定功能: 如果你对某个特定的转换感兴趣 (例如箭头函数是如何转换的),可以找到对应的插件 (@babel/plugin-transform-arrow-functions),然后深入研究它的实现。

6. 总结

Babel 是现代前端开发中不可或缺的工具。它通过解析、转换、生成这三个核心步骤,将最新的 JavaScript 代码转换为兼容性更好的版本,并能处理 JSX、TypeScript 等。其强大的插件和预设系统提供了极高的灵活性。

  • @babel/parser 负责将代码变成 AST。
  • @babel/traverse 配合插件的 visitor 模式来遍历和修改 AST。
  • @babel/generator 将修改后的 AST 变回代码。
  • @babel/types@babel/template 是操作 AST 的好帮手。
  • @babel/core 是这一切的调度中心。
  • @babel/plugin-transform-runtime@babel/runtime (及其 core-js 版本) 对于优化辅助函数和提供无污染的 polyfill 至关重要,尤其适合库的开发。