Vite 代码降级完全指南:从build.target到自动化 Polyfill
前言
在前端开发中,浏览器兼容性是一个绕不开的话题。之前我曾探讨过如何用 Vite 解决低版本浏览器的白屏问题,但方案尚不完善。本文将以一个实际问题为切入点,深入剖析 Vite 项目中代码降级的两种核心方式——语法转译与 API Polyfill,并提供一套精准、自动化的终极解决方案。
一个实际问题:Object.hasOwn
引发的兼容性报错
有一次我的在开发微信H5页面应用的时候,我引入ky
这个请求库,然后我就发现,即便是最新的微信开发者工具,打开还是会报错:
Uncaught TypeError: Object.hasOwn is not a function
Object.hasOwn
是 ES2022 引入的新 API,显然,目标环境的 JavaScript 引擎并不支持它。当时,由于缺乏成熟的 Vite + Babel 插件方案,我采用了一个“快速修复”的办法:手动引入 core-js
中对应的 Polyfill。
先安装core-js
:
pnpm add core-js
然后在入口文件main.ts
中引入:
import "core-js/actual/object/has-own";
注意: 这个引入必须放在最开头
这个手动操作虽然解决了燃眉之急,但它暴露了一个更深层次的问题:在 Vite 项目中,我们应该如何系统性地处理这类兼容性需求?
vite的build.target配置
在vite的构建选项中有一个target
选项,用于表示构建目标的浏览器兼容性,不同时期的vite版本,这个选项的默认值也不同。
- vite5版本,默认值为:
'modules'
,表示只兼容到:['es2020', 'edge88', 'firefox78', 'chrome87', 'safari14']
版本的浏览器。 - vite6版本(v7.0),默认值为:
'baseline-widely-available'
,表示只兼容到:['chrome107', 'edge107', 'firefox104', 'safari16']
版本的浏览器。 - 除了默认值还有一个特殊值:
'esnext'
—— 即假设有原生动态导入支持,并只执行最低限度的转译。
由于vite在build的时候使用的是esbuild
进行编译,所以它还可以设置为esbuild的target值,为此官方还提供了esbuild的文档:target
阅读文档可以发现,esbuild的target选项可以设置的有:
具体的JavaScript 语言版本/环境,比如:
- esnext
- es2015/es6
- es2016
- es2017
- es2018
- es2019
- es2020
- es2021
- es2022
- es2023
- ...
具体的运行平台和特定版本,比如:
- chrome(如 chrome58 或 chrome58.0.3029)
- edge(如 edge16)
- firefox(如 firefox57)
- safari(如 safari11、ios11)
- node(如 node12)
- opera(如 opera45)
支持同时指定多个 target:
esbuild.build({ target: ['chrome58', 'firefox57', 'safari11', 'edge16'], })
默认值:
- esnext:默认值,表示使用最新版本的 JavaScript 语法,并使用原生动态导入支持。
通过配置选项的值,我们会发现esbuild其实是没有对低于ES2015的版本做兼容的,如果我们想兼容到es5
的版本,esbuild在文档中有提到:如果你使用了尚未支持转换的语法功能,它会在不支持的语法上抛出错误。
代码降级的两种方式
但是问题来了,当我将target
设置为es2015
的时候,build打包后,ky插件还是会报错:Object.hasOwn is not a function
,这是为什么?
这里我们就需要了解下代码降级的两种方式:
- 代码转译(Syntax Transpilation)。
- API填充(Polyfilling)。
代码转译(Syntax Transpilation)
全称是Syntax Transpilation
,是指将高版本的JavaScript语法转换为低版本浏览器能理解的等效语法。
例子:
- 可选链 (
?.
):a?.b
->a === null || a === void 0 ? void 0 : a.b
- 箭头函数 (
=>
):() => {}
->function() {}
const
/let
:const a = 1;
->var a = 1;
API填充(Polyfilling)
中文经常翻译成垫片
,是指在低版本浏览器中,提供供它们原生缺失的函数或对象的实现。
例子:
Promise
: IE11 没有Promise
对象,需要提供一个Promise
的实现。Array.prototype.flat()
: 旧版浏览器没有这个数组方法。Object.hasOwn()
: 这是一个 ES2022 的新静态方法,es2015
标准的浏览器环境里根本不存在这个函数。
esbuild 的职责范围
Vite 在构建时默认使用 esbuild 进行转译。esbuild 官方文档明确指出(target):
Note that this is only concerned with syntax features, not APIs. It does not automatically add polyfills for new APIs that are not used by these environments. You will have to explicitly import polyfills for the APIs you need (e.g. by importing core-js). Automatic polyfill injection is outside of esbuild's scope.
中文翻译:
请注意,这仅涉及语法功能,而不是 API。它不会自动添加未使用这些环境的 API 的 polyfills。您必须显式导入您需要的 API 的 polyfills(例如,通过导入 core-js )。自动 polyfill 注入超出了 esbuild 的范围。
也就是说,esbuild并不会自动为低版本浏览器添加 polyfills,需要我们手动导入 polyfills。它只做代码转译,而且只会转译ES6版本的语法,ES5语法的降级是不支持的。
为什么ky插件会报错?
我们再回过头来分析问题,为什么报错Object.hasOwn is not a function
,就是因为这是浏览器API的缺失,而不是通过转换语法就能解决的。
所以esbuild只会原样输出该代码,而不会做任何降级操作,从而导致在微信开发者工具中报错。
结论:build.target 只解决了“语法看不懂”的问题,没有解决“函数不存在”的问题。
@vitejs/plugin-legacy插件
既然 esbuild 不行,那么官方推荐的 @vitejs/plugin-legacy
插件能否成为“银弹”?它的文档声称能“自动生成传统版本的 chunk 及与其相对应 ES 语言特性方面的 polyfill”。听起来很完美。
安装插件
pnpm add @vitejs/plugin-legacy terser -D
terser是插件文档要求安装的依赖,用于压缩代码。
配置插件
import { defineConfig } from "vite"; import legacy from "@vitejs/plugin-legacy"; export default defineConfig({ plugins: [ legacy({ targets: [ "last 2 versions", "safari >= 11", "chrome >= 58", "firefox >= 54", "edge >= 16", ], }), ], build: { target: "es2015", // 为了方便查看,我们关闭代码压缩 minify: false, }, });
targets我配置了一个es2015的浏览器兼容性,这样就可以测试到ky插件的报错问题。
运行build进行构建
由于vite在server阶段,是不会使用
legacy
和build.target
的配置,所以我们需要使用打包命令来测试效果。构建完成后,我们会发现原来一个的js文件现在变成了好几个:
- index-DAY4N0oU.js
- index-legacy-DHDPLbax.js
- polyfills-legacy-c_wdFKuV.js
可以看到确实有polyfills参与,但是你得看清楚,它是带legacy的。
测试
我们通过vite自带的
preview
命令来预览dist文件,在微信开发者工具打开网页,可以看到还是报错:index-DAY4N0oU.js:288 Uncaught TypeError: Object.hasOwn is not a function
可以看到,明明html结构是正常的,有
type="module"
的script标签,也有nomodule
的script标签,为什么还是报错呢?答案很简单,就是微信开发者工具是支持
type="module"
的script标签的,所以它不会加载nomodule
的script标签,导致代码降级失败。从network中加载的js文件就可以发现问题:
所以
@vitejs/plugin-legacy
插件并不是解决问题的“银弹”,它解决的是在不支持type="module"
的浏览器中,增加了兼容方案,并且它对低版本的浏览器做了两种降级处理,也就是上面所说的两种方式:代码转译和API填充。但是对于支持
type="module"
的浏览器,它只会用到esbuild的代码转译,没有API填充,所以Object.hasOwn
方法还是不存在,还是会报错。查看legacy文件
我们打开legacy文件,可以看到它是如何做代码降级的:
// `HasOwnProperty` abstract operation // https://tc39.es/ecma262/#sec-hasownproperty // eslint-disable-next-line es/no-object-hasown -- safe hasOwnProperty_1 = Object.hasOwn || function hasOwn(it, key) { return hasOwnProperty(toObject(it), key); };
这里只截取了部分代码,但是可以看到,它是通过判断是否存在
Object.hasOwn
方法来决定是否使用polyfill,如果不存在,则使用hasOwnProperty
方法来判断。而hasOwnProperty
是ES3的标准,可以放心使用。在新版本的浏览器中,推荐使用
Object.hasOwn
,原因是因为hasOwnProperty
是原型链上的方法,是可以被重写的,而Object.hasOwn
是静态方法,不会被重写,更加安全。问题分析
究其原因是因为
@vitejs/plugin-legacy
插件默认只会处理不支持type="module"
的浏览器,所以它只会在legacy bundle中引入polyfill,而不会在现代 bundle中引入polyfill。我们需要让它在现代 bundle中引入polyfill,也就是
type="module"
的浏览器。
临时方案:手动引入polyfill
首先我们需要安装core-js
。
pnpm add core-js
精确引入,我知道我的项目中只用到了
Object.hasOwn
,所以我们只需要在入口文件引入它的polyfill。import "core-js/actual/object/has-own";
半手动,引入一个特性集合。
import "core-js/actual/promise"; // 这会引入 Promise, Promise.all, .race, .resolve, etc.
全手动,引入所有polyfill。
import "core-js/stable"; // 引入所有 polyfill
手动引入 core-js
显然不是长久之计,我们无法预知所有依赖库使用了哪些新 API。幸运的是,@vitejs/plugin-legacy
提供了更精细的配置项,让我们能够优雅地解决这个问题。
使用 plugin-legacy
实现自动化 Polyfill
关键配置项:
modernPolyfills: true
: 核心开关。当设为true
时,插件会为现代版本的 chunk 也生成一个独立的 Polyfill 文件,并自动引入。modernTargets
: 指定现代浏览器的目标环境。
import { defineConfig } from "vite";
import legacy from "@vitejs/plugin-legacy";
export default defineConfig({
plugins: [
legacy({
modernTargets: [
"last 2 versions",
"safari >= 11",
"chrome >= 58",
"firefox >= 54",
"edge >= 16",
],
modernPolyfills: true,
}),
],
build: {
target: "es2015",
// 为了方便查看,我们关闭代码压缩
minify: false,
},
});
这样我们build后,现代的js文件中会自动引入polyfill。
除了这样其实还有更多配置项,比如:
additionalLegacyPolyfills
: 添加自定义的polyfill到legacy bundle中。additionalModernPolyfills
: 添加自定义的polyfill到现代 bundle中。renderLegacyChunks
:设置为false
以禁用legacy的生成。
如果我们只需要兼容现代浏览器,也就是支持type="module"
的浏览器,我们可以将renderLegacyChunks
设置为false
,这样可以不生成legacy bundle,减少打包体积。
import { defineConfig } from "vite";
import legacy from "@vitejs/plugin-legacy";
export default defineConfig({
plugins: [
legacy({
renderLegacyChunks: false,
modernTargets: [
"last 2 versions",
"safari >= 11",
"chrome >= 58",
"firefox >= 54",
"edge >= 16",
],
modernPolyfills: true,
}),
],
build: {
target: "es2015",
// 为了方便查看,我们关闭代码压缩
minify: false,
},
});
这个配置只会生成现代代码和对应的 Polyfill,既解决了 API 兼容性问题,又保持了代码体积的精简。
总结
为 Vite 项目实现可靠的代码降级,需要遵循以下思路:
- 明确降级目标:你的应用需要兼容到哪个程度?是仅支持 ES 模块的浏览器,还是需要兼容 IE11?
区分转译和 Polyfill:
- 使用
build.target
来处理 JavaScript 语法的向后兼容。 - 使用 Polyfill 来解决 JavaScript API 的缺失问题。
- 使用
善用
@vitejs/plugin-legacy
:- 不要停留在它的默认功能,那只为最古老的浏览器服务。
- 开启
modernPolyfills: true
,为所有支持 ES 模块的浏览器提供按需 Polyfill,这是解决像Object.hasOwn
这类问题的最佳实践。 - 如果不需要支持古老浏览器,设置
renderLegacyChunks: false
来优化构建产物。
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
全部评论 1
wu先生
Google Chrome Windows 10