vite 兼容性

92 阅读5分钟

Vite

背景

在开发环境下,启动服务器时,需要先对对整个应用打包处理成能在浏览器中能运行的文件,才能提供服务。当构建大的应用程序时,通常需要很长时间(甚至几分钟)才能启动开发服务器。
当源文件被修改后,基于打包的方式即使使用了模块热替换(HMR):允许模块“热替换”它自己,而不会影响页面其余部分,大大提升了开发体验。然而即使使用了HMR模式,其热更新速度也会随着应用规模的增长而显著下降。
Vite使用原生ESM的方式打包运行源码。Vite按需打包转换浏览器加载的ES模块,不需要所有的都打包处理,大大提升了启动速度。当源码文件被修改后,无论应用大小,始终能保持快速更新。

1.png

2.png

尽管ESM现在得到了广泛支持,但是如果浏览器加载使用ESM需要加载很多ES模块,会导致额外的网络往返。为了在生产环境中获得最佳的加载性能,最好还是将代码进行 tree-shaking、懒加载和 chunk 分割(以获得更好的缓存)。

Vite构建生产版本的兼容性

当需要将应用部署到生产环境时,只需运行 vite build 命令。默认情况下,它使用 <root>/index.html 作为其构建入口点,并生成能够静态部署的应用程序包。

默认情况下,生产包假定使用包含在 Baseline 广泛可用目标中的现代浏览器。默认的浏览器支持范围是:

  • Chrome >=107
  • Edge >=107
  • Firefox >=104
  • Safari >=16

你也可以通过 build.target 配置项 指定构建目标,最低支持 es2015。如果设置较低的目标值,Vite 仍然需要这些最低的浏览器支持范围,因为它依赖于原生的 ESM 动态导入和 import.meta

  • Chrome >=64
  • Firefox >=67
  • Safari >=11.1
  • Edge >=79

请注意,默认情况下 Vite 只处理语法转译,且 不包含任何 polyfill。你可以访问 cdnjs.cloudflare.com/polyfill/ ,这个网站可以根据用户的浏览器 UserAgent 字符串自动生成 polyfill 包。

传统浏览器可以通过插件 @vitejs/plugin-legacy 来支持,它将自动生成传统版本的 chunk 及与其相对应 ES 语言特性方面的 polyfill

@vitejs/plugin-legacy

支持原生ESM原生ESM动态导入import.meta特性的浏览器才能使用Vite。这个插件为不支持这些特性的传统浏览器做兼容性处理。 插件工作流程:

  • 为最每个生成的ESM模块化方式的 chunk也对应生成一个传统浏览器的兼容版本chunk,这个兼容版本的chunk@babel/preset-env转译(Vite 的内部集成了 Babel),生成SystemJS模块以供使用。
  • 生成一个polyfill chunk。这个chunk包含SystemJS运行,指定的兼容的浏览器和chunk中实际用到的一些必须的polyfills
  • 生成 <script nomodule> 标签,并注入到 HTML 文件中,用来在不兼容 ESM 的老旧浏览器中加载 polyfill 和 兼容版本chunk

配置

// vite.config.js
import legacy from '@vitejs/plugin-legacy'

export default {
  plugins: [
    legacy({
      targets: ['defaults', 'not IE 11'],
    }),
  ],
}

构建产物

Vite 打包后HTML

<html>
    <head>
        <script type="module" crossorigin src="/assets/index-a712caef.js"></script>
        <link rel="stylesheet" href="/assets/index-d853141a.css" />
        
        <script type="module">
            import.meta.url;
            import("_").catch(() => 1);
            async function* g() { }
            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>
        <script nomodule crossorigin id="vite-legacy-polyfill" src="/assets/polyfills-legacy-d5e90708.js"></script>
        <script nomodule crossorigin id="vite-legacy-entry" data-src="/assets/index-legacy-4aa958d8.js">
            System.import(
                document.getElementById("vite-legacy-entry").getAttribute("data-src")
            );
        </script>
    </body>
</html>

对于携带 type="module" 这个属性的 script 标签,不支持 ESM 的浏览器不会执行内部代码,所以报错也就不存在了,与之对应的 script 上还有 nomodule 这个属性,支持ESM的浏览器会忽略携带这个属性的 script,可以防止某些兼容逻辑在高版本浏览器执行,这两个属性组合使用就是为了决定浏览器在面对未知版本浏览器时的代码执行策略。

上面代码处理流程:

  1. 对于支持ESM的浏览器,使用ESM的方式加载JS
  2. 对于支持ESM又不支持  **import.meta.url; import("").catch(() => 1); async function g() { }_* 这三种语法之一Vite 通过 SystemJs 加载了兼容版本的chunk和对应的polyfill
  3. Vite 对低版本浏览器使用 nomodulescript 标签,加载和执行 兼容版本chunkpolyfill

在支持 ESM、同时不支持高级上述三种语法任意一种的时候, window.__vite_is_modern_browser 为 false,Vite 通过 SystemJs 加载了 legacy 文件,但也因为当前浏览器支持 ESM,致使 Vite 在第一步中通过 ESM 加载的 JS 是重复加载!
也就是说,Vite 在这种情况下同时加载了原生模块化的文件和兼容文件

但更值得思考的还在后面:不管是原生模块化的文件,还是兼容文件,他们对页面的处理逻辑是一致的,因为文件的同时加载,这会不会导致页面执行两次相同的逻辑?

答案是:不会。

Vite 是知道这种情况的,并且已经处理过了,它处理的手段你肯定会觉得很眼熟,就在整个 ESM 文件入口的前几行(也就是本文构建产物中的 index-a712caef.js )

function __vite_legacy_guard() {
    import.meta.url;
    import("_").catch(() => 1);
    async function* g() {};
};

(function polyfill() {
    // 后续其他逻辑不在这里贴了,可以使用 Vite 自行打包查看
    ...
})();

...

如果当前环境不支持高版本语法,那就在语法解析阶段报错就好了,直接暴力阻止后续逻辑的执行,因为使用了原生模块化的能力,反正错误也不会抛给外面,对页面没有什么影响!

82157686-552307f23ca5d3eb_fix732.webp