前言
最近遇到一个项目,需要兼容旧版本浏览器
因为我们的项目全部采用的是Vite构建,所以Vite如何兼容旧版本浏览器成为了一个重点研究的东西。
按照Vite官网描述,@vitejs/plugin-legacy
是官方推荐的兼容插件,那么话不多说,直接引入看看效果。
使用方式
首先安装该插件
pnpm add @vitejs/plugin-legacy -D
之后在vite.config.ts
文件中引入该插件,并配置需要兼容的浏览器版本:
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import legacy from '@vitejs/plugin-legacy';
export default defineConfig({
plugins: [
vue(),
legacy({
targets: ['defaults', 'not IE 11'],
}),
],
});
最后执行pnpm run build
看下打包效果,如下所示:
可以看到,打包产物中有常规的index-7bacbbdc.js
和 index-ea6784f3.css
,除此之外,还可以看到有两个带有legacy
标识的js产物,聪明的小伙伴想必已经猜出来了,这两个文件估计就是用来兼容旧版本浏览器的了。
没错,这两个文件就是用来做旧版本浏览器兼容的
其实Vite会为当前每个文件都生成一个legacy文件,legacy文件中是通过babel转化后的兼容代码,所以Vite做兼容的本质还是通过babel实现的。
那legacy文件是怎么做兼容的呢,这两个文件中的内容又是什么呢?
带着这两个疑问我们接下来看入口文件index.html
代码分析
前置知识
在先开始看html代码之前,我们先来对 script
标签做个细节科普:
首先,script
标签存在一个type
属性,
根据MDN文档的描述,声明type="module"
在支持模块化的浏览器中,该script代码会被当做JavaScript模块进行解析。
在不支持模块化的浏览器中,该script代码会被当做数据块而不会被解析
通俗点说就是,支持模块化的浏览器会执行声明了type="module"
的代码,而不支持模块化的浏览器则不会执行。
同样,为了给旧版本浏览器添加后门,script
标签还衍生出一个nomodule
属性
在支持模块化的浏览器中会忽略带有nomodule
属性的标签,即不执行该script标签代码
而在不支持模块化的浏览器中他会认为nomodule
是一个自定义属性,不影响script标签内的代码执行
通俗点说就是,支持模块化的浏览器不会执行带有nomodule
属性的代码,而不支持模块化的浏览器则会执行。
index.html
有了这个知识储备,我们接下来就研究下index.html文件的代码吧:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
<script type="module" crossorigin src="/assets/index-7bacbbdc.js"></script>
<link rel="stylesheet" href="/assets/index-ea6784f3.css" />
<script type="module">
import.meta.url;
import('_').catch(() => 1);
async function* g() {}
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 crossorigin id="vite-legacy-polyfill" src="/assets/polyfills-legacy-3189795e.js"></script>
<script nomodule crossorigin id="vite-legacy-entry" data-src="/assets/index-legacy-151e35d2.js">
System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'));
</script>
</body>
</html>
代码分为几部分,咱们一部分一部分的来说:
引入模块化源码
在head标签中我们越过模版代码来到第一个js脚本标签,这也是项目的主入口脚本文件
<script type="module" crossorigin src="/assets/index-7bacbbdc.js"></script>
可以看到他是带有type="module"
属性的,按照前面说的,这个标签内的代码在支持模块化的浏览器中才会执行,那如果是旧版本浏览器呢?岂不是不会执行,那页面怎么显示呢?带着疑问咱们继续往下看。
来到了这段js脚本代码,如下:
<script type="module">
import.meta.url;
import('_').catch(() => 1);
async function* g() {}
if (location.protocol != 'file:') {
window.__vite_is_modern_browser = true;
}
</script>
这是什么东西啊!import
、async
这可都是es标准中很新的代码,旧版浏览器肯定不支持啊
而且这段代码看起来完全没有作用,胡写一通。
这么写,难道真不怕报错和白屏吗?坑我们广大前端开发者不成!
其实不然,注意看这段代码是写在type="module"
中的
ESM 模块有一个好处就是在type="module"
中的代码报错只会阻塞当前模块的后续逻辑,不会阻塞模块外的逻辑和页面渲染
注意,此特性并非
type="module"
所特有,只要是分开的script代码块都可以,感谢各位掘友的指出,这里之所以把代码放在type="module"
的script标签中,只是为了防止此段代码在不支持js模块化的浏览器中执行。
写个demo验证下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
console.log('代码开始执行了');
</script>
<script type="module">
console.log('代码来到了报错module中');
throw new Error();
console.log('模块代码报错,我无法执行了');
</script>
<script type="module">
console.log('外部模块代码报错,我仍然可以执行了');
</script>
</body>
</html>
执行结果:
不难发现,虽然第二部分的js脚本报错了,阻塞了其内部报错处后续的代码,但是第三部分的js脚本依然执行了,很好的验证了该理论。
好,回到刚才index.html
那段代码,即使那段很高级的语法报错了,也不会影响外部的代码执行,只是会影响__vite_is_modern_browser
参数的值,如果当前浏览器全部支持当前语法,那么此值会为true
,而只要有一个语法不支持,那这个值就是undefined
的了。
这块的作用其实就是做一个标记,标记当前浏览器是不是一个“现代浏览器”,这样更有利于后续Vite做兼容。
好,继续往下走:
<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>
又来到一个js模块,看下这块做了什么事:
- 判断下前面设置的
__vite_is_modern_browser
变量,如果为真,则直接return
- 动态创建一个
script
标签,并把src属性指向具有id = 'vite-legacy-polyfill'
元素的src属性,其实就是把下边的script重新加载一遍
<script nomodule crossorigin id="vite-legacy-polyfill" src="/assets/polyfills-legacy-3189795e.js">
</script>
- 当以上js加载完成后,使用
System
模块化系统(不知道System模块化系统的小伙伴请移步这里,其实就是在浏览器内模拟了一个模块化环境)去加载带有id = 'vite-legacy-entry'
元素的data-src属性值,其实就是加载/assets/index-legacy-151e35d2.js
脚本。
<script nomodule crossorigin id="vite-legacy-entry" data-src="/assets/index-legacy-151e35d2.js">
System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'));
</script>
- 最后将当前创建的script标签插入到文档中。
分析下这么写的原因:
首先能执行到这段代码,说明当前浏览器支持模块化语法,因为这段代码是写在带有type="module"
属性的script标签内的。
其次判断__vite_is_modern_browser
属性,如果为真,则任务当前浏览器为现在浏览器,支持全部的新语法,后续代码放心加载即可。
而如果不为真,则说明当前浏览器是处在支持模块化和最新语法之间,则需要加载兼容代码。
兼容性代码是通过后续含有 nomodule 属性的script标签引入的,这里我们已经可以确定,当前的浏览器支持模块化,所以就不会执行 带有nomodule 属性的script标签内的代码,所以Vite就使用动态创建script标签的方式引入了兼容代码
而如果当前浏览器不支持模块化,则前面的带有type="modeule"
的script标签都不会被执行,直接来到最后的兼容性标签,如下:
<script nomodule crossorigin id="vite-legacy-polyfill" src="/assets/polyfills-legacy-3189795e.js"></script>
<script nomodule crossorigin id="vite-legacy-entry" data-src="/assets/index-legacy-151e35d2.js">
System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'));
</script>
直接加载兼容代码即可。
这个过程,真可谓是精妙绝伦啊,不愧是我尤大,小弟佩服,佩服。
发现端倪
等等,等等,我好像发现了这个插件的一个bug,如果我当前的浏览器支持模块化,但是不支持最新的语法,那么就说明__vite_is_modern_browser
属性为假,那我的脚本加载顺序岂不是以下这样(注意注释):
<!DOCTYPE html>
<html lang="en">
<head>
<!-- 代码执行,加载index文件 -->
<script type="module" crossorigin src="/assets/index-7bacbbdc.js"></script>
<!-- 代码执行,报错,__vite_is_modern_browser为undefined -->
<script type="module">
import.meta.url;
import('_').catch(() => 1);
async function* g() {}
if (location.protocol != 'file:') {
window.__vite_is_modern_browser = true;
}
</script>
<!-- 代码执行,加载兼容的polyfills-legacy文件 和 index-legacy文件 -->
<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 crossorigin id="vite-legacy-polyfill" src="/assets/polyfills-legacy-3189795e.js"></script>
<!-- 不执行 -->
<script nomodule crossorigin id="vite-legacy-entry" data-src="/assets/index-legacy-151e35d2.js">
System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'));
</script>
</body>
</html>
根据刚才咱们分析的思路,index文件会执行两次啊,一次是没有做兼容的代码,一次是做了兼容的代码,同样功能的代码执行了两次,这不纯纯的bug吗,肯定有问题啊。
不急,咱们分析下入口脚本文件index-7bacbbdc.js
原来他们是做了处理的,我上面的那种情况是不会执行两遍的,原因如下:
打开 index-7bacbbdc.js 文件,文件前几行又是熟悉的代码:
他竟然把刚才那三个用来测试浏览器对新语法支持度的代码又搬了过来
如果当前浏览器支持模块化而不支持最新语法,那么在执行到 index-7bacbbdc.js 文件时,就会报错啊
又因为他是一个带有 type="module"
的script 标签,代码报错不影响模块外的代码执行,所以就会只执行 index-legacy-151e35d2.js
兼容性代码,还是只执行一次
妙啊 这个过程真的是太妙了 !!
加餐
上面我们分析了index.html加载js文件的流程,想必大家都已经清楚了入口处是如何做旧版本兼容的。
那么问题来了,我们在做Vue项目时,很多时候都是要用到路由的,而且可以动态加载路由,即做代码切分,那代码切分了之后Vite又是如何加载兼容性代码的呢?
这边小杨特意找了一个带有动态路由的项目做了下打包,结果如下:
路由相关代码,小杨做了4个动态加载路由:
打包后文件分布:
看下index.html 的入口脚本:
旧版浏览器js入口:
找到这个文件,看下里边的源代码是什么,结果如下:
可以很清楚的看到,legacy 文件中加载的动态路由也是经过legacy后的文件,所以总结下就是:
@vitejs/plugin-legacy
会生成生成两套编译代码,一套是支持现代浏览器的,一套是兼容旧版本浏览器的带legacy的文件
当浏览器为现代浏览器时,加载现代浏览器版本的文件
当浏览器不是现代浏览器时,加载带legacy的兼容文件,legacy后续加载的动态代码也是带legacy的。
总结
@vitejs/plugin-legacy
生成两套代码,一套是支持现代浏览器的高语法版本,一套是使用babel生成的带legacy标志的兼容旧版本浏览器的代码。- index.html 文件中根据当前浏览器对现代语法的支持度智能选择加载哪一套代码。
- script标签的
type="module"
和nomodule
属性的作用 - 标记有
type="module"
的script标签内的模块化代码报错只阻塞该模块内的后续代码逻辑,不影响该模块外的js脚本执行 @vitejs/plugin-legacy
生成的支持现代浏览器的入口文件顶部添加了测试语法支持度代码,为的是防止代码执行两次- 尤大真是细节怪!!!