你的Vite应用需要@vitejs/plugin-legacy构建Legacy包吗

背景

vite构建出来的包是针对支持Native ESM, native ESM dynamic import, 和 import.meta这些特性的浏览器的,所以如果有存量的目标用户的浏览器不支持这些特性,主要指Chrome64safari12以下的浏览器,就会出现打不开网站的情况,这时候就有必要研究一下给代码构建一份Legacy包了。

解决方案

官方插件@vitejs/plugin-legacy,这个vite插件就是为那些不支持新特性的旧版浏览器提供支持的。

方案原理

那这个插件@vitejs/plugin-legacy具体是怎么做的呢?

代码转义

看一下插件部分源码

typescript 复制代码
const legacyPostPlugin = {
  name: 'vite:legacy-post-process',
  enforce: 'post',
  apply: 'build',
  configResolved(config) {
    // 省略部分代码
    //
    //
    // 在原有输出目标上再输出一份systemjs格式的代码
    const createLegacyOutput = (options2 = {}) => {
      return {
        ...options2,
        format: 'system',
        entryFileNames: getLegacyOutputFileName(options2.entryFileNames),
        chunkFileNames: getLegacyOutputFileName(options2.chunkFileNames)
      };
    };
    const { rollupOptions } = config.build;
    const { output } = rollupOptions;
    if (Array.isArray(output)) {
      rollupOptions.output = [...output.map(createLegacyOutput), ...(genModern ? output : [])];
    } else {
      rollupOptions.output = [createLegacyOutput(output), ...(genModern ? [output || {}] : [])];
    }
  }
};

首先通过configResolved钩子,重新定义了output,在原有的基础上再输出一份SystemJS格式的代码,这种代码格式需要依赖[SystemJS]运行时(github.com/systemjs/sy...)

typescript 复制代码
function polyfillsPlugin(imports, excludeSystemJS) {
  return {
    name: "vite:legacy-polyfills",
    load(id) {
      if (id === polyfillId) {
        return [...imports].map((i) => `import ${JSON.stringify(i)};`).join("") + (excludeSystemJS ? "" : `import "systemjs/dist/s.min.js";`);
      }
    }
  };
}

可以看到在为legacy包注入polyfills的时候,额外注入了import "systemjs/dist/s.min.js";

不过这样还没有完,打包成systemjs格式的知识解决了低版本浏览器不支持Native ESM的问题,接下来还需要转义掉代码里面的native ESM dynamic import动态导入的语法。

typescript 复制代码
let babel;
async function loadBabel() {
  if (!babel) {
    babel = await import('@babel/core');
  }
  return babel;
}
async renderChunk(raw, chunk, opts) {
  const babel2 = await loadBabel();
  const result = babel2.transform(raw, {
    babelrc: false,
    configFile: false,
    compact: !!config.build.minify,
    sourceMaps,
    inputSourceMap: void 0,
    // sourceMaps ? chunk.map : undefined, `.map` TODO: moved to OutputChunk?
    presets: [
      // forcing our plugin to run before preset-env by wrapping it in a
      // preset so we can catch the injected import statements...
      [
        () => ({
          plugins: [
            recordAndRemovePolyfillBabelPlugin(legacyPolyfills),
            replaceLegacyEnvBabelPlugin(),
            wrapIIFEBabelPlugin()
          ]
        })
      ],
      [
        (await import('@babel/preset-env')).default,
        createBabelPresetEnvOptions(targets, {
          needPolyfills,
          ignoreBrowserslistConfig: options.ignoreBrowserslistConfig
        })
      ]
    ]
  });
  if (result)
    return { code: result.code, map: result.map };
  return null;
}

它通过在renderChunk钩子中用babel将代码转义掉了,至此代码降级工作已经做完了。 接下来还需要在html入口里面判断什么时候加载ESM格式的代码,什么时候加载Systemjs格式的代码

入口html文件兼容不同版本脚本资源

在介绍它是如何兼容之前先了解几个概念

script标签的type属性

所有浏览器不认识的属性值,所嵌入的内容被视为一个数据块,不会被浏览器处理。开发人员必须使用有效的 MIME 类型,但不是 JavaScript MIME 类型来表示数据块。所有其他属性,包括 src 均会被忽略。

比如在不支持 ES 模块的浏览器中,这种引用<script type="module" src=".."/>就不会被处理,src也会被忽略。MDN地址

script标签的nomodule属性

这个布尔属性被设置来标明这个脚本不应该在支持 ES 模块的浏览器中执行。实际上,这可用于在不支持模块化 JavaScript 的旧浏览器中提供回退脚本。MDN地址

link标签的rel属性

rel 属性没有默认值,在 <link> 元素上,如果 rel 属性不存在,没有关键词,或者不是当前浏览器所认识的值,那么该元素就不会创建任何链接,比如老版浏览器就不会认识modulepreload,自然也就不会处理这个元素。MDN地址

熟悉了上面的姐概念,再来看一份用该插件构建完后生成的html文件,就会很容易明白它是如何在html里面加载不同版本的脚本资源的。

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <script type="module" crossorigin src="/assets/js/polyfills-a1742436.js"></script>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=0, viewport-fit=cover"
    />
    <meta name="format-detection" content="telephone=no" />
    <title>Demo</title>
    <!--不支持ES模块的浏览器,不会加载这些脚本-->
    <script type="module" crossorigin src="/assets/js/index-fa4687ac.js"></script>
    <link rel="modulepreload" crossorigin href="/assets/js/vue-cbf56f4b.js" />
    <link rel="modulepreload" crossorigin href="/assets/js/vendor-bf7d30f9.js" />
    <link rel="stylesheet" href="/assets/css/vendor-0e3eb21a.css" />
    <link rel="stylesheet" href="/assets/css/index-443b8e38.css" />
    <script type="module">
      // 这边做了判断,浏览器支持ES模块,但是不支持import()动态导入的语法
      // __vite_is_modern_browser就不会被标记为true
      import.meta.url;
      import('_').catch(() => 1);
      async function* g() {}
      if (location.protocol != 'file:') {
        window.__vite_is_modern_browser = true;
      }
    </script>
    <script type="module">
       // 支持ES模块,但是不支持import()动态导入的语法的浏览器
       // 使用System.import加载legacy包
      !(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>
      // 这个是修复一个safari10的bug,
      // 因为safari10虽然支持ES模块,但是它还是会处理nomodule标记的脚本
      // 这么做可以避免safari10处理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>
     <!--支持ES模块的浏览器,不会加载这些脚本-->
    <!--不支持ES模块的浏览器使用System.import加载legacy包-->
    <script nomodule crossorigin id="vite-legacy-polyfill" src="/assets/js/polyfills-legacy-9a47fced.js"></script>
    <script nomodule crossorigin id="vite-legacy-entry" data-src="/assets/js/index-legacy-ac35d1f7.js">
      System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'));
    </script>
  </body>
</html>

可以看出在html中也是根据浏览器特性判断加载现代的包还是legacy的包

总结

根据分析,额外打出来的legacy包不会影响现代浏览器的用户,而且也不会对原来现代的包有侵入,所以可以根据目标用户使用的浏览器考虑是否要加这一份额外的打包,而不必担心对现代版本浏览器的目标用户造成影响。

相关推荐
Danny_FD23 分钟前
Vue2 + Node.js 快速实现带心跳检测与自动重连的 WebSocket 案例
前端
uhakadotcom23 分钟前
将next.js的分享到twitter.com之中时,如何更新分享卡片上的图片?
前端·javascript·面试
韦小勇24 分钟前
el-table 父子数据层级嵌套表格
前端
奔赴_向往26 分钟前
为什么 PWA 至今没能「掘进」主流?
前端
小小愿望26 分钟前
微信小程序开发实战:图片转 Base64 全解析
前端·微信小程序
掘金安东尼29 分钟前
2分钟创建一个“不依赖任何外部库”的粒子动画背景
前端·面试·canvas
电商API大数据接口开发Cris29 分钟前
基于 Flink 的淘宝实时数据管道设计:商品详情流式处理与异构存储
前端·数据挖掘·api
小小愿望30 分钟前
解锁前端新技能:让JavaScript与CSS变量共舞
前端·javascript·css
程序员鱼皮33 分钟前
爆肝2月,我的 AI 代码生成平台上线了!
java·前端·编程·软件开发·项目
天生我材必有用_吴用1 小时前
一文搞懂 useDark:Vue 项目中实现深色模式的正确姿势
前端·vue.js