Vite兼容方案探索

故事背景

近期发生一起 '惊人事件',重庆某男子,晚上 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 中的代码也必须试使用用 importexport 定义导入和导出模块。如果浏览器不支持,便会忽略 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 会覆盖目标版本。

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)

globalThis 处理

globalThis 是一个全局对象,用于统一不同 Javascript 环境下的 this 对象。

  • 浏览器环境:window、self、frames、globalThis
  • Node 环境:global、globalThis

globalThis 变量在低版本下不存在,因此还需要做兼容处理。

caniuse.com/?search=glo...

错误提示

为何代码会存在 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 很多内容打进来。

github.com/zloirock/co...

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 加载方式,同时又能回退到传统浏览器版本。
相关推荐
慧一居士17 分钟前
flex 布局完整功能介绍和示例演示
前端
DoraBigHead19 分钟前
小哆啦解题记——两数失踪事件
前端·算法·面试
一斤代码6 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子6 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年6 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子6 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina6 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路7 小时前
React--Fiber 架构
前端·react.js·架构
伍哥的传说8 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js
qq_424409198 小时前
uniapp的app项目,某个页面长时间无操作,返回首页
前端·vue.js·uni-app