背景
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
包不会影响现代浏览器的用户,而且也不会对原来现代
的包有侵入,所以可以根据目标用户使用的浏览器考虑是否要加这一份额外的打包,而不必担心对现代版本浏览器的目标用户造成影响。