谈一谈vite框架在低版本兼容降级方案

2,136 阅读8分钟

背景

之前基于Svelte开发了一款npm插件,近期有小伙伴反馈在低版本浏览器下有脚本报错,导致影响到了页面使用。

收到反馈以后,我第一反应就是语法兼容问题,因为我基于vite打包构建的,大家都知道vite默认基于现代浏览器构建的esm 包,所以少部分低版本浏览器肯定无法解析。

一般对于有低版本浏览器有要求的C端项目,我们还是尽可能使用webpack方案或者采用最近比较火的rsbuild方案,他们天然集成babel打包配置较为容易,对于公司内部使用的后台系统尽可能考虑使用vite方案,毕竟开发速度嗖嗖的。

今天再聊一聊关于vite框架下的兼容降级方案。

方案一(项目降级)

vite官网也有解释,【下一代开发构建工具】,主要是面向现代浏览器的。对开发者极度友好,随心所欲的使用各种ES特性,不需要任何顾虑。

image.png

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会覆盖目标版本。

www.vitejs.net/guide/build…

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. 打包项目

image.png

打包以后,会发现,每个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_browserTRUE

你可能会好奇,如果浏览器不支持,页面岂不是挂了?当然不是,上文已经说过了,如果浏览器不支持 代码块并不会执行。

还有一种情况,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降级,所以,最后我采用了rollupbabel方案。 因为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

以上配置以后,基本就能把constlet、箭头函数、MapSetPromise等语法进行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… 欢迎体验。

image.png

image.png

也欢迎对低代码感兴趣的同行加入我们,一起进步。