我理解的babel配置

181 阅读9分钟

配置示例

const babelPresetFlowVue = {
  plugins: [
    require('@babel/plugin-proposal-class-properties'),
    require('@babel/plugin-transform-flow-strip-types')
  ]
}

module.exports = {
  presets: [
    require('@babel/preset-env'),
    babelPresetFlowVue
  ],
  plugins: [
    require('babel-plugin-transform-vue-jsx'),
    require('@babel/plugin-syntax-dynamic-import')
  ],
  ignore: [
    'dist/*.js',
    'packages/**/*.js'
  ]
}

先看整体结构

这个配置文件主要分 3 部分:

  • presets:预设(一组插件的集合,方便批量使用)
  • plugins:单独的插件(处理特定语法)
  • ignore:指定不需要 Babel 处理的文件

一、presets 部分:预设(批量处理语法)

预设是 “打包好的一组插件”,这里有两个预设:@babel/preset-env 和自定义的 babelPresetFlowVue

1. @babel/preset-env

作用:自动把 ES6+ 新语法(如箭头函数、constPromise 等)转换成浏览器能兼容的旧语法(如 ES5)。
关键:根据你目标浏览器的版本,自动决定转换哪些语法(比如给 IE 转换箭头函数,给 Chrome 可能不转换)。

示例:不配置会怎样?
如果代码里用了箭头函数:

// 新语法:箭头函数
const add = (a, b) => a + b;
  • 旧浏览器(如 IE11)不认识 =>,会直接报错:
    语法错误:缺少 ';'(因为 IE 完全看不懂箭头函数)。

配置后会怎样?
Babel 会把箭头函数转换成旧语法的 function

// 转换后:IE11 能看懂
var add = function(a, b) {
  return a + b;
};
// 浏览器正常运行,不报错。

2. 自定义预设 babelPresetFlowVue

里面包含两个插件,专门处理 “Flow 类型注解” 和 “类的新语法”。

(1)@babel/plugin-proposal-class-properties

作用:支持类(class)里的 “直接定义属性” 语法(ES 提案语法,浏览器默认不支持)。

示例:不配置会怎样?
如果代码里这样写类:

class Person {
  // 直接在类里定义属性(新语法)
  name = "张三"; // 注意:这里没写在 constructor 里

  sayHi() {
    console.log(this.name);
  }
}
  • Babel 会报错:SyntaxError: 意外的标记 '='(因为默认不认识类里直接写 name = "张三" 的语法)。

配置后会怎样?
Babel 会把代码转换成浏览器能懂的形式:

class Person {
  constructor() {
    // 自动放到 constructor 里
    this.name = "张三";
  }

  sayHi() {
    console.log(this.name);
  }
}
// 运行正常,能正确创建 Person 实例。
(2)@babel/plugin-transform-flow-strip-types

作用:移除代码里的 Flow 类型注解(Flow 是一种类型检查工具,会写类似 : number 这样的类型标记,浏览器不认识)。

示例:不配置会怎样?
如果代码里用了 Flow 类型注解:

// Flow 类型注解:规定参数 ab 必须是数字,返回值也是数字
function add(a: number, b: number): number {
  return a + b;
}
  • 浏览器看到 a: number 会直接报错:语法错误:意外的标识符(因为浏览器不认识 : number 这种标记)。

配置后会怎样?
Babel 会移除所有类型注解,变成纯 JavaScript 代码:

// 去掉了 : number 这些标记,浏览器能看懂
function add(a, b) {
  return a + b;
}
// 运行正常,不会报错。

二、plugins 部分:单独插件(处理特定场景语法)

这里有两个插件,处理 Vue 的 JSX 和动态导入语法。

1. babel-plugin-transform-vue-jsx

作用:让 Vue 代码里的 JSX 语法(类似 <div>Hello</div>)能被转换成 Vue 能理解的代码。
(JSX 是一种在 JavaScript 里写 HTML 标签的语法,浏览器本身不认识)。

示例:不配置会怎样?
如果 Vue 组件里用了 JSX:

// Vue 组件的 render 函数里用 JSX
export default {
  render() {
    return <div>Hello Vue</div>; // JSX 语法
  }
};
  • Babel 会报错:SyntaxError: 意外的标记 '<'(因为不认识 <div> 这种在 JS 里的标签)。

配置后会怎样?
Babel 会把 JSX 转换成 Vue 的 h 函数调用(Vue 内部用来创建虚拟 DOM 的函数):

export default {
  render() {
    // 转换后:h 函数创建 div 元素
    return h('div', 'Hello Vue');
  }
};
// Vue 能正常渲染出 <div>Hello Vue</div>。

2. @babel/plugin-syntax-dynamic-import

作用:允许使用 “动态导入” 语法(import('./模块.js')),这是 ES 的新特性,用来按需加载模块(比如点击按钮后才加载某个组件)。

示例:不配置会怎样?
如果代码里用了动态导入:

// 点击按钮后才加载 ./modal.js 模块
document.getElementById('btn').addEventListener('click', () => {
  import('./modal.js').then(module => {
    module.showModal();
  });
});
  • Babel 会报错:SyntaxError: 意外的标记 import(因为默认不支持在函数里写 import(...))。

配置后会怎样?
Babel 会允许这种语法,并转换成浏览器兼容的代码(通常结合 Webpack 等工具实现按需加载):

// 转换后能正常使用动态导入,点击按钮时才加载 modal.js,不报错。
document.getElementById('btn').addEventListener('click', () => {
  __webpack_require__.e(/* import() */).then(__webpack_require__.t.bind(null, './modal.js', 7)).then(module => {
    module.showModal();
  });
});

三、ignore 部分:指定不需要处理的文件

ignore: [
  'dist/*.js',    // 忽略 dist 目录下的所有 .js 文件
  'packages/**/*.js'  // 忽略 packages 目录及子目录下的所有 .js 文件
]

转存失败,建议直接上传图片文件

作用:告诉 Babel 这些文件不需要转换(比如已经是转换过的成品文件,或者不需要兼容旧浏览器的文件)。

  • 不配置的话:Babel 会对这些文件重复转换,浪费时间,可能还会导致代码出错(比如把已经转换好的旧语法再转一遍)。
  • 配置后:跳过这些文件,提高 Babel 运行效率,避免重复处理。

为什么和presets同级已经有定义plugins了,还要在presets里定义plugins?这两者不能合并吗?

举个通俗的例子

把 Babel 配置比作「做一道菜」:

  • presets 相当于「预制调料包」(比如 “川菜调料包”,里面包含了花椒、辣椒等固定组合的调料);
  • presets 里的 plugins 就是调料包里的具体调料(花椒、辣椒);
  • 同级 plugins 相当于你额外加的调料(比如你想多加点蒜,单独放)。

你不能把调料包里的花椒、辣椒倒出来和额外加的蒜混在一起 —— 因为调料包的调料有固定比例(执行顺序),混在一起可能味道错乱(转换错误);而且下次做另一道菜需要同样的 “川菜调料包”,直接拿包用就行,不用重新买花椒辣椒(复用性)。

1. 职责分离

  • presets 里的插件:属于预设的 “内置功能”,是预设为了完成自身使命必须包含的(比如 @babel/preset-env 必须包含处理 let/const 的插件,否则就失去了转换 ES6 语法的意义)。
  • 同级 plugins:是你根据项目需求额外添加的 “个性化插件”,和预设的功能不重叠。

2. 执行顺序不同

Babel 对插件和预设的执行顺序有严格规定:

  • 同级 plugins从前往后执行(先写的先运行)。
  • presets从后往前执行(后写的先运行),且 presets 里的插件会在「预设自身的逻辑中按顺序执行」。

如果合并,会彻底打乱执行顺序。比如:
假设 presets 里的插件 A 需要在插件 B 之前执行(因为 A 处理基础语法,B 处理基于 A 的扩展语法),如果合并到同级 plugins 且顺序写错,就会导致 B 执行时 A 还没处理,出现语法转换错误(比如无法识别扩展语法)。

总结:执行顺序速查表

┌─────────────────────────────────────────────────────────────┐
│  执行顺序:                                                   │
│                                                             │
│  1. 同级 plugins 数组(从前往后)                             │
│     ├── pluginA                                              │
│     └── pluginB                                              │
│                                                             │
│  2. presets 数组(从后往前)                                 │
│     ├── presetY                                              │
│     │   └── 内部 plugins(按声明顺序)                        │
│     └── presetX                                              │
│         └── 内部 plugins(按声明顺序)                        │
└─────────────────────────────────────────────────────────────┘

这个顺序是 Babel 设计的核心规则,不能修改。配置时需根据这个顺序安排插件和预设的位置

为什么实际中不能这么做(不能合并)?

因为预设(presets不只是插件的简单集合,还可能包含其他关键配置,这些配置无法通过 “合并插件” 实现。

反例 1:带额外配置的预设(比如@babel/preset-env

@babel/preset-env是最常用的预设,它不仅包含插件,还需要配置目标环境(比如浏览器版本):

module.exports = {
  presets: [
    [require('@babel/preset-env'), { targets: ">0.25%, not dead" }] // 带targets配置
  ]
};

@babel/preset-env 不是一个 “固定的插件列表”,而是一个 “智能插件管理器” —— 它会根据你配置的 targets(目标浏览器),动态决定需要加载哪些插件,而不是包含固定的插件。这也是它和普通 “纯插件集合” 预设的本质区别。 如果你强行把它的插件拆出来放到 plugins 里,就必须手动维护这份 “动态列表”—— 这几乎不可能(你不可能知道所有浏览器支持哪些语法,也不可能每次更新浏览器版本都手动改插件)。而 targets 配置正是告诉 @babel/preset-env“如何动态选择插件” 的核心指令,拆分后自然就丢失了这个能力。

反例 2:预设的依赖关系

很多预设依赖其他预设或插件的执行结果。比如@babel/preset-react(处理 React 语法)依赖@babel/preset-env的转换结果。如果把它们的插件拆出来合并,可能会破坏依赖顺序,导致语法转换失败。

例如,React 的 JSX 语法需要先被@babel/preset-react转换,再被@babel/preset-env处理兼容性。如果合并后顺序错了,JSX 语法没被转换就交给@babel/preset-env,会报 “未知语法” 错误。

当然,如果满足以下前提:

  1. @babel/preset-react 是 “固定插件包”(即内部插件列表明确且不变);
  2. 手动排列的插件顺序完全复刻预设内部的插件执行顺序
  3. 手动为拆分后的插件配置预设内部默认的参数(如 @babel/preset-react 的 pragma 配置等);

那么 @babel/preset-react 的插件确实可以拆出来合并到外层 plugins 中,且能达到和使用预设相同的效果。

但为什么实际中没人这么做?

理论可行不代表实际推荐,核心原因是维护成本极高

  1. 预设可能更新插件@babel/preset-react 升级时可能新增 / 移除插件(例如 React 17 后支持 jsxRuntime,预设新增了 @babel/plugin-transform-react-jsx-runtime 插件),手动拆分需要同步跟进所有更新,否则会遗漏功能。
  2. 默认配置易遗漏:预设内部可能有很多隐含配置(比如 @babel/preset-react 对 development 环境的特殊处理),手动配置时容易遗漏,导致转换结果不符合预期。
  3. 顺序依赖复杂:即使是固定插件集合,插件间的依赖关系也可能很隐蔽(比如 @babel/plugin-transform-react-jsx-self 依赖 @babel/plugin-transform-react-jsx 的输出),手动排序时一旦出错就会编译失败。

总结

这个 .babelrc.js 的核心作用是:
把项目里的新语法(ES6+、JSX、Flow 类型、类属性等)转换成浏览器能看懂的代码,同时按需加载模块,让项目能在各种浏览器里正常运行。
如果不配置这些插件 / 预设,代码里的新语法会直接报错,项目根本跑不起来;配置后,所有语法都能被正确转换,项目正常运行。