目录
问题现象
可以编译的语法
// 源代码(ES2020+)
const value = obj?.property?.value;
const result = arr?.map(x => x * 2);
// Babel 编译后(ES5)
var value = obj == null ? void 0 : obj.property == null ? void 0 : obj.property.value;
var result = arr == null ? void 0 : arr.map(function(x) { return x * 2; });
无法编译的 API
// 源代码
const arr = Array.from([1, 2, 3]);
const promise = Promise.resolve(42);
const result = Object.assign({}, obj1, obj2);
// Babel 编译后(仍然是原样)
const arr = Array.from([1, 2, 3]); // ❌ 旧浏览器不支持
const promise = Promise.resolve(42); // ❌ 旧浏览器不支持
const result = Object.assign({}, obj1, obj2); // ❌ 旧浏览器不支持
问题表现
// 在 IE 11 中运行
const arr = Array.from([1, 2, 3]);
// 错误:Array.from is not a function
const promise = Promise.resolve(42);
// 错误:Promise is not defined
核心原理
Babel 的两种转换
Babel 转换 = 语法转换(Syntax Transform) + API 填充(Polyfill)
语法转换:
- 可选链 ?.
- 空值合并 ??
- 箭头函数 =>
- 解构赋值
- 类 class
- async/await
- 等等...
API 填充:
- Array.from
- Promise
- Object.assign
- Map/Set
- String.includes
- 等等...
为什么需要区分
// 语法转换:可以转换为等价的旧语法
// 可选链 ?. → if/else 判断
// API 转换:无法转换为旧语法
// Array.from → 需要实现整个函数
// Promise → 需要实现整个类
语法转换 vs API 转换
1. 语法转换(Syntax Transform)
工作原理
// 源代码(ES2020)
const value = obj?.property?.value;
// Babel 解析为 AST
{
type: "OptionalMemberExpression",
object: obj,
property: "property",
optional: true
}
// Babel 转换为旧语法 AST
{
type: "ConditionalExpression",
test: {
type: "BinaryExpression",
operator: "==",
left: obj,
right: null
},
consequent: undefined,
alternate: {
// 继续处理...
}
}
// 生成代码(ES5)
var value = obj == null ? void 0 : obj.property == null ? void 0 : obj.property.value;
转换示例
// 1. 可选链
obj?.property
// 转换为
obj == null ? void 0 : obj.property
// 2. 空值合并
value ?? 'default'
// 转换为
value != null ? value : 'default'
// 3. 箭头函数
const fn = (x) => x * 2
// 转换为
var fn = function(x) { return x * 2; }
// 4. 解构赋值
const { a, b } = obj
// 转换为
var a = obj.a;
var b = obj.b;
// 5. 类
class MyClass {}
// 转换为
function MyClass() {}
2. API 转换(需要 Polyfill)
为什么无法转换
// Array.from 的实现
Array.from = function(arrayLike, mapFn, thisArg) {
var C = this;
var items = Object(arrayLike);
if (arrayLike == null) {
throw new TypeError('Array.from requires an array-like object');
}
var mapFunction = mapFn !== undefined;
var T;
if (typeof mapFn !== 'function') {
throw new TypeError('Array.from: when provided, the second argument must be a function');
}
var len = ToLength(items.length);
var A = IsCallable(C) ? Object(new C(len)) : new Array(len);
var k = 0;
var kValue;
while (k < len) {
kValue = items[k];
if (mapFunction) {
A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k);
} else {
A[k] = kValue;
}
k += 1;
}
A.length = len;
return A;
};
// 这是一个完整的函数实现,不是语法转换
// 需要添加到全局对象上
API 转换的本质
// API 转换 = 添加实现代码(Polyfill)
// 源代码
Array.from([1, 2, 3]);
// 需要添加
if (!Array.from) {
Array.from = function(arrayLike) {
// 实现代码...
};
}
// 然后才能使用
Array.from([1, 2, 3]);
为什么无法编译 API
1. 语法 vs 实现
语法转换
// 语法:语言的结构
obj?.property
// 可以转换为等价的旧语法
obj == null ? void 0 : obj.property
// 转换是结构性的,不改变功能
API 转换
// API:具体的功能实现
Array.from([1, 2, 3])
// 需要实现整个 Array.from 函数
// 不是语法转换,而是添加代码
// 转换是功能性的,需要完整实现
2. AST 转换的局限性
// Babel 的 AST 转换只能处理语法结构
// 可以转换:
const value = obj?.property;
// AST: OptionalMemberExpression → ConditionalExpression
// 无法转换:
Array.from([1, 2, 3]);
// AST: CallExpression
// 无法知道 Array.from 需要什么实现
// 只能保持原样
3. 运行时 vs 编译时
// 语法转换:编译时完成
const value = obj?.property;
// 编译时转换为旧语法
// API 转换:需要运行时支持
Array.from([1, 2, 3]);
// 编译时无法知道运行环境是否有 Array.from
// 需要运行时检查并添加
4. 全局对象修改
// API 转换需要修改全局对象
// 需要添加:
Array.from = function() { ... };
Promise = function() { ... };
Object.assign = function() { ... };
// 这些是运行时操作,不是编译时转换
解决方案
方案 1:使用 @babel/polyfill(已废弃)
// ❌ 不推荐:已废弃
import '@babel/polyfill';
// 问题:
// 1. 包含所有 polyfill,体积大
// 2. 污染全局对象
// 3. 无法按需加载
方案 2:使用 core-js(推荐)
安装
npm install --save core-js@3
配置 babel.config.js
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage', // 按需引入
corejs: 3, // 使用 core-js 3
targets: {
browsers: ['> 0.5%', 'last 2 versions', 'not dead']
}
}]
]
};
工作原理
// 源代码
const arr = Array.from([1, 2, 3]);
const promise = Promise.resolve(42);
// Babel 自动添加 polyfill
import 'core-js/modules/es.array.from';
import 'core-js/modules/es.promise';
const arr = Array.from([1, 2, 3]);
const promise = Promise.resolve(42);
方案 3:使用 @babel/plugin-transform-runtime
安装
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime
配置
// babel.config.js
module.exports = {
plugins: [
['@babel/plugin-transform-runtime', {
corejs: 3, // 使用 core-js 3
helpers: true,
regenerator: true
}]
]
};
工作原理
// 源代码
const arr = Array.from([1, 2, 3]);
// 转换为
import _Array$from from '@babel/runtime-corejs3/core-js/array/from';
const arr = _Array$from([1, 2, 3]);
// 优势:不污染全局对象
方案 4:手动引入 polyfill
在入口文件引入
// src/index.js
import 'core-js/stable';
import 'regenerator-runtime/runtime';
// 或者按需引入
import 'core-js/features/array/from';
import 'core-js/features/promise';
// 然后使用
const arr = Array.from([1, 2, 3]);
const promise = Promise.resolve(42);
方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| core-js + useBuiltIns: 'usage' | 按需引入,体积小 | 需要配置 | 推荐使用 |
| @babel/plugin-transform-runtime | 不污染全局 | 配置复杂 | 库开发 |
| 手动引入 | 完全控制 | 需要手动管理 | 特殊需求 |
实际案例
案例 1:完整配置示例
package.json
{
"dependencies": {
"core-js": "^3.32.0"
},
"devDependencies": {
"@babel/core": "^7.22.0",
"@babel/preset-env": "^7.22.0"
}
}
babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: {
version: 3,
proposals: true
},
targets: {
browsers: [
'> 0.5%',
'last 2 versions',
'not dead',
'not IE 11' // 如果需要支持 IE 11,移除这行
]
},
debug: false // 设置为 true 可以看到引入了哪些 polyfill
}]
]
};
源代码
// src/index.js
const arr = Array.from([1, 2, 3]);
const promise = Promise.resolve(42);
const result = Object.assign({}, { a: 1 }, { b: 2 });
const value = obj?.property?.value;
编译结果
// 自动引入的 polyfill
import 'core-js/modules/es.array.from.js';
import 'core-js/modules/es.promise.js';
import 'core-js/modules/es.object.assign.js';
// 转换后的代码
var arr = Array.from([1, 2, 3]);
var promise = Promise.resolve(42);
var result = Object.assign({}, { a: 1 }, { b: 2 });
var value = obj == null ? void 0 : obj.property == null ? void 0 : obj.property.value;
案例 2:支持 IE 11
babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3,
targets: {
ie: '11' // 支持 IE 11
}
}]
]
};
需要的 polyfill
// IE 11 需要大量 polyfill
import 'core-js/modules/es.array.from.js';
import 'core-js/modules/es.promise.js';
import 'core-js/modules/es.object.assign.js';
import 'core-js/modules/es.string.includes.js';
import 'core-js/modules/es.array.includes.js';
// ... 更多 polyfill
案例 3:使用 transform-runtime
babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
// 不在这里配置 core-js
}]
],
plugins: [
['@babel/plugin-transform-runtime', {
corejs: 3,
helpers: true,
regenerator: true
}]
]
};
转换结果
// 源代码
const arr = Array.from([1, 2, 3]);
// 转换后(不污染全局)
import _Array$from from '@babel/runtime-corejs3/core-js/array/from';
var arr = _Array$from([1, 2, 3]);
最佳实践
1. 推荐配置
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage', // 按需引入
corejs: {
version: 3,
proposals: true // 支持提案阶段的特性
},
targets: {
browsers: ['> 0.5%', 'last 2 versions', 'not dead']
},
debug: false // 开发时可以设为 true 查看引入的 polyfill
}]
]
};
2. 库开发配置
// babel.config.js(库开发)
module.exports = {
presets: [
['@babel/preset-env', {
modules: false // 保留 ES modules
}]
],
plugins: [
['@babel/plugin-transform-runtime', {
corejs: 3,
helpers: true,
regenerator: true
}]
]
};
3. 检查引入的 polyfill
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3,
debug: true // 查看引入了哪些 polyfill
}]
]
};
// 构建时会输出:
// [BABEL] Note: The code generator has deoptimised the styling of "..." as it exceeds the max of "500KB".
// Using polyfills: Array.from, Promise, Object.assign
4. 优化 polyfill 体积
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3,
targets: {
// 提高目标浏览器版本,减少 polyfill
browsers: [
'Chrome >= 60',
'Firefox >= 60',
'Safari >= 12',
'Edge >= 79'
]
}
}]
]
};
5. 排除不需要的 polyfill
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3,
exclude: [
// 排除某些 polyfill(如果确定不需要)
'es.array.iterator',
'es.map'
]
}]
]
};
常见问题
问题 1:仍然报错 "Array.from is not a function"
原因
- polyfill 没有正确引入
- core-js 版本不匹配
- useBuiltIns 配置错误
解决方案
// 1. 检查 core-js 版本
npm list core-js
// 2. 确保 babel.config.js 配置正确
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage', // 必须是 'usage'
corejs: 3 // 必须指定版本
}]
]
};
// 3. 检查是否在入口文件之前引入
// 确保 polyfill 在代码之前加载
问题 2:polyfill 体积过大
原因
- 目标浏览器版本太低
- 引入了不必要的 polyfill
解决方案
// 1. 提高目标浏览器版本
targets: {
browsers: ['> 0.5%', 'last 2 versions'] // 移除 'not dead'
}
// 2. 排除不需要的 polyfill
exclude: ['es.array.iterator', 'es.map']
// 3. 使用 transform-runtime(库开发)
// 不污染全局,按需引入
问题 3:polyfill 污染全局对象
原因
- 使用 useBuiltIns: 'usage' 会修改全局对象
解决方案
// 使用 transform-runtime
module.exports = {
plugins: [
['@babel/plugin-transform-runtime', {
corejs: 3
}]
]
};
// 这样不会污染全局对象
问题 4:TypeScript 项目中的问题
原因
- TypeScript 编译和 Babel 转换冲突
解决方案
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020", // 让 Babel 处理转换
"module": "ESNext"
}
}
// babel.config.js
module.exports = {
presets: [
'@babel/preset-typescript',
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3
}]
]
};
总结
核心原理
-
语法转换:
- Babel 可以转换语法结构
- 转换为等价的旧语法
- 编译时完成
-
API 转换:
- Babel 无法转换 API
- 需要添加实现代码(polyfill)
- 运行时完成
-
为什么无法转换 API:
- API 是功能实现,不是语法结构
- 需要修改全局对象
- 需要运行时支持
解决方案
// 推荐配置
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage', // 按需引入
corejs: 3, // 使用 core-js 3
targets: {
browsers: ['> 0.5%', 'last 2 versions', 'not dead']
}
}]
]
};
关键点
- 语法转换:Babel 自动处理
- API 转换:需要配置 polyfill
- 推荐方案:core-js + useBuiltIns: 'usage'
- 库开发:使用 transform-runtime
检查清单
□ 安装了 core-js
□ 配置了 useBuiltIns: 'usage'
□ 指定了 corejs 版本
□ 配置了 targets
□ 检查了引入的 polyfill
□ 优化了目标浏览器版本