Skip to content

Babel 与 AST

一、Babel 概述

1.1 Babel 是什么

Babel 是一个 JavaScript 编译器,主要用于:

  • 语法转换 (ES6+ → ES5)
  • Polyfill (core-js)
  • 源码转换 (JSX, TypeScript)

1.2 核心包

包名作用
@babel/core核心编译引擎
@babel/cli命令行工具
@babel/preset-env智能预设,按目标环境转换
@babel/preset-reactReact JSX 转换
@babel/preset-typescriptTypeScript 转换

二、Babel 编译流程

2.1 三个阶段

源代码 → Parse(解析) → AST → Transform(转换) → AST → Generate(生成) → 代码

2.2 详细流程

1. Parse (解析)
   ├── 词法分析: 代码 → Tokens
   └── 语法分析: Tokens → AST

2. Transform (转换)
   └── 遍历 AST,应用插件转换

3. Generate (生成)
   └── AST → 代码字符串

2.3 示例

javascript
// 输入
const add = (a, b) => a + b;

// Parse 后的 AST (简化)
{
  "type": "VariableDeclaration",
  "declarations": [{
    "type": "VariableDeclarator",
    "id": { "type": "Identifier", "name": "add" },
    "init": {
      "type": "ArrowFunctionExpression",
      "params": [
        { "type": "Identifier", "name": "a" },
        { "type": "Identifier", "name": "b" }
      ],
      "body": {
        "type": "BinaryExpression",
        "operator": "+",
        "left": { "type": "Identifier", "name": "a" },
        "right": { "type": "Identifier", "name": "b" }
      }
    }
  }]
}

// 输出
var add = function(a, b) { return a + b; };

三、Babel 配置

3.1 配置文件

javascript
// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', {
      targets: '> 0.25%, not dead',
      useBuiltIns: 'usage',
      corejs: 3
    }],
    '@babel/preset-react',
    '@babel/preset-typescript'
  ],
  plugins: [
    '@babel/plugin-proposal-decorators',
    ['@babel/plugin-transform-runtime', {
      corejs: 3
    }]
  ]
};

3.2 Preset 与 Plugin

  • Plugin: 单个转换功能
  • Preset: 一组 Plugin 的集合

执行顺序:

  1. Plugins 先于 Presets
  2. Plugins 按顺序执行
  3. Presets 逆序执行

3.3 Polyfill 策略

方式一: useBuiltIns

javascript
// babel.config.js
{
  presets: [
    ['@babel/preset-env', {
      useBuiltIns: 'usage',  // 按需引入
      corejs: 3
    }]
  ]
}

// 缺点: 污染全局

方式二: @babel/plugin-transform-runtime (推荐)

javascript
{
  plugins: [
    ['@babel/plugin-transform-runtime', {
      corejs: 3  // 使用 @babel/runtime-corejs3
    }]
  ]
}

// 优点: 不污染全局,适合库开发

四、AST 操作

4.1 AST 节点类型

类型说明示例
Identifier标识符foo
Literal字面量42, "hello"
BinaryExpression二元表达式a + b
CallExpression函数调用foo()
FunctionDeclaration函数声明function foo() {}
ArrowFunctionExpression箭头函数() => {}
VariableDeclaration变量声明const a = 1
ImportDeclaration导入声明import x from 'y'

4.2 AST 操作工具

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

// 1. 解析
const ast = parser.parse(code, {
  sourceType: 'module',
  plugins: ['jsx', 'typescript']
});

// 2. 遍历转换
traverse(ast, {
  // 访问 Identifier 节点
  Identifier(path) {
    if (path.node.name === 'foo') {
      path.node.name = 'bar';
    }
  },

  // 访问函数调用
  CallExpression(path) {
    // path.node - 当前节点
    // path.parent - 父节点
    // path.replaceWith() - 替换节点
    // path.remove() - 删除节点
  }
});

// 3. 生成代码
const output = generate(ast, {}, code);
console.log(output.code);

五、Babel 插件开发

5.1 插件结构

javascript
module.exports = function(babel) {
  const { types: t } = babel;

  return {
    name: 'my-plugin',
    visitor: {
      // 访问者模式
      Identifier(path) {
        // 处理 Identifier 节点
      },
      CallExpression(path) {
        // 处理函数调用
      }
    }
  };
};

5.2 实战:移除 console.log

javascript
module.exports = function({ types: t }) {
  return {
    name: 'remove-console',
    visitor: {
      CallExpression(path) {
        const { callee } = path.node;

        // 匹配 console.log / console.warn 等
        if (
          t.isMemberExpression(callee) &&
          t.isIdentifier(callee.object, { name: 'console' })
        ) {
          path.remove();
        }
      }
    }
  };
};

5.3 实战:自动导入 React

javascript
module.exports = function({ types: t }) {
  return {
    name: 'auto-import-react',
    visitor: {
      Program: {
        enter(path) {
          // 检查是否已导入 React
          const hasReactImport = path.node.body.some(
            node => t.isImportDeclaration(node) &&
                    node.source.value === 'react'
          );

          if (!hasReactImport) {
            // 创建 import React from 'react'
            const importDeclaration = t.importDeclaration(
              [t.importDefaultSpecifier(t.identifier('React'))],
              t.stringLiteral('react')
            );
            path.unshiftContainer('body', importDeclaration);
          }
        }
      }
    }
  };
};

5.4 实战:埋点插件

javascript
module.exports = function({ types: t }) {
  return {
    name: 'auto-tracker',
    visitor: {
      // 为函数添加埋点
      FunctionDeclaration(path) {
        const functionName = path.node.id.name;

        // 创建 tracker 调用
        const trackerCall = t.expressionStatement(
          t.callExpression(
            t.memberExpression(
              t.identifier('tracker'),
              t.identifier('log')
            ),
            [t.stringLiteral(functionName)]
          )
        );

        // 插入到函数体开头
        path.get('body').unshiftContainer('body', trackerCall);
      }
    }
  };
};

// 转换前
function handleClick() {
  doSomething();
}

// 转换后
function handleClick() {
  tracker.log('handleClick');
  doSomething();
}

六、高频面试题

Q1: Babel 的编译流程?

  1. Parse: 将代码解析为 AST
  2. Transform: 遍历 AST,应用插件转换
  3. Generate: 将 AST 生成代码

Q2: Preset 和 Plugin 的执行顺序?

  1. Plugins 先于 Presets 执行
  2. Plugins 按配置顺序执行
  3. Presets 按配置逆序执行

Q3: useBuiltIns 的三个值的区别?

说明
false不自动注入 polyfill
entry在入口处全量引入
usage按需引入使用到的

Q4: @babel/plugin-transform-runtime 的作用?

  1. 复用辅助函数,减少代码体积
  2. 使用沙箱化的 polyfill,不污染全局
  3. 适合库的开发

前端面试知识库