欢迎关注公众号:前端成长指南
前言
在解决了前两篇问题之后,又遇到一个白屏的问题(实际上是路由跳转页面不能加载)。通过仔细的阅读文档、分析理解,我对 Vite 打包及其对浏览器兼容的处理有了一个新的认识。
本篇是一个总结,具体的问题不是重点。其中,对文档概念的理解很重要。另外,我借助 ChatGPT 对 plugin-legacy 的整篇文档进行了翻译。我将文档部分的理解放在前面,后面再说具体的问题。
Vite 构建文档
生产构建
这个文档英文原文没有包含最低支持 es2015 时支持的浏览器范围(黄框),但是官网中文版如上截图。
有一个最新的 issue:docs: clarify browser compatibility #19253 有修改回来,但是网站还没更新,搞不懂怎么回事。
以上官方文档说明 Vite 可以设置 build.target 进行兼容,但是由于原生 ESM 的支持,只能支持到如上的浏览器范围。而且 Vite 只处理语法转译,不包含任何 polyfill。如果需要 polyfill 可以使用 babel 或其他方式。
通过 @vitejs/plugin-legacy 插件能够解决传统浏览器的兼容问题。其中包含语法转移和 polyfill 相关的设置。
build.target:
内容和 Guide 部分差不多:
-
target默认是modules,相当于:['es2020', 'edge88', 'firefox78', 'chrome87', 'safari14']。 -
esnext是包含原生动态导入的更新更小的支持范围。 -
Vite 使用
esbuild进行语法转换,target 可以是 ES version 如es2015或者浏览器版本或者数组。 -
如果代码包含了不能被安全转换的特性,构建会失败。 我尝试了一下,在 esbuild.github.io/try/ 设置
--target=es2015转换import.meta;会报错:"import.meta" is not available in the configured target environment ("es2015") and will be empty [empty-import-meta]
@vitejs/plugin-legacy 文档
Vite 的默认浏览器支持基线是 Native ESM、native ESM dynamic import 和 import.meta。当构建生产版本时,该插件为不支持这些特性的旧版浏览器提供支持。
默认情况下,该插件会执行以下操作:
-
为最终构建中的每个 chunk 生成一个对应的 legacy chunk,使用 @babel/preset-env 进行转换,并以 SystemJS 模块 的形式输出(仍支持代码拆分)。
-
生成一个包含 SystemJS 运行时的 polyfill chunk,并根据指定的浏览器目标和实际使用情况自动注入必要的 polyfills。
-
在生成的 HTML 中注入
<script nomodule>标签,以便在不支持现代特性的浏览器中,按需加载 polyfills 和 legacy bundle。 -
注入
import.meta.env.LEGACY变量,该变量在旧版生产构建中为true,在所有其他情况下为false。
使用方法
// vite.config.js
import legacy from '@vitejs/plugin-legacy'
export default {
plugins: [
legacy({
targets: ['defaults', 'not IE 11'],
}),
],
}
由于 plugin-legacy 使用 Terser 进行代码压缩,因此必须安装 Terser:
npm add -D terser
选项
targets
- 类型:
string | string[] | { [key: string]: string } - 默认值:
'last 2 versions and not dead, > 0.3%, Firefox ESR'
如果显式设置,它将传递给 @babel/preset-env 以渲染 legacy chunks。
该查询符合 Browserslist compatible。更多详细信息请参考 Browserslist 最佳实践。
如果未设置,plugin-legacy 将加载 Browserslist config sources,然后回退到默认值。
modernTargets
- 类型:
string | string[] - 默认值:
'edge>=79, firefox>=67, chrome>=64, safari>=12, chromeAndroid>=64, iOS>=12'
如果显式设置,它将传递给 @babel/preset-env 以渲染 modern chunks。
该查询符合 Browserslist compatible。更多详细信息请参考 Browserslist 最佳实践。
如果未设置,plugin-legacy 将回退到默认值。
polyfills
- 类型:
boolean | string[] - 默认值:
true
默认情况下,会基于 target 浏览器范围和最终 bundle 中的实际使用情况(通过 @babel/preset-env 的 useBuiltIns: 'usage' 进行检测)生成 polyfills chunk。
设置为字符串数组可以显式控制要包含的 polyfills。详情请参考 Polyfill Specifiers。
设置为 false 可避免生成 polyfills,需自行处理(仍然会生成 legacy chunks 并进行语法转换)。
additionalLegacyPolyfills
- 类型:
string[]
可向 legacy polyfills chunk 添加自定义导入项。由于基于使用情况的 polyfill 检测仅涵盖 ES 语言特性,因此可能需要使用此选项手动指定额外的 DOM API polyfills。
additionalModernPolyfills
- 类型:
string[]
可向 modern polyfills chunk 添加自定义导入项。由于基于使用情况的 polyfill 检测仅涵盖 ES 语言特性,因此可能需要使用此选项手动指定额外的 DOM API polyfills。
modernPolyfills
- 类型:
boolean | string[] - 默认值:
false
默认为 false。启用此选项将为 modern build 生成单独的 polyfills chunk(适用于支持 ESM 但不支持某些现代特性的浏览器)。
如果设置为字符串数组,可以显式控制要包含的 polyfills,详情请参考 Polyfill Specifiers。
如果未设置 modernTargets,不推荐使用 true,因为 core-js@3 默认包含大量 polyfills,即使针对 Native ESM 的支持就会注入 15KB 的 polyfills!
如果你不依赖前沿的运行时特性,可以完全避免在现代构建中使用 polyfills。或者,你可以考虑设置 modernTargets,或使用 cdnjs.cloudflare.com/polyfill/ 这样的按需服务,根据浏览器的 User-Agent 仅加载必要的 polyfills(大多数现代浏览器什么都不需要!)。
renderLegacyChunks
- 类型:
boolean - 默认值:
true
设置为 false 可禁用 legacy chunks 的生成。这个选项只在使用 modernPolyfills 时有用,它允许你使用这个插件只给 modern build 注入 polyfills。
import legacy from '@vitejs/plugin-legacy'
export default {
plugins: [
legacy({
modernPolyfills: [
/* ... */
],
renderLegacyChunks: false,
}),
],
}
externalSystemJS
- 类型:
boolean - 默认值:
false
默认为 false。启用此选项将从 polyfills-legacy chunk 中排除 systemjs/dist/s.min.js。
renderModernChunks
- 类型:
boolean - 默认值:
true
设置为 false 仅输出支持所有 target 浏览器的 legacy bundles。
兼容 ESM 但不支持广泛可用特性的浏览器
该插件允许在 modern build 中使用广泛可用的特性,同时在支持 ESM 但不支持这些特性的浏览器(例如旧版 Edge)中回退到 legacy build。
此功能通过注入运行时检查,在必要时加载带有 SystemJS 运行时的 legacy bundle。然而,有以下缺点:
- 所有 ESM 浏览器都会下载 modern bundle
- 在不支持这些特性的浏览器中,现代代码会抛出
SyntaxError
以下语法被认为是广泛可用的:
- dynamic import
import.meta- async generator
Polyfill 规范
polyfills 和 modernPolyfills 可使用以下字符串:
- 任何
core-js3 子模块路径 - 例如es/map会引入core-js/es/map - 任何
core-js3 单个模块 - 例如es.array.iterator会引入core-js/modules/es.array.iterator.js
示例
import legacy from '@vitejs/plugin-legacy'
export default {
plugins: [
legacy({
polyfills: ['es.promise.finally', 'es/map', 'es/set'],
modernPolyfills: ['es.promise.finally'],
}),
],
}
几个问题
看完以上所有相关文档之后,有几个问题需要搞清楚。读者不妨先看问题思考一下。
很多人以为 Vite 解决兼容问题只要用上官方插件就行了,这是错误的理解,也许这样做之后就解决了当时面临的兼容问题,但是理解原理才是根本之道。很多文章也只是讲 plugin-legacy 是什么怎么用,没有讲清楚原理。
理解上面的文档很重要!理解上面的文档很重要!理解上面的文档很重要!
Vite build 兼容和 plugin-legacy 兼容是什么关系?
Vite 使用 esbuild 打包(Vite 的实现原理可以复习一下,这里不再赘述),打包的产物可以根据 target 进行调整。那么既然 Vite 可以打包出更兼容的代码,为什么还需要 plugin-legacy?
因为 esbuild 不能兼容到 ES5,其次虽然可以兼容到 es2015,但是仅仅是语法转译,不包含任何 polyfill。而插件是利用 babel 进行兼容处理,能尽可能的兼容我们的 target 浏览器。
至于它们生成的产物是什么关系,是否会冲突,在了解插件原理之后就会清楚。
知道这些之后,就知道 Vite build 的兼容不一定要用插件 plugin-legacy,根据 target 浏览器,自行使用 babel 也能解决。只不过 plugin-legacy 是官方插件,处理兼容问题更加方便。
什么是 polyfill,什么是语法转译?
语法转译主要用于将现代 JavaScript 语法转换为旧版 JavaScript 语法,使其能够在不支持新语法的旧版浏览器中运行。主要解决浏览器不支持 let、const、箭头函数、async/await 等语法的问题。常见的语法转译工具是 Babel,@babel/preset-env 可以根据目标环境自动转换语法。例如,将:
const sum = (a, b) => a + b;
转为:
var sum = function (a, b) {
return a + b;
};
polyfill 用于填补旧版浏览器缺失的 JavaScript API 或功能。主要解决浏览器不支持 Promise、Object.fromEntries、Array.prototype.flat 等 API 的问题。通常使用 core-js 或 @babel/polyfill 来自动引入 polyfills。例如旧版浏览器不支持 Promise.finally,解决方案是引入 core-js 提供的 polyfill:
import 'core-js/es/promise/finally';
什么是 browserslist?
Browserslist 是一个用于定义项目需要支持的浏览器范围的工具,主要用于前端构建工具(如 Autoprefixer, Babel, ESLint, PostCSS, and Webpack 等)。
通过 browsersl.ist/ 可以在线查看浏览器的支持范围,例如设置 target 为 defaults(等同于 > 0.5%, last 2 versions, not dead),则表示支持以下浏览器范围:
支持的浏览器全球市场占有率 88.9%,Chrome 最低支持到 109,如果我使用兼容插件生成兼容代码,Chrome 109 以下不支持。
区分传统浏览器和现代浏览器的标准是什么?
plugin-legacy 的判断标准是全部支持以下 3 个语法就是现代浏览器,否则是传统浏览器。
import.meta.url;import("_").catch(()=>1);(async function*(){})().next();
但是有可能,浏览器全部支持这些语法(也就是被判断为现代浏览器),却不支持其他某些被认为是现代浏览器已经广泛支持的特性。比如上一篇提到的展开运算符,WebView 内核为 Chrome 72,但却不支持展开运算符,可能是国产改造的原因。
plugin-legacy 兼容的原理是什么?
生成两套代码,modern build(使用 esbuild 生成,支持 ESM,不带 polyfill),legacy build(使用 babel 降级,转换为 ES5,并添加 core-js polyfills)。
HTML 自动注入 script type="module" 供现代浏览器使用,nomodule 供旧浏览器使用,部分 ESM 浏览器(如 Legacy Edge)用 SystemJS 兼容。
Vite是如何兼容旧版本浏览器的 这篇文章写的很好,可以参考。
plugin-legacy 只是解决传统浏览器兼容吗?
兼容插件给人的感觉就是对传统浏览器提供兼容支持,这样理解没错。但是插件不仅仅对 legacy chunks 产生影响,也能对 modern chunks 产生影响。什么时候需要对 modern chunks 做处理呢?
当我们按照文档简单的使用插件:
// vite.config.js
import legacy from '@vitejs/plugin-legacy'
export default {
plugins: [
legacy({
targets: ['defaults', 'not IE 11'],
}),
],
}
打包会生成 legacy chunks,但是此时的 modern chunks 不包含 polyfill。假设有一个浏览器是现代浏览器,但是不支持项目中使用的某个新 API,那么项目就会报错,此时需要对 modern chunks 进行兼容。
plugin-legacy 选项说明:
modern chunks 和 legacy chunks 是相对的两种构建产物。插件的设置选项也是两两相对的。
targets和modernTargets是一对,target指定 legacy chunks 的语法转译范围,modernTargets指定针对 modern 浏览器的生成的 modern chunks 语法转译范围。polyfills和modernPolyfills是一对。polyfills指定 legacy chunks 需要的垫片,默认为 true,使用 @babel/preset-env 的 useBuiltIns: 'usage',即按需引入;modernPolyfills指定 modern chunks 需要的垫片,默认为 false。renderLegacyChunks和renderModernChunks是一对。renderLegacyChunks指定是否生成 legacy chunks,默认 true;renderModernChunks指定是否生成 modern chunks,默认 false。
一个白屏的问题
背景
项目使用 Vite + Vue3,使用 @vitejs/plugin-legacy 兼容低版本浏览器。
项目中的有一个页面白屏,机型为 vivo y5s,后续发现 vivo 多款机型都有问题。
问题排查
首先在开发环境启动,发现页面打不开。
构建,pnpm run preview,打开页面,发现控制台报错:
TypeError: flat is not a function
说明此手机 WebView 是一个较低的版本,Vite 开发环境需要支持较多的新语法。有可能生产加载的是兼容版本代码(为什么是可能?)
打印 WebView navigator,显示内核为 Chrome 68。
调试构建 bundle 发现,此 WebView 被判断为现代浏览器,也就是全部支持上面提到的 3 个语法。
那么,页面加载就是 modern chunks,现代构建 Vite 只进行了语法转换,并没有 polyfill。上面分析已经说明。而 flat 方法需要 polyfill。
Vite build 设置 target: es2015,说是能兼容到 Chrome 64(中文官网构建生产版本)
flat 兼容性
可以看到浏览器兼容中,最低 Chrome 69 才开始支持 flat:
我们的 WebView 是 Chrome 68,刚好不满足。
另外需要注意 flat 在 2020 得到广泛支持,但是 Chrome 69 在 2018 年就开始支持了。
解决
配置:
modernPolyfills: ['es.array.flat'],
配置之前 build,index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>modernPolyfills</title>
<script type="module" crossorigin src="./js/entry-Z-2BFW7w.js"></script>
<link rel="modulepreload" crossorigin href="./js/.pnpm-DrVSy4BF.js">
<link rel="modulepreload" crossorigin href="./js/_plugin-vue_export-helper-Du3cLKUM.js">
<link rel="modulepreload" crossorigin href="./js/dayofdesign01-BWiILTOM.js">
<link rel="stylesheet" crossorigin href="./assets/.pnpm-qnPx-A1M.css">
<link rel="stylesheet" crossorigin href="./assets/_plugin-vue_export-helper-DAw3AN3A.css">
<link rel="stylesheet" crossorigin href="./assets/entry-Dz9Tb4eM.css">
<script type="module">import.meta.url;import("_").catch(()=>1);(async function*(){})().next();if(location.protocol!="file:"){window.__vite_is_modern_browser=true}</script>
<script type="module">!function(){if(window.__vite_is_modern_browser)return;console.warn("vite: loading legacy chunks, syntax error above and the same error below should be ignored");var e=document.getElementById("vite-legacy-polyfill"),n=document.createElement("script");n.src=e.src,n.onload=function(){System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))},document.body.appendChild(n)}();</script>
</head>
<body>
<div id="app"></div>
<script nomodule>!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",(function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()}),!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();</script>
<script nomodule crossorigin id="vite-legacy-polyfill" src="./js/polyfills-legacy-DXmxMS5x.js"></script>
<script nomodule crossorigin id="vite-legacy-entry" data-src="./js/entry-legacy-EhgMgmo3.js">System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))</script>
</body>
</html>
配置 modernPolyfills 之后,build:
<!doctype html>
<html lang="en">
<head>
<script type="module" crossorigin src="./js/polyfills-D3FdfBm4.js"></script>
<meta charset="UTF-8" />
</head>
</html>
区别只是多了最上面的一行 script module polyfill 文件。是针对 modern bundle 的 polyfill。
总结
- 首先要理解 Vite build 和兼容插件所做的工作,它们之间的区别。
Vite build会依据target对构建产物进行一定范围的语法转译。插件plugin-legacy是为了更大程度的兼容低版本浏览器。 - 语法转译(
transform)和polyfill是不同的概念。Vite build(esbuild)只对语法进行转译(transform),不包含任何垫片(polyfill)。 plugin-legacy不仅可以生成、设置 legacy chunks,而且可以设置 modern chunks。对二者是分开设置的。分别可以设置语法转译和 polyfill 的范围。target设置需要兼容的浏览器范围,遵循browserslist规范。Vite buildtarget设置 esbuild 转译的范围;plugin-legacytarget设置 legacy chunks 需要转译的语法范围,modernTargets设置 modern chunks 需要转译的语法范围。esbuild并不是所有语法都会转译,比如 BigInt 是 es2020 规范,但是 esbuild 不会转译,在 target 设置 es2020 时,原样输出;在 target 设置 es2020 以下时,build 报错。具体有哪些语法,可以参考文档。- 知道
plugin-legacy对现代浏览器的判断条件是什么,所谓的现代浏览器并非绝对支持所有的 widely-available 语法。 - 国产浏览器/WebView可能会存在:被插件判断为现代浏览器,但是对某些 widely-available features 却不支持,导致报错和页面白屏。 也就是它加载了 modern chunks,其中只是 esbuild 的转译。