vite-@vitejs/plugin-legacy源码阅读
vitelegacypolyfill
@vitejs/plugin-legacy
作为兼容不支持原生ESM的方案插件,其根据构建目标生成对应polyfill的插件设计思路很值得学习特此记录。
用法及参数说明
ts
// ...export default { plugins: [ legacy({ // 构建目标,最终由@babel/preset-env处理 // 自定义可使用browserslist数组形式: ['last 2 versions nad not dead', > 0.3%, 'Firefox ESR'] targets: 'defaults', // 默认true,使用@babel/preset的useBuiltIns: 'usage' // 自定义可使用polyfill specifiers数组形式:['es.promise.finally', 'es/map', 'es/set', ...] // false时则生成任何polyfill polyfills: true, // 添加自定义导入至基于es语言功能生成的polyfill中,例如额外的DOM API polyfill additionalLegacyPolyfills: ['resize-observer-polyfill'], // 忽略@babel/preset-env的browserslist检测 ignoreBrowserslistConfig: false, // 默认为false,为true时会会分开生成modern polyfill // 自定义可使用polyfill specifiers数组形式:['es.promise.finally', 'es/map', 'es/set', ...] modernPolyfills: false, // 默认为true,一般用在使用modernPolyfill为现代语法构建注入polyfill时设置为false renderLegacyChunks: true, // 默认为false,为true时systemjs/dist/s.min.js将不会包含在polyfills-legacy块中 externalSystemJS: false }) ]}
viteLegacyPlugin流程
从入口文件默认导出了由三个内部插件组成分别处理对应阶段分别config
阶段的legacyConfigPlugin
> generateBundle
阶段的legacyGenerateBundlePlugin
> 以及后置处理的legacyPostPlugin
,以下会按照流程顺序依次解释:
ts
// ...function viteLegacyPlugin(options: Options = {}): Plugin[] { // ... const legacyConfigPlugin: Plugin = { // ... } const legacyGenerateBundlePlugin: Plugin = { // ... } const legacyPostPlugin: Plugin = { // ... } return [legacyConfigPlugin, legacyGenerateBundlePlugin, legacyPostPlugin]}// ...export default viteLegacyPlugin
在vite-config.ts中调用实际形式如下:
ts
export default { plugins: [ // legacy({ targets: 'last 2 versions' }) { name: 'vite:legacy-config', config: () => {/**/}, configResolved: () => {/**/} }, { name: 'vite:legacy-generate-polyfill-chunk', apply: 'build', generateBundle: () => {/**/} }, { name: 'vite:legacy-post-process', enforce: 'post', apply: 'build', configResolved: () => {/**/}, renderChunk: () => {/**/} } ]}
legacyConfigPlugin
此步骤插件在vite配置为解析前后(config
、configResolved
)进行了处理:
- 在
config
钩子中对build
配置进行设值cssTarget
、target
- 并对是否会覆盖原有构建目标进行判断
- 使用
define
定义环境变量import.meta.env.LEGACY=__VITE_IS_LEGACY__
并返回 - 最终在
configResolved
中判断有无用户自定义的构建目标,有的话就提示将会被覆盖,应该在legacy插件中定义构建目标
ts
const legacyConfigPlugin = { name: "vite:legacy-config", config(config2, env) { if (env.command === "build") { if (!config2.build) { config2.build = {}; } if (!config2.build.cssTarget) { config2.build.cssTarget = "chrome61"; } if (genLegacy) { overriddenBuildTarget = config2.build.target !== void 0; config2.build.target = [ "es2020", "edge79", "firefox67", "chrome64", "safari12" ]; } } return { define: { "import.meta.env.LEGACY": env.command === "serve" || config2.build?.ssr ? false : legacyEnvVarMarker } }; }, configResolved(config2) { if (overriddenBuildTarget) { config2.logger.warn( picocolorsExports.yellow( `plugin-legacy overrode 'build.target'. You should pass 'targets' as an option to this plugin with the list of legacy browsers to support instead.` ) ); } }}
legacyGenerateBundlePlug
此步骤插件在vite构建产物的generateBundle
钩子中进行处理:
- 如果不是
legacy
类型的bundle则走modern
polyfill构建处理 - 将在
renderChunk
中处理后的polyfill集合信息单独构建polyfillChunk
并追加到原有的bundle信息中,在bundle.write时真正输出
polyfillsPlugin
ts
function polyfillsPlugin(imports, excludeSystemJS) { return { name: "vite:legacy-polyfills", resolveId(id) { if (id === polyfillId) { return id; } }, // polyfillsPlugin将legacyPolyfill在load钩子中遍历拼接import "systemjs/dist/s.min.js注入到入口文件中 load(id) { if (id === polyfillId) { return [...imports].map((i) => `import ${JSON.stringify(i)};`).join("") + (excludeSystemJS ? "" : `import "systemjs/dist/s.min.js";`); } } };}
buildPolyfillChunk
ts
async function buildPolyfillChunk(mode, imports, bundle, facadeToChunkMap, buildOptions, format, rollupOutputOptions, excludeSystemJS) { let { minify, assetsDir } = buildOptions; minify = minify ? "terser" : false; const res = await vite.build({ mode, // so that everything is resolved from here root: path.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (document.currentScript && document.currentScript.src || new URL('index.cjs', document.baseURI).href)))), configFile: false, logLevel: "error", plugins: [polyfillsPlugin(imports, excludeSystemJS)], build: { write: false, minify, assetsDir, rollupOptions: { input: { polyfills: polyfillId }, output: { format, entryFileNames: rollupOutputOptions.entryFileNames } } }, // Don't run esbuild for transpilation or minification // because we don't want to transpile code. esbuild: false, optimizeDeps: { esbuildOptions: { // If a value above 'es5' is set, esbuild injects helper functions which uses es2015 features. // This limits the input code not to include es2015+ codes. // But core-js is the only dependency which includes commonjs code // and core-js doesn't include es2015+ codes. target: "es5" } } }); const _polyfillChunk = Array.isArray(res) ? res[0] : res; if (!("output" in _polyfillChunk)) return; // polyfillChunk信息:以legacyPolyfills作为入口文件内容构建的产物 const polyfillChunk = _polyfillChunk.output[0]; for (const key in bundle) { const chunk = bundle[key]; // 在整体bundle中查找chunk类型,并将以chunk的实际磁盘位置作为key,polyfillChunk的fileName作为value保存 // 后后置transformIndexHtml钩子中会用到 if (chunk.type === "chunk" && chunk.facadeModuleId) { facadeToChunkMap.set(chunk.facadeModuleId, polyfillChunk.fileName); } } // 将上面构建出的polyfillChunk追加到原有的bundle中,在后续的实际产出中将会打包出来 bundle[polyfillChunk.fileName] = polyfillChunk;}
legacyGenerateBundlePlugin
ts
const legacyGenerateBundlePlugin = { name: "vite:legacy-generate-polyfill-chunk", apply: "build", async generateBundle(opts, bundle) { if (config.build.ssr) return; // isLegacyBundle会对format为system的chunk进行是否包含-legacy字段的判断 // 如果是false则会进行以format为es的形式对chunk进行modern polyfill构建 if (!isLegacyBundle(bundle, opts)) { if (!modernPolyfills.size) return; isDebug && console.log( `[@vitejs/plugin-legacy] modern polyfills:`, modernPolyfills ); await buildPolyfillChunk( config.mode, modernPolyfills, bundle, facadeToModernPolyfillMap, config.build, "es", opts, true ); return; } // 如果genLegacy为true则return,不将生成 if (!genLegacy) return; // 此时经过renderChunk的babel处理后,legacyPolyfills中已经记录了需要的polyfill信息(ImportDeclaration、ExpressionStatement等) if (legacyPolyfills.size) { // 根据输入语法检测在legacyPolyfills中增加对应ImportDeclaration类型的polyfill模块名称 await detectPolyfills( `Promise.resolve(); Promise.all();`, targets, legacyPolyfills ); isDebug && console.log( `[@vitejs/plugin-legacy] legacy polyfills:`, legacyPolyfills ); // 进行vite函数式构建 await buildPolyfillChunk( config.mode, legacyPolyfills, bundle, facadeToLegacyPolyfillMap, // force using terser for legacy polyfill minification, since esbuild // isn't legacy-safe config.build, "iife", opts, options.externalSystemJS ); } }}
legacyPostPlugin
此步骤插件仅在构建时对相同configResolved
、renderChunk
、transformIndexHtml
和generateBundle
钩子做后置处理:
ts
const legacyPostPlugin = { name: "vite:legacy-post-process", enforce: "post", apply: "build", configResolved, renderChunk, transformIndexHtml, generateBundle}