说到兼容旧浏览器,很多人第一反应是 Babel。但 Babel 只解决语法问题,解决不了API问题——Promise、Array.prototype.includes、Object.assign 这些新方法在旧浏览器里根本不存在,语法转译再完美也变不出来。补上这些运行时能力的,就是 core-js。
一、为什么需要 core-js
先分清两类「新特性」:
第一类:语法(syntax)
const fn = (a = 1) => a; // 箭头函数、默认参数
旧浏览器不认识这些写法,会直接语法报错,页面完全跑不起来。这类由 Babel 转译解决——把新语法改写成旧语法。
第二类:内置 API(built-in)
Promise.resolve();
[1, 2, 3].includes(2);
Object.assign({}, { a: 1 });
'abc'.padStart(5, '0');
async function foo() {}
这些都是运行时方法,旧浏览器可能压根没有 Promise,甚至连 Array.prototype.includes 都是 undefined。语法转译帮不了它们——Babel 会原样保留 [1,2,3].includes(2),然后运行时抛出 includes is not a function。
要补上这类能力,就必须在运行时先定义好这些方法,也就是 polyfill。core-js 就是目前最主流、最完整的 polyfill 库。
二、core-js 是什么
core-js 是由 Denis Pushkarev(zloirock)维护的 JavaScript 标准库 polyfill 实现,特点是:
- 覆盖全面:从 ES5 到最新标准,包括 Promise、Symbol、Map/Set、迭代器、各种数组/字符串/对象方法、
URL、queueMicrotask等。 - 按模块组织:可以全量引入,也可以精细到单个特性。
- 支持提案特性:除稳定特性外,还提供
proposals目录下的实验性特性。 - 被广泛依赖:Babel、主流框架及大量工具链都基于它做 polyfill。
它的引入粒度大致是:
core-js/stable // 所有稳定特性
core-js/es // 同上,仅 ES 标准
core-js/features // 稳定 + 提案
core-js/es/array/from // 单个特性
由于是污染全局的 polyfill(直接修改 Promise、Array.prototype 等),它能透明地让旧环境像新环境一样工作。
三、怎么用
1. 最直接:手动全量引入
// src/index.js 顶部
import 'core-js/stable';
这样所有稳定特性都会被补上,简单但体积最大——很多用不到的特性也一起进来了。
2. 按需手动引入
import 'core-js/es/promise';
import 'core-js/es/array/includes';
import 'core-js/es/object/assign';
体积可控,但需要人工判断用了哪些特性,维护成本高。
3. 配合 @babel/preset-env 自动引入(推荐)
这才是实际项目最常用的方式:Babel 根据代码里实际使用的特性和 browserslist 的目标环境,自动注入所需的 core-js 模块。
npm install core-js --save-dev
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage', // 按使用情况自动引入
corejs: '3.37', // 指定 core-js 版本
},
],
],
},
},
},
],
},
};
useBuiltIns 的取值:
| 取值 | 行为 | 说明 |
|---|---|---|
false | 不自动引入 polyfill | 默认,需要自己管理 |
'entry' | 在入口处按目标环境引入所需 polyfill | 需手动写 import 'core-js/stable',Babel 会精简它 |
'usage' | 按代码实际使用逐文件引入 | 最省体积,推荐应用项目使用 |
⚠️ corejs 选项必须与安装的 core-js 主版本匹配。使用 core-js@3 时写 corejs: 3 或 corejs: '3.37';如果写错,Babel 会给出警告,或引入错误的模块。
4. 不污染全局:transform-runtime
useBuiltIns 会修改全局对象。如果你开发的是类库/组件库,直接污染使用者的全局环境是不可接受的。这时改用 @babel/plugin-transform-runtime + @babel/runtime-corejs3:
npm install @babel/runtime-corejs3 --save-dev
npm install @babel/plugin-transform-runtime --save-dev
{
presets: ['@babel/preset-env'],
plugins: [
[
'@babel/plugin-transform-runtime',
{
corejs: 3, // 使用 core-js@3 做 polyfill
helpers: true, // 复用 Babel helper,减小体积
},
],
],
}
它会以模块局部引用的方式提供 polyfill,而不是改写全局,适合库作者。
5. 三种方式对比
| 方式 | 是否污染全局 | 体积 | 适用场景 |
|---|---|---|---|
手动 import 'core-js/stable' | 是 | 大 | 快速验证、老项目 |
useBuiltIns: 'usage' | 是 | 小(按需) | 应用项目(推荐) |
transform-runtime + corejs | 否 | 较小 | 类库、组件库 |
注意:不要同时为同一个文件同时使用
useBuiltIns和transform-runtime的 corejs polyfill,会造成重复引入和体积浪费,Babel 也会提示冲突。
四、和 browserslist 的配合
Babel 判断「需要补哪些 polyfill」的依据,正是 browserslist。目标环境越新,注入的 polyfill 越少:
{
"browserslist": [
"> 0.5%",
"last 2 versions",
"not dead"
]
}
如果目标里包含很老的浏览器,polyfill 会明显增多;反之,只面向现代浏览器时,core-js 的引入量会大幅减少。所以浏览器兼容范围是控制体积的关键杠杆。
五、注意事项
- core-js 有版本区分。core-js@2 与 @3 的模块路径不同(
core-js/es6vscore-js/es),确认corejs选项与实际安装版本一致。 regenerator-runtime:使用 generator /async/await时,除了 core-js,还需要 regenerator 运行时。useBuiltIns: 'usage'/'entry'通常会帮你自动引入,也可手动import 'regenerator-runtime/runtime'。- polyfill 必须早于业务代码执行。用
'usage'时由 Babel 自动保证顺序;手动引入时一定要放在入口最前面。 - 不要重复引入。同一份 polyfill 被多个地方引入会增大体积,尽量统一由 preset-env 或入口管理。
- 有副作用的模块。core-js 属于有副作用的 polyfill,如果你配置了 Tree Shaking 的
sideEffects,注意不要把它误删(一般作为依赖引入的包不受项目sideEffects影响)。 - 维护现状:core-js 由个人长期维护,是否继续依赖可按团队情况评估。它仍是当前生态的事实标准,短期内没有等量替代品。
六、完整配置示例(应用项目)
// webpack.config.js
const path = require('path');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js',
clean: true,
},
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
presets: [
[
'@babel/preset-env',
{
// 保留 ESM,交给 webpack 做 Tree Shaking
modules: false,
useBuiltIns: 'usage',
corejs: '3.37',
},
],
],
},
},
},
],
},
};
package.json:
{
"browserslist": ["> 0.5%", "last 2 versions", "not dead"],
"devDependencies": {
"core-js": "^3.37.0"
}
}
小结
| 问题 | 答案 |
|---|---|
| Babel 能补 Promise 吗 | 不能,Babel 只管语法 |
| 谁负责补 API | core-js |
| 应用项目怎么用 | @babel/preset-env + useBuiltIns: 'usage' + corejs: 3 |
| 库项目怎么用 | @babel/plugin-transform-runtime + @babel/runtime-corejs3 |
| 体积怎么控制 | 靠 browserslist + usage 按需引入 |
| 注意什么 | 版本匹配、regenerator、避免重复引入 |
一句话:Babel 负责「让新语法能跑」,core-js 负责「让新 API 存在」。 两者结合,再加上合理的 browserslist,才能既兼容旧环境,又不让产物体积失控。