故事背景
近期发生一起 '惊人事件',重庆某男子,晚上 7 点独自一人开着小面飞驰在解放路上,原本 2 个小时的行程,1 个小时就到达终点,小手一点,结束了订单。在体验到了赚钱的快感后,便准备拉他的好友李某下水,于是在某拉的 APP 上面发现了司机拉新送现金功能,就在此时,惊人的一幕出现了:页面消失了!嗯?它消失了,对,消失的它。
当晚狂风造作,何某紧急联系重庆警方小郑,郑警官火速出警,经过邻里走访取证,同行页面均访问正常,不存在内容消失情况。何某开始躁动起来:如果我朋友不能跟我一起来挣钱,留它何用!
第二天便找来律师小陈,经走访调查,发现该重庆男子何某,所用手机为京东 618 期间购买,售价 314,品牌型号未知 (不肯说,也不知道,只知道不是苹果),在一番非常人的询问下,陈律师确认页面的确消失了。
警方发布通告,汽销拉新,身高 155.85 毫米,在当晚 7 点丢失于解放路,警方公布照片如下:
以上故事为虚构,借用故事来描述前端白屏问题。
问题取证
陈律师一眼便知是老旧机所致,但苦证据久矣。找来伙计 (内部工具),经过 2 个小时拷问,在刺眼的聚光灯下,何某开口了:Vivo X20。
就在此时,律师陈某伙同警方快速锁定线索:
-
vivo x20 于 17 年发布,Android7.x 系统,chrome 53 (不是绝对的,版本是可以升级的)
官方 7.0 对应的是 chrome51:developer.android.com/about/versi...
-
控制台提示:no Vue instance found
-
import 语法不支持
-
globalThis 变量报错
漆黑的夜晚,星空璀璨,可它却消失不见。
解决方案
当务之急
- 解决 globalThis 问题
- Vue 加载失败问题
- Vite 项目兼容降级
Vite 现有方案
为现代浏览器而生,不仅构建快,而且打包出的文件支持 ESM,体验感极强。
html
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
script
标签添加 type=module
后,浏览器便会按模块的方式进行加载和执行,同时 main.js
中的代码也必须试使用用 import
和 export
定义导入和导出模块。如果浏览器不支持,便会忽略 type="module"
脚本,后文会解释原因。
JS 加载方式
- 直接加载:
<script src="main.js"></script>
特点:阻塞 Dom 渲染。
- Async 模式:
<script async src="main.js"></script>
特点:异步加载,多个 async 脚本,无序执行,谁先加载完成,谁先执行。
- Defer 模式:
<script defer src="main.js"></script>
特点:延迟加载,多个 defer 脚本,有序执行。
-
Module 模式:
<script type="module" src="main.js"></script>
特点:现代浏览器加载模式,异步加载,有序执行。也可以配合
importmap
一起使用,但注意importmap
必须在module
模块之前定义:html<script type="importmap"> { "imports": { "lodash": "/node_modules/lodash-es/" } } </script> <script type="module"> import { debounce } from 'lodash'; </script>
-
Preload 模式:
<link rel="preload" href="main.js"></script>
特点:提前加载。针对页面重要资源,提前加载。
-
Prefetch 模式:
<link rel="prefetch" href="main.js"></script>
特点:资源预加载。对于后续使用的资源提前加载,提升后续的打开速度。
因此在现代浏览器下可继续使用 ESM 模式加载,在传统浏览器下,需要生成 polyfill 文件。
如何降级
Vite 打包文件默认支持 ES Module
,官方提供 @vitejs/plugin-legacy 插件兼容传统浏览器。
安装插件
bash
# 安装插件
yarn add @vitejs/plugin-legacy -D
# 安装 terser
yarn add terser -D
Vite 默认基于 esbuild 压缩构建,但
legacy
插件打包传统文件时,基于 terser 压缩算法。
项目配置
ts
// vite.config.js
import legacy from '@vitejs/plugin-legacy';
import { defineConfig } from 'vite';
export default defineConfig({
build: {
target: ['es2015'],
},
plugins: [
legacy({
targets: ['chrome > 52'],
}),
],
});
默认构建目标:支持 ESM Script 标签、支持 ESM 动态导入。
注意:build 中的 target 最低支持 es2015,修改为 chrome 53 是无效的,legacy 会覆盖目标版本。
Targets 遵循 browserlist 规范:github.com/browserslis...
- last 2 versions
- not ie <= 8
- Chrome 61
- Chrome > 61
- defaults (> 0.5%, last 2 versions,Firefox ESR,not dead)
globalThis 处理
globalThis 是一个全局对象,用于统一不同 Javascript 环境下的 this
对象。
- 浏览器环境:window、self、frames、globalThis
- Node 环境:global、globalThis
globalThis 变量在低版本下不存在,因此还需要做兼容处理。
错误提示
为何代码会存在 globalThis?
一开始,我怀疑是 Vue.js 做了跨平台环境判断导致的,后来发现是项目中使用的一个插件被 vite 打包生成了 globalThis 导致的。
解决方案:
一、网页头部增加变量赋值
html
<script>
this.globalThis || (this.globalThis = this);
</script>
二、legacy 增加 polyfill
js
legacy({
targets: ['chrome > 52'],
modernPolyfills: ['es.global-this'],
});
给现代浏览器增加 globalThis 的 polyfill,打包后会生成 polyfill 文件。还是推荐使用第一种,因为这种会把 core-js 很多内容打进来。
Vue 加载问题
根据报错提示:no Vue instance found,说明 vue.js 文件未加载成功,因为里面涉及脚本报错,代码执行中断。
- 可能是 globalThis 报错引起的。
- 重新升级了 Vue3 的版本。
- 兼容降级处理后,问题自然消失。
结局
经过以上步骤后,就完美解决了老旧机型的兼容问题。
思考
- 会不会影响构建速度
- 会不会影响整体的性能
- @vitejs/plugin-legacy 是如何降级的
- @vitejs/plugin-legacy 都有哪些配置
会不会影响构建速度?
肯定会,打包后,每个 chunk 文件附带生产了一份 legacy 文件,过去打包在 40s 左右,现在增加到了 60s。
会不会影响整体性能?
我们先看一下打包的 index.html 文件,为了方便演示,我删除了不相关的代码。
html
<!doctype html>
<html lang="en" data-van-env="stg">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"
/>
<title>邀请好友</title>
<meta
name="description"
content="邀请没有车的司机或朋友成功租买车,立马得奖金,奖励无上限!"
/>
<script type="module" crossorigin src="/assets/index-fa4d14dd.js"></script>
<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 class="loading">
<img
src="https://static.huolala.cn/image/63cbe5c7aceb40104d8a0d2c4295621c3fcf00f5.svg"
alt="loading"
/>
<p>加载中...</p>
</div>
</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="/assets/polyfills-legacy-cd4abac1.js"
></script>
<script
nomodule
crossorigin
id="vite-legacy-entry"
data-src="/assets/index-legacy-b8298135.js"
>
System.import(
document.getElementById('vite-legacy-entry').getAttribute('data-src'),
);
</script>
</body>
</html>
入口文件中,有三个 module
,三个 nomodule
,他们实际上是 ES6 Module,chrome 61 就已经支持了,当前只有 IE 不支持。
分析:
- 在支持 ESM 的浏览器中,会按照模块解析该文件,同时忽略 nomodule 所在的脚本。
- 在不支持 ESM 的浏览器中,会忽略 module 所在脚本,执行 nomodule 所在脚本。
仔细思考一下,这是不是就是实现类似版本发布和回退的功能?
为什么不支持 ESM 的浏览器不会执行 module 对应的脚本?
因为 script 标签,只能解析 type="text/javascript" 脚本,如果没有指定 type 属性,则默认为 text/javascript,而 type="module" 会认为是一个无效的脚本,刚好 nomodule 没有 type 属性,此时会优先执行。
所以,对于现代浏览器来说,不会解析使用 core-js polyfill 出来的文件,因此并不会影响加载性能。但是,在低版本浏览器下面,由于引入了很多 core-js 实现的传统代码,一定程度上有所影响,但影响也很有限,因为本身 legacy 文件依然是按需加载。
不得不说,设计的真是精妙!!!
@vitejs/plugin-legacy 是如何降级的
上面已经介绍过了 type="module" 和 nomodule 的作用,接下来看一下解析过程。
现代浏览器标记
html
<script type="module">
import.meta.url;
import('_').catch(() => 1);
async function* g() {}
if (location.protocol != 'file:') {
window.__vite_is_modern_browser = true;
}
</script>
这是一坨发了疯的代码,看似毫无规律,实则暗藏凶器,这段代码就是用来检测 ESM 是否支持,比如:import、import()、async 等,如果支持,会标记 __vite_is_modern_browser 为 TRUE。
你可能会好奇,如果浏览器不支持,页面岂不是挂了?当然不是,上文已经说过了,如果浏览器不支持代码块并不会执行。
还有一种情况,type="module" 支持,但是里面的代码快报错了,比如 async 不支持,怎么办,页面会不会白屏?答案是不会,因为 type="module" 也算异步加载,里面语法报错,不会阻塞外部脚本执行。
传统浏览器加载兼容脚本
继续看一段代码:
html
<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>
<script
nomodule
crossorigin
id="vite-legacy-polyfill"
src="https://static.huolala.cn/activity/357059/assets/polyfills-legacy-cd4abac1.js"
></script>
<script
nomodule
crossorigin
id="vite-legacy-entry"
data-src="https://static.huolala.cn/activity/357059/assets/index-legacy-b8298135.js"
>
System.import(
document.getElementById('vite-legacy-entry').getAttribute('data-src'),
);
</script>
上文已经描述过,如果是现代浏览器,直接执行对应脚本,nomodule 模块全部忽略,这段代码刚好有一个标记判断,假如是现代浏览器,直接 return。如果是传统浏览器,则继续往下执行:
- 从新获取 id="vite-legacy-polyfill" 对象,再次执行对应脚本 (因为默认已经执行过一次了)。
- 再次执行的目的是为了动态加载兼容版本的 entry 文件。
- 入口文件默认设置的是 data-src 并不会立刻被解析,默认只是加载了 polyfill 对应的脚本。
- System 对象来自于 polyfill-legacy 文件,也是一个模块化系统,传统浏览器不支持 ESM,就用 System 包模拟了一套模块化系统。参考官方 Github:github.com/systemjs/sy...
@vitejs/plugin-legacy 配置
官方文档:github.com/vitejs/vite...
targets:
构建目标,获取浏览器目标以后会交给 @babel/preset-env
处理,而底层依然基于 core-js 处理,并不是 vite 自己做的降级。
编写方式同 browserlist 规范。github.com/browserslis...
polyfills:
传统浏览器对应的 polyfill。
modernPolyfills:
现代浏览器开启 polyfills。针对现代浏览器,可能出现的不兼容问题,通过此字段进行配置。官方强烈不推荐设置为 TRUE。默认为 FALSE,一旦开启为 TRUE 以后,所有的 ES 特性都会被 polyfill,增加包体积。
总结
- Vite 开启 legacy 兼容降级。
- Module 和 nomodule 使用妙处。
- globalThis 加载处理。
- Vite 降级基于 @babel/preset-env 插件集实现,最终底层基于 core-js 实现。
- 兼容模式下,vite 基于 terser 压缩算法,默认基于 esbuild 构建。
- Vite 降级后,依然实现按需加载。同时支持现代浏览器的 ESModule 加载方式,同时又能回退到传统浏览器版本。