来自掘金小册 深入浅出 Vite 的学习实践与总结
Babel语法降级与polyfill
编译时工具
- @babel/preset-env:babel 的预设工具集,基本为 babel 必装的库
- @babel/plugin-transform-runtime:polyfill 的另一种解决方案,可以使用非全局的方式导入 polyfill,对于工具函数,也是将其变为了 import 语句进行导入
用于在代码编译阶段进行语法降级,以及添加 polyfill 代码的引用语句,例如:
import "core-js/modules/es6.set.js"
运行时工具
其中包含两个基础库:
- core-js
- regenerator-runtime
polyfill就是依赖于这两个库来运行的
babel 对上面两个库又进行了封装,就有了下面 4 个库:
- @babel/polyfill
- @babel/runtime
- @babel/runtime-corejs2
- @babel/runtime-corejs3
除了@babel/runtime中不包含 core-js,这些库都是 core-js 和 regenerator-runtime 不同版本的封装
core-js 有 3 种产物:
- core-js:全局 polyfill,以全局变量存在的形式
- core-js-pure:不会把 polyfill 注入到全局环境,可以按需引入,例如 @babel/runtime-corejs3
- core-js-bundle:打包好的产物,包含所有 polyfill
使用示例
源代码仓库地址: babel-study
@babel/preset-env解决方案
先安装一下以下的包:
- @babel/cli:babel 官方脚手架工具
- @babel/core:babel 核心编译库
- @babel/preset-env:babel 的预设工具集
针对下面一段代码进行编译,进行语法降级以及注入 polyfill:
// src/index.js
async function foo() {
console.log('金小钗');
}
new Promise(() => {
console.log('金小钗');
});
添加 babel 配置文件:
// .babelrc.json
// 使用 useBuiltIns 配置完成 polyfill 注入
{
"presets": [
[
"@babel/preset-env",
{
// 浏览器兼容版本
"targets": {
"ie": "11"
},
/*
使用 browserslist语法
含义:ie 不低于 11 版本,全球超过 0.5% 使用,且还在维护更新的浏览器
*/
// "targets": "ie >= 11, > 0.5%, not dead"
// core.js 的版本
"corejs": 3,
/*
polyfill 注入策略,可以设置为 entry | usage | false
默认值:false,即不添加 polyfill
入口文件需要主动导入 core-js,该策略会全量导入 polyfill
*/
"useBuiltIns": "entry",
// 设置为不将 ES 模块语法转换为其他模块语法
"modules": false
}
]
]
}
由于useBuiltIns的值为entry,所以需要在入口文件导入core-js:
// src/index.js
// useBuiltIns = entry 时使用,全量导入 polyfill
import 'core-js';
// 省略内容...
添加一个构建脚本并执行:"build": "babel src --out-dir dist"。产物文件中会出现一些工具函数以及导入全部的 polyfill:
按需导入 polyfill
在上面的产物中,导入了全部 polyfill,产生了很多无用的代码,下面修改配置来进行按需导入 polyfill:
// .babelrc.json
{
"presets": [
[
"@babel/preset-env",
{
// 按需导入 polyfill
"useBuiltIns": "usage",
}
]
]
}
并注释掉导入 polyfill 的语句:
// src/index.js
// import 'core-js';
构建一下,发现少了好几百行代码,因为实现了 polyfill 的按需导入:
总结
上述两种方式已经达到了语句降级与 polyfill 的注入,即可以全量导入也可以按需导入,但也存在一些问题:
- 因为是在全局环境注入 polyfill,那么开发第三方工具库时,就会存在对全局空间造成污染的问题
- 工具函数(例如
asyncGeneratorStep、_asyncToGenerator)会在每个文件都生成,导致代码冗余
transform-runtime解决方案
使用 transform-runtime 可以很好地解决上面提到的问题。
先安装一下以下的包:
- @babel/plugin-transform-runtime:编译时工具,用来转换语法和注入 polyfill
- @babel/runtime-corejs3:封装了基础库 core-js 和 regenerator-runtime
修改 babel 的配置文件:
// .babelrc.json
// 使用 plugin-transform-runtime 来完成 polyfill 注入
{
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
],
"presets": [
[
"@babel/preset-env",
{
"targets": {
"ie": "11"
},
"corejs": 3,
// 使用了 transform-runtime 后,
// 就得关闭 useBuiltIns 配置
"useBuiltIns": false,
"modules": false
}
]
]
}
执行构建脚本,可以看到无论是 polyfill 还是工具函数,都是通过具体的模块进行导入了,而不是全局注入了:
Vite语法降级与Polyfill注入
在 Vite 中如果要进行语法降级与 polyfill 注入,则可以使用 Vite 提供的官方插件 @vitejs/plugin-legacy,开箱即用。
其底层实现原理也是基于 babel 来完成的,使用了 @babel/preset-env 的 useBuiltIns 配置进行语法降级与注入polyfill。
基本使用
安装插件:
npm i @vitejs/plugin-legacy@1.7.1
配置插件:
// vite.config.ts
import legacy from '@vitejs/plugin-legacy';
export default defineConfig({
plugins: [
legacy({
// 设置目标浏览器,browserslist 配置语法
targets: ['ie >= 11'],
}),
],
});
build 一下,多了 3 个文件:
而在构建后的 index.html 中,有着支持现代与旧浏览器的两种解决方案:
<!-- index.html -->
<html lang="en">
<head>
<!-- 针对现代浏览器的产物 -->
<script type="module" crossorigin src="/assets/index.de2cfa0f.js"></script>
<link rel="modulepreload" href="/assets/vendor.f47da054.js" />
<link rel="stylesheet" href="/assets/index.cd9c0392.css" />
<script type="module"><!-- 动态导入处理代码 --></script>
</head>
<body>
<!-- 针对旧浏览器的产物 -->
<script nomodule>
<!-- 这里的代码是用来兼容 iOS nomodule 特性的 polyfill -->
</script>
<script
nomodule
id="vite-legacy-polyfill"
src="/assets/polyfills-legacy.5ee4502b.js"
></script>
<script
nomodule
id="vite-legacy-entry"
data-src="/assets/index-legacy.39c9a687.js"
>
System.import(
document.getElementById('vite-legacy-entry').getAttribute('data-src')
);
</script>
</body>
</html>
插件执行原理
简化后的插件执行流程图:
- configResolved 的主要实现代码:
// 在这里添加一个 output 配置,用于构建出一个 system 产物
const createLegacyOutput = (options = {}) => {
return {
...options,
// system 格式产物
format: 'system',
// 转换效果: index.[hash].js -> index-legacy.[hash].js
entryFileNames: getLegacyOutputFileName(options.entryFileNames),
chunkFileNames: getLegacyOutputFileName(options.chunkFileNames)
}
}
const { rollupOptions } = config.build
const { output } = rollupOptions
if (Array.isArray(output)) {
rollupOptions.output = [...output.map(createLegacyOutput), ...output]
} else {
rollupOptions.output = [createLegacyOutput(output), output || {}]
}
- renderChunk 的主要实现代码:
renderChunk(raw, chunk, opts) {
// 1. 使用 babel + @babel/preset-env 进行语法转换与 Polyfill 注入
// 2. 由于此时已经打包后的 Chunk 已经生成
// 这里需要去掉 babel 注入的 import 语句,并记录所需的 Polyfill
// 3. 最后的 Polyfill 代码将会在 generateBundle 阶段生成
}
由于场景是应用打包,这里直接使用 @babel/preset-env 的useBuiltIns: 'usage'来进行全局 Polyfill 的收集是比较标准的做法。
- generateBundle 的主要实现代码:
// 对之前收集到的 polyfill 进行统一的打包
// 主要逻辑由该函数实现
async function buildPolyfillChunk(
name,
imports
bundle,
facadeToChunkMap,
buildOptions,
externalSystemJS
) {
let { minify, assetsDir } = buildOptions
minify = minify ? 'terser' : false
// 调用 Vite 的 build API 进行打包
const res = await build({
// 根路径设置为插件所在目录
// 由于插件的依赖包含`core-js`、`regenerator-runtime`这些运行时基础库
// 因此这里 Vite 可以正常解析到基础 Polyfill 库的路径
root: __dirname,
write: false,
// 这里的插件实现了一个虚拟模块
// Vite 对于 polyfillId 会返回所有 Polyfill 的引入语句
plugins: [polyfillsPlugin(imports, externalSystemJS)],
build: {
rollupOptions: {
// 访问 polyfillId
input: {
// name 暂可视作`polyfills-legacy`
// pofyfillId 为一个虚拟模块,经过插件处理后会拿到所有 Polyfill 的引入语句
[name]: polyfillId
},
}
}
});
// 拿到 polyfill 产物 chunk
const _polyfillChunk = Array.isArray(res) ? res[0] : res
if (!('output' in _polyfillChunk)) return
const polyfillChunk = _polyfillChunk.output[0]
// 后续做两件事情:
// 1. 记录 polyfill chunk 的文件名,方便后续插入到 Modern 模式产物的 HTML 中;
// 2. 在 bundle 对象上手动添加 polyfill 的 chunk,保证产物写到磁盘中
}
可以理解为这个函数的作用是通过 renderChunk 中收集到 polyfill 代码进行打包,生成一个单独的 chunk:
需要注意的是,polyfill chunk 中除了包含一些 core-js 和 regenerator-runtime 的相关代码,也包含了 SystemJS 的实现代码,在上面的 index.html 文件中,针对旧浏览器就使用到了 System 来加载模块。
- transformIndexHtml 的主要实现代码:
{
transformIndexHtml(html) {
// 1. 插入 Polyfill chunk 对应的 <script nomodule> 标签
// 2. 插入 Legacy 产物入口文件对应的 <script nomodule> 标签
}
}
到了这里,整个流程就结束了。
注意事项: