跳转至

Babel 概念与原理

概述

Babel 是一个流行的用于将新版本 ES6+ 代码转换为向后兼容版本(ES5) 代码的 JavaScript 编译器。它还为 JSX 语法提供了编译 支持,以及一些其他插件可用于转换特定类型的代码。

Babel 的作用

1. 语法转换

将 ES6+ 的新语法转换为 ES5,使代码可以在旧版浏览器中运行。

示例:

// 输入(ES6+)
const add = (a, b) => a + b;
class Person {
  constructor(name) {
    this.name = name;
  }
}

// 输出(ES5)
var add = function add(a, b) {
  return a + b;
};
var Person = function Person(name) {
  this.name = name;
};

2. Polyfill 注入

为目标环境中缺少的功能添加 polyfill。

// 输入
const promise = Promise.resolve();
const arr = [1, 2, 3].includes(2);

// Babel 会注入必要的 polyfill
import "core-js/modules/es.promise";
import "core-js/modules/es.array.includes";

3. 源码转换

支持 JSX、TypeScript、Flow 等语法的转换。

// JSX 转换
const element = <h1>Hello, world!</h1>;

// 转换后
const element = React.createElement("h1", null, "Hello, world!");

Babel 工作原理

Babel 的工作原理可以分为三个主要阶段:解析(Parse)转换(Transform)生成(Generate)

源代码 → 解析 → AST → 转换 → 新的 AST → 生成 → 目标代码

1. 解析(Parse)

当 Babel 接收到源代码时,将会调用一个叫做解析器的工具,用于将源代码转换为抽象语法树(AST)。

解析过程分为两个阶段:

词法分析(Lexical Analysis)

将代码字符串转换为令牌(tokens)流。

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

// Tokens
[
  { type: 'Keyword', value: 'const' },
  { type: 'Identifier', value: 'add' },
  { type: 'Punctuator', value: '=' },
  { type: 'Punctuator', value: '(' },
  { type: 'Identifier', value: 'a' },
  { type: 'Punctuator', value: ',' },
  { type: 'Identifier', value: 'b' },
  { type: 'Punctuator', value: ')' },
  { type: 'Punctuator', value: '=>' },
  // ...
]

语法分析(Syntactic Analysis)

将令牌流转换为 AST。

{
  type: "VariableDeclaration",
  kind: "const",
  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" }
        }
      }
    }
  ]
}

在这个过程中,解析器会识别代码中的语法结构,并将其转换为对应的节点类型。例如,当解析器遇到一个变量声明语句时,它将会创建一个 "VariableDeclaration" 节点,并将该节点的信息存储在 AST 中。

AST 是一个以节点为基础组成的树形结构,每个节点都有相应的类型、属性和子节点等信息。

2. 转换(Transform)

一旦 AST 被创建,Babel 将遍历整个树形结构,对每个节点进行转换。这些转换可以是插件、预设或手动创建的。

转换器会检查 AST 中的每个节点,然后对其进行相应的修改或替换,以将新语法转换为旧语法。

转换过程:

  1. 遍历 AST:使用访问者模式(Visitor Pattern)遍历 AST
  2. 应用插件:每个插件定义了对特定节点类型的转换规则
  3. 修改节点:根据转换规则修改、替换或删除节点

示例:箭头函数转换

// Babel 插件示例
module.exports = function({ types: t }) {
  return {
    visitor: {
      ArrowFunctionExpression(path) {
        // 将箭头函数转换为普通函数
        const functionExpression = t.functionExpression(
          null,
          path.node.params,
          t.blockStatement([
            t.returnStatement(path.node.body)
          ])
        );
        path.replaceWith(functionExpression);
      }
    }
  };
};

例如,如果 Babel 遇到一个包含箭头函数的节点,而你已经启用了转换插件,该插件将会将箭头函数转换为其等效的普通函数。

代码转换后,Babel 将会生成一个新的 AST。

3. 生成(Generate)

最后,Babel 将基于转换后的 AST 生成代码文本。

在这个步骤中,Babel 将遍历转换后的 AST,并创建对应的代码字符串,并将这些字符串组合成一个完整的 JavaScript 文件。如果启用了代码压缩,Babel 还可以将生成的代码进行压缩。

生成过程:

  1. **深度优先遍历**转换后的 AST
  2. 生成代码字符串:根据节点类型生成对应的代码
  3. 生成 Source Map:如果配置了,生成源码映射文件
// 转换后的 AST → 生成代码
var add = function add(a, b) {
  return a + b;
};

Babel 配置

配置文件

Babel 支持多种配置文件格式:

  • babel.config.js(推荐,项目范围配置)
  • babel.config.json
  • .babelrc(文件相对配置)
  • .babelrc.js
  • package.json 中的 babel 字段

基本配置示例

// babel.config.js
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          browsers: ['> 1%', 'last 2 versions', 'not dead']
        },
        useBuiltIns: 'usage',
        corejs: 3
      }
    ],
    '@babel/preset-react',
    '@babel/preset-typescript'
  ],
  plugins: [
    '@babel/plugin-proposal-class-properties',
    '@babel/plugin-proposal-object-rest-spread',
    [
      '@babel/plugin-transform-runtime',
      {
        corejs: 3
      }
    ]
  ]
};

Babel 核心概念

1. Presets(预设)

Presets 是一组插件的集合,用于支持特定的语法特性。

@babel/preset-env

最常用的预设,可以根据目标环境自动确定需要的转换。

{
  presets: [
    [
      '@babel/preset-env',
      {
        // 目标环境
        targets: {
          chrome: '58',
          ie: '11',
          browsers: ['> 1%', 'last 2 versions']
        },
        // polyfill 策略
        useBuiltIns: 'usage', // 'usage' | 'entry' | false
        corejs: 3,
        // 模块转换
        modules: 'auto', // 'auto' | 'amd' | 'umd' | 'systemjs' | 'commonjs' | false
        // 调试
        debug: false
      }
    ]
  ]
}

useBuiltIns 选项:

  • false:不自动添加 polyfill
  • 'entry':在入口文件引入全部 polyfill
  • 'usage':按需引入 polyfill(推荐)
// useBuiltIns: 'entry'
import 'core-js/stable';
import 'regenerator-runtime/runtime';

// useBuiltIns: 'usage'
// Babel 自动按需引入

@babel/preset-react

用于转换 JSX 语法。

{
  presets: [
    [
      '@babel/preset-react',
      {
        runtime: 'automatic', // 'automatic' | 'classic'
        development: process.env.NODE_ENV === 'development',
        importSource: '@emotion/react' // 自定义 JSX runtime
      }
    ]
  ]
}

@babel/preset-typescript

用于转换 TypeScript。

{
  presets: [
    [
      '@babel/preset-typescript',
      {
        isTSX: true,
        allExtensions: true,
        allowNamespaces: true
      }
    ]
  ]
}

2. Plugins(插件)

插件是 Babel 的核心,每个插件负责转换特定的语法特性。

常用插件

{
  plugins: [
    // 类属性
    '@babel/plugin-proposal-class-properties',

    // 装饰器
    ['@babel/plugin-proposal-decorators', { legacy: true }],

    // 对象展开
    '@babel/plugin-proposal-object-rest-spread',

    // 可选链
    '@babel/plugin-proposal-optional-chaining',

    // 空值合并
    '@babel/plugin-proposal-nullish-coalescing-operator',

    // 动态 import
    '@babel/plugin-syntax-dynamic-import'
  ]
}

@babel/plugin-transform-runtime

用于复用 Babel 注入的辅助代码,减小打包体积。

{
  plugins: [
    [
      '@babel/plugin-transform-runtime',
      {
        corejs: 3,              // false | 2 | 3
        helpers: true,          // 是否复用辅助函数
        regenerator: true,      // 是否使用 regenerator
        useESModules: true      // 是否使用 ES 模块
      }
    ]
  ]
}

对比:

// 不使用 transform-runtime
// 每个文件都会注入辅助函数
function _classCallCheck(instance, Constructor) { /* ... */ }
function _defineProperties(target, props) { /* ... */ }

// 使用 transform-runtime
// 从统一的模块引入
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import _defineProperties from "@babel/runtime/helpers/defineProperties";

Babel 在不同场景中的使用

1. 在 Webpack 中使用

npm install -D babel-loader @babel/core @babel/preset-env
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            cacheDirectory: true
          }
        }
      }
    ]
  }
};

2. 在 Vite 中使用

Vite 默认使用 esbuild 进行转译,但也可以配置 Babel:

npm install -D @vitejs/plugin-react
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [
          ['@babel/plugin-proposal-decorators', { legacy: true }]
        ]
      }
    })
  ]
});

3. 在 Node.js 中使用

npm install -D @babel/core @babel/node @babel/preset-env
// package.json
{
  "scripts": {
    "start": "babel-node src/index.js"
  }
}

或使用 register:

// index.js
require('@babel/register')({
  presets: ['@babel/preset-env']
});

require('./app.js');

4. 命令行使用

# 转换单个文件
npx babel src/app.js --out-file dist/app.js

# 转换整个目录
npx babel src --out-dir dist

# 监听模式
npx babel src --out-dir dist --watch

# 生成 source map
npx babel src --out-dir dist --source-maps

Polyfill 策略

1. 使用 @babel/polyfill(已废弃)

// 入口文件
import '@babel/polyfill';

2. 使用 core-js + regenerator-runtime(推荐)

npm install core-js regenerator-runtime
// babel.config.js
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'usage',
        corejs: 3
      }
    ]
  ]
};

3. 使用 @babel/plugin-transform-runtime

npm install @babel/runtime-corejs3
{
  plugins: [
    [
      '@babel/plugin-transform-runtime',
      {
        corejs: 3
      }
    ]
  ]
}

对比:

方案 优点 缺点 适用场景
core-js + useBuiltIns 按需引入,体积小 污染全局 应用开发
transform-runtime 不污染全局 不支持实例方法 库开发

最佳实践

1. 针对不同环境使用不同配置

// babel.config.js
module.exports = function(api) {
  const isProduction = api.env('production');

  return {
    presets: [
      [
        '@babel/preset-env',
        {
          targets: isProduction
            ? { browsers: ['> 1%', 'last 2 versions'] }
            : { node: 'current' }
        }
      ]
    ],
    plugins: isProduction
      ? ['transform-remove-console']
      : []
  };
};

2. 启用缓存

// webpack.config.js
{
  loader: 'babel-loader',
  options: {
    cacheDirectory: true,
    cacheCompression: false
  }
}

3. 减小转译范围

{
  test: /\.js$/,
  exclude: /node_modules/,
  include: path.resolve(__dirname, 'src')
}

4. 使用 browserslist

package.json 中定义:

{
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead",
    "not ie <= 11"
  ]
}

或创建 .browserslistrc

> 1%
last 2 versions
not dead
not ie <= 11

5. 性能优化

// babel.config.js
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        // 只转换需要的特性
        targets: { esmodules: true },
        // 按需引入 polyfill
        useBuiltIns: 'usage',
        corejs: 3,
        // 不转换模块(让 webpack 处理)
        modules: false
      }
    ]
  ],
  // 缓存配置
  cacheDirectory: true,
  // 缓存标识符
  cacheIdentifier: process.env.NODE_ENV
};

常见问题

1. async/await 转换问题

问题: 使用 async/await 但运行时报错

解决:

{
  presets: [
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'usage',
        corejs: 3
      }
    ]
  ]
}

或安装 regenerator-runtime

import 'regenerator-runtime/runtime';

2. 装饰器语法问题

问题: 装饰器语法报错

解决:

{
  plugins: [
    ['@babel/plugin-proposal-decorators', { legacy: true }],
    '@babel/plugin-proposal-class-properties'
  ]
}

注意:装饰器插件必须放在 class-properties 之前。

3. 动态 import 问题

问题: 动态 import 语法无法使用

解决:

{
  plugins: [
    '@babel/plugin-syntax-dynamic-import'
  ]
}

总结

Babel 的原理就是将 JavaScript 源代码转换为抽象语法树(AST),然后对 AST 进行转换,生成与源代码功能相同但向后兼容的代码。Babel 提供了一个强大的生态系统,使得开发者可以轻松扩展并自定义转换器,实现自己的功能需求。

延伸阅读