共计 7274 个字符,预计需要花费 19 分钟才能阅读完成。
作者:京东科技 孙凯
一、前言
对前端开发者来说,Vite 应该不算生疏了,它是一款基于 nobundle 和 bundleless 思维诞生的前端开发与构建工具,官网对它的概括和期待只有一句话:“下一代的前端工具链”。
Vite 最早的版本由尤雨溪公布于 3 年前,经验了 3 年多的倒退,Vite 也已逐步迭代成熟,它的稳定性、扩展性、周边生态足以在生产环境中撑持各种业务场景的落地。然而对于 Vite 的优劣势剖析咱们就戛然而止,不在深刻开展了,这不是本文的重点。
本文的重点在于探索 Vite 如何实现兼容低版本浏览器,这所有还得从那个阳光明媚的午后说起🤔。
二、那个午后
本着尝鲜的态度,我在某一个我的项目中用了 Vite,过后还是 3.x.x 的版本,跟着文档配置,从我的项目启动到我的项目构建,一路都很“德芙”(纵享丝滑),在经验了 Vite 带来的短暂新鲜感后,就始终沉迷在业务模块的开发中了,因为在 Vite 刚公布后的那段时间曾看过相干原理解析,是基于浏览器原生的模块化能力按需构建 BALABALA 等,所以起初 Vite 的这种新鲜感对我而言并没有放弃多久。
但直到有天下午我开始打包提测,审查页面元素后发现构建产物竟然跟以往 webpack 的产物居然有点不一样,在好奇心的驱使下,于是我开始尝试解谜。
三、跟 webpack 构建产物到底哪里不一样?
1. 筹备工作
为了能更好的比照两者产物到底有什么区别,咱们首先要确保咱们的业务代码基本一致,不统一的中央仅仅是应用不同工具(vite 和 webpack)进行构建,这样能力排除最大烦扰因素。
于是咱们别离应用最新版的 Vite 和 webpack 初始化了两个页面,为了做作辨别,两个页面的仅题目和题目背景不统一,他们在浏览器中渲染后的别离长这个样子:
2. 构建工具版本阐明
• Vite:v4.1.4
• webpack:v5.75.0
3. 构建工具配置项阐明
• Vite(非常简单,啥也没有)
// vite.config.js
import {defineConfig} from 'vite'
import legacy from '@vitejs/plugin-legacy'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue(),
legacy({targets: ['ios >= 9', 'android >= 4.2', '> 1%']
})
],
server: {host: '127.0.0.1'},
build: {minify: false}
})
• webpack(太多了,也比拟惯例,就不在这里贴出来全副配置项了,仅在这里配置好跟 Vite 一样的须要兼容到最低的浏览器版本)
// .browserslistrc
ios >= 9
android >= 4.2
> 1%
至此,筹备工作结束,让咱们看看两者的构建产物吧。
4. 构建产物
从产物的命名中,咱们就能多少看出些许区别,webpack 的产物比较简单,中规中矩,而 Vite 的 JS 文件岂但比 webpack 多,而且局部文件命名中还多了一个单词:legacy,百度翻译对它的解释是:遗产; 遗赠财物; 遗留; 后遗症;(计算机系统或产品)已停产的,通过翻译,或者你能够猜出来,名字中带 legacy 的文件大概率就是浏览器的兼容文件,那么事实到底是不是这样呢?
如果你足够仔细,其实你应该能够从下面 Vite 的配置项代码中嗅到一丝端倪,在 Vite 的配置文件中,有一个名为 @vitejs/plugin-legacy 的插件,它的名字也蕴含 legacy,Vite 官网中对这个插件的解释是这样的:
“传统浏览器能够通过插件 @vitejs/plugin-legacy 来反对,它将主动生成传统版本的 chunk 及与其绝对应 ES 语言个性方面的 polyfill。兼容版的 chunk 只会在不反对原生 ESM 的浏览器中进行按需加载。”
也就是说,这个插件它岂但提供了低版本浏览器的兼容能力,还提供了检测是否反对原生 ESM 的能力。那么这个插件都做了哪些事?
次要是以下三点:
- 为最每个生成的 ESM 模块化形式的 chunk 也对应生成一个 legacy chunk,同时应用 @babel/preset-env 转换(没错,Vite 的外部集成了 Babel),生成一个 SystemJS 模块,对于 SystemJS 能够看点击这里查看,它在浏览器中实现了模块化,用来加载有依赖关系的各个 chunk。
- 生成 polyfill 包,蕴含 SystemJS 的运行时,同时蕴含由要兼容的指标浏览器版本和代码中的高级语法产生的 polyfill。
- 生成 <script nomodule> 标签,并注入到 HTML 文件中,用来在不兼容 ESM 的老旧浏览器中加载 polyfill 和 legacy chunk。
如此可见,Vite 兼容低版本浏览器的能力就是来自于 @babel/preset-env 无疑了,都是生成 polyfill 和语法转换,然而这不就和 webpack 一样了么,事实是 Vite 又帮咱们多做了一层,那就是下面重复提到的原生浏览器模块化能力 ESM。
5. Vite 的原生模块化能力
咱们看看 Vite 打包后 HTML 中的内容,内容较多,我离开讲,先看 head 标签中的内容
<head>
<script type="module" crossorigin src="/assets/index-a712caef.js"></script>
<link rel="stylesheet" href="/assets/index-d853141a.css" />
<script type="module">
import.meta.url;
import("_").catch(() => 1);
async function* g() {}
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>
代码的前两行加载了入口 JS(index-a712caef.js,记住这个文件名,前面会用到)和 CSS,JS 资源应用了 ESM 的模块化形式加载,等等,嗯?JS 竟然应用了 ESM?如果以后浏览器不兼容 ESM,那这段 JS 岂不是永远不会执行?
其实这就是 ESM 模块化的能力之一,对于携带 type=”module” 这个属性的 script 标签,不反对 ESM 的浏览器不会执行外部代码,所以报错也就不存在了,与之对应的 script 上还有 nomodule 这个属性,反对 ESM 的浏览器会疏忽携带这个属性的 script,能够避免某些兼容逻辑在高版本浏览器执行,这两个属性组合应用就是为了 决定浏览器在面对未知版本浏览器时的代码执行策略,咱们画个繁难流程图了解一下:
持续往下看。
接下来的两段内联 script 标签中的内容很要害,咱们先看第一段代码,这段代码暂且命名为 代码 A :
<script type="module">
import.meta.url;
import("_").catch(() => 1);
async function* g() {}
window.__vite_is_modern_browser = true;
</script>
期初我看下面这段代码的时候,我就想:这写的都是些个什么货色!前三行都是高级 ES 语法,局部浏览器基本不兼容好嘛,这都能写上去,真不怕报错和白屏?
其实要留神 script 标签上 type=”module” 这个属性,ESM 模块化的益处之一就是,它在解决报错信息的时候,不像一般 script 一样会把谬误抛到模块内部,外部出错也不会阻塞后续逻辑的执行和页面渲染,接下来咱们验证一下这个观点,间接上代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>title</title>
</head>
<body>
<script type="module">
throw new Error('抛出一个谬误')
console.log('这段代码执行了没')
</script>
<script type="module">
console.log('代码执行了')
</script>
<script>
console.log('代码又又又执行了')
</script>
</body>
</html>
执行后果如下:
先不论代码后果的输入程序,咱们在这只看输入后果,与上述论断统一的,即谬误影响了外部模块,并中断了后续的代码逻辑,而内部不受影响。
在 Vite 生成的 HTML 中这样做的益处就是为了检测浏览器对相干语法的反对水平,如果模块中的语法不反对,就会进行执行;如果反对,那么同时打上一个标记,也就是上述示例 代码 A 的倒数第二行 —— 通过在 window 上设置全局变量(因为 ESM 模块中的变量影响不到内部)window.\_\_vite\_is\_modern\_browser = true,来标识以后浏览器是否为一个“古代浏览器”,是否反对的某些语法个性(import.meta、动静导入、异步生成器),这样能够使 Vite 后续更精确的判断该加载那些 JS。
于是接下来咱们就看到了上面这段代码:
<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>
它外部判断了 window.\_\_vite\_is\_modern\_browser 这个全局标识是否存在,如果存在,阐明上一个模块中的代码执行没有问题,间接退出;如果不存在,阐明以后浏览器不是一个“古代浏览器”,那就该加载和执行兼容文件了,于是能够看到这段代码的后半段,Vite 应用 SystemJs 加载了带有 legacy 标记的文件。
到了这里还没有完结,尽管 Vite 在个别情况下加载了兼容文件,但如果你认真看上述代码,会发现整个加载逻辑是放在领有 type=”module” 这个属性的 script 中的,在后面曾经论述过了,type=”module” 在低版本浏览器是不会执行的,换句话说就是,低版本浏览器的兼容文件并不会被加载。于是 Vite 为了低版本浏览器能失常执行业务逻辑,又做了如下操作——
以下代码来自 VIte 打包后 body 标签中的内容:
<script nomodule crossorigin id="vite-legacy-polyfill" src="/assets/polyfills-legacy-d5e90708.js"></script>
<script nomodule crossorigin id="vite-legacy-entry" data-src="/assets/index-legacy-4aa958d8.js">
System.import(document.getElementById("vite-legacy-entry").getAttribute("data-src")
);
</script>
能够看到,在低版本浏览器中 Vite 应用了带有 nomodule 属性的 script 标签,先加载了 polyfills 文件,确保后续代码中应用的 API 能正确执行,再通过 SystemJs 加载入口文件执行后续逻辑,至此,Vite 兼容旧版本浏览器的逻辑算是根本讲述结束了。
6.“魔鬼藏在细节中”
纵观 Vite 的整个加载流程,粗一看没有什么大问题,然而经不起斟酌,咱们再来捋一捋,看看还产生了什么。
第一步,Vite 在页面最开始加载了 CSS 和 JS,加载 JS 的形式是应用 ESM
第二步,Vite 判断了古代浏览器的兼容性,如果是古代浏览器,则不执行 nomodule 中的代码,也不会应用 SystemJs 加载 legacy 文件,反之亦然。
第三部,Vite 对低版本浏览器应用 nomodule 的 script 标签,加载和执行 legacy 文件。
等等,你有没有发现,第一步和第二步有些问题?
咱们后面曾经说过了,在第二步中,Vite 依据 window.\_\_vite\_is\_modern\_browser 解决了是否加载 legacy 文件的逻辑,然而这里的代码是包裹在 type=”module” 这个属性的 script 中的!问题就呈现在这里!
咱们设想一个场景:总有那么一部分浏览器反对 ESM 的同时,又不反对 _import.meta.url; import(“_”).catch(() => 1); async function g() {}_* 这三种语法之一,这是必然的,因为语法诞生的工夫不统一。
这也就导致了一个 Vite 的行为:在反对 ESM、同时不反对高级上述三种语法任意一种的时候,window.\_\_vite\_is\_modern\_browser 为 false,Vite 通过 SystemJs 加载了 legacy 文件,但也因为以后浏览器反对 ESM,以致 Vite 在第一步中通过 ESM 加载的 JS 是反复加载!
也就是说,Vite 在这种状况下同时加载了原生模块化的文件和兼容文件!
但更值得思考的还在前面:不论是原生模块化的文件,还是兼容文件,他们对页面的解决逻辑是统一的,因为文件的同时加载,这会不会导致页面执行两次雷同的逻辑?
答案是:不会。
Vite 是晓得这种状况的,并且曾经解决过了,它解决的伎俩你必定会感觉很眼生,就在整个 ESM 文件入口的前几行(也就是本文构建产物中的 index-a712caef.js)——
function __vite_legacy_guard() {
import.meta.url;
import("_").catch(() => 1);
async function* g() {};
};
(function polyfill() {
// 后续其余逻辑不在这里贴了,能够应用 Vite 自行打包查看
...
})();
...
它申明了一个函数,函数外部蕴含了高版本的语法,Vite 充分利用了 JS 语法边解析边执行的个性:如果以后环境不反对高版本语法,那就在语法解析阶段报错就好了,间接暴力阻止后续逻辑的执行,因为应用了原生模块化的能力,反正谬误也不会抛给里面,对页面没有什么影响!
怎么样,这才是残缺的 Vite 兼容计划,不得不说,真是有很多细节值得学习和思考。
对于反复加载 ESM 文件,@vitejs/plugin-legacy 是抵赖毛病存在的,这个插件在 README 中原文是这样解释的:
The legacy plugin offers a way to use widely-available features natively in the modern build, while falling back to the legacy build in browsers with native ESM but without those features supported (e.g. Legacy Edge). This feature works by injecting a runtime check and loading the legacy bundle with SystemJs runtime if needed. There are the following drawbacks:
Modern bundle is downloaded in all ESM browsers
Modern bundle throws SyntaxError in browsers without those features support
The following syntax are considered as widely-available:
dynamic import
import.meta
async generator
大略意思就是:它认为支流浏览器对这三种语法是宽泛认可的,换句话也就是说,Vite 的指标其实还是绝大部分古代浏览器,太过低端的曾经不思考了。。。
最初放出 @vitejs/plugin-legacy 的 README 地址:https://github.com/vitejs/vite/tree/main/packages/plugin-legacy#readme
四、总结
不啰嗦,间接上加载过程残缺的流程图,一百句话也不如一张图直观。
最初,实名感激各位小伙伴的观看,如果能点个赞就更好了 🙌。