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

背景

vite构建出来的包是针对支持Native ESM, native ESM dynamic import, 和 import.meta这些特性的浏览器的,所以如果有存量的目标用户的浏览器不支持这些特性,主要指Chrome64、safari12以下的浏览器,就会出现打不开网站的情况,这时候就有必要研究一下给代码构建一份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包不会影响现代浏览器的用户,而且也不会对原来现代的包有侵入,所以可以根据目标用户使用的浏览器考虑是否要加这一份额外的打包,而不必担心对现代版本浏览器的目标用户造成影响。

相关推荐
code2cat2 分钟前
HTML基础篇之第一张网页:从index.html到浏览器里的任务清单
前端·html
miss1 小时前
AI 改完不敢上线?OpenSpec 规约驱动开发实战
前端·代码规范·前端工程化
AiMan1 小时前
腾讯ima还是不可以导出左侧所有对话,只能单条导出,方法不对吗
前端
去伪存真1 小时前
并发量一高就装死?Agent 全链路“防猝死”手册
前端·agent
Moment1 小时前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
AiMan2 小时前
先确认能不能一次把Microsofat Copilot所有内容打包一个文件导出
前端
总要学点什么2 小时前
第97篇:三维数据加载-城市建筑白模自定义着色器——让一条光带沿高度扫上去,快慢却由帧率说了算
前端·gis·webgl·cesium·着色器·空间分析·白模
上海懒图科技2 小时前
工厂厂区电子地图定位导航:厂区人车精准定位,安全合规双保障
前端·arcgis·vue
西安小哥3 小时前
端侧跑大模型,真正的门槛不在参数量:一次 596 秒到 10 秒的压缩路径复盘
前端·人工智能
陈随易3 小时前
Cloudflare 出钱,Vite 团队挑人,44 个维护者分 30 万美元
前端·后端·程序员