背景
之前基于Svelte开发了一款npm插件,近期有小伙伴反馈在低版本浏览器下有脚本报错,导致影响到了页面使用。
收到反馈以后,我第一反应就是语法兼容问题,因为我基于vite打包构建的,大家都知道vite默认基于现代浏览器构建的esm 包,所以少部分低版本浏览器肯定无法解析。
一般对于有低版本浏览器有要求的C端项目,我们还是尽可能使用webpack方案或者采用最近比较火的rsbuild方案,他们天然集成babel打包配置较为容易,对于公司内部使用的后台系统尽可能考虑使用vite方案,毕竟开发速度嗖嗖的。
今天再聊一聊关于vite框架下的兼容降级方案。
方案一(项目降级)
vite官网也有解释,【下一代开发构建工具】,主要是面向现代浏览器的。对开发者极度友好,随心所欲的使用各种ES特性,不需要任何顾虑。
vite一方面本地启动贼快,另一方面打包构建效率也比webpack快。主要的原因是借助浏览器的现代特性直接使用原生导入,省去了深度编译时间,同时基于esbuild打包构建。
如果我们基于vite开发了一个H5项目,由于用户范围较广,可能会出现兼容问题,应该如何解决?
答案就是:@vitejs/plugin-legacy 做兼容降级。
这是官网推荐的降级方案,而且设计非常巧妙,我给大家简单介绍一下思路。
1. 安装插件。
// 安装插件
yarn add @vitejs/plugin-legacy -D
// 安装 terser
yarn add terser -D
2. 配置插件
// 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)
关于browserlist规范,大家自行查看官网说明就行。
3. 打包项目
打包以后,会发现,每个js文件都会对应一个xxx-legacy-xxx.js,这个就是对应的语法兼容版本,就好比不带legacy的文件里面都是const、let、Promise、箭头函数这一类语法,而legacy文件里面都是polyfill后的语法。
4. 浏览器如何加载的?
<!DOCTYPE html>
<html lang="en" data-van-env="stg">
<head>
<meta charset="UTF-8" />
<title>兼容降级方案</title>
<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>
<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>
index.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 的作用,接下来看一下解析过程。
现代浏览器标记
<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" 也算异步加载,里面语法报错,不会阻塞外部脚本执行。
传统浏览器加载兼容脚本
继续看一段代码:
<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:https://github.com/systemjs/systemjs
以上就是针对一个vite项目对应的官方降级方案和原理解析,大家认真看完,应该能感受到它的精妙之处。
方案二(组件库降级)
开头我讲了,我做的是一个组件库,所以,方案一只是铺垫一下,项目和组件库是有区别的,项目有index.html作为入口,而组件库打包以后只有一个js包,对于组件库如何降级?
vite官网并不支持对库模式进行legacy降级,所以,最后我采用了rollup的babel方案。 因为vite的线上构建其实基于rollup编译的,而rollup支持babel插件。
1. 安装插件
yarn add @babel/core @babel/preset-env @rollup/plugin-babel
2. 配置插件
// rollup.config.js
import babel from '@rollup/plugin-babel';
export default [
{
input: 'src/index.js',
output: {
file: 'dist/index.js',
name: 'index',
format: 'umd',
sourcemap: true,
},
plugins: [babel()],
},
];
我们简单打包一个index.js,输出umd格式,关键在于要使用babel插件。
注意:format对应的几个格式,如果大家不懂的一定要去查一下,搞明白,每个格式是什么意思。 非常重要。
3. babel配置
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"ie": "7"
},
"useBuiltIns": "usage",
"corejs": "3.6.5"
}
]
]
}
这些其实都是babel官网的配置,因为编译属于babel的职责,rollup只负责打包,对于ES语法转换它是不管的,需要通过plugins来集成babel的能力。
- targets 其实官方不推荐在里面配置,推荐使用
browserlist规范,我这儿感觉不用很复杂,简单配置一下,所以就不创建browserlist文件了。 - useBuiltIns 这个配置也很重要,默认是
false就是把所有新特性都打包进来,而useage会按需打包。 - corejs 对应的是标准语法的实现版本,当前最新版本是
3.x
以上配置以后,基本就能把const、let、箭头函数、Map、Set、Promise等语法进行polyfill了。
讲到这里需要说明一下,对于项目兼容,我们有一些懒人方案:
- 在早期,我们不管三七二十一,直接在项目入口(main.js)中引入
polyfill插件,比如:import '@babel/polyfill'就可以了,这个包里面已经把新特性全部实现了一遍,所以像includes等都可以直接使用。 - 由于版本迭代,现在官网推荐使用这种方式:
import 'core-js/stable';
import 'regenerator-runtime/runtime';
为什么突然要引用两个包? core-js是对新特性的标准实现,runtime'是对一些api的实现。 如果你的代码只是涉及到一些语法的转换,可以只引入core-js/stable即可。
总结
- 如果你是
vite开发的项目,可以使用官网的legacy降级方案。 - 如果你是
vite开发的组件库,可以考虑引入babel插件配合rollup实现打包编译。 - 如果你觉得
babel配置过于麻烦,可以选择懒人方案,直接在入口引入core-js/stable包即可。
对于很多传统的webpack项目,它天然集成bable所以很自然的就能实现es5的编译,所以,我觉得对于vite开发者,也很有必要搞懂这款现代构建工具的降级方案。
最后介绍一下我个人开源的低代码平台:www.marsview.cc/ 专为后台系统搭建而生,人性化的设计,操控性很好,代码已开源,github.com/JackySoft/m… 欢迎体验。
也欢迎对低代码感兴趣的同行加入我们,一起进步。