一、血泪开篇:一个插件并行 Bug 毁了我的上线日
上周三晚上 8 点,距离项目上线只剩 1 小时 ——
我信心满满地执行vite build,控制台却突然爆红:Error: 雪碧图文件不存在。
明明本地开发时好好的,为啥打包就翻车?
- 我用了「SVG 雪碧图插件 A」处理图标,再用「Icon 组件生成插件 B」自动生成组件
- 为了让插件 A 先跑,还特意给 A 加了enforce: 'pre'
- 结果插件 B 的buildStart比插件 A 的异步处理先执行,拿着 “还没生成的雪碧图路径” 去造组件,直接报错!
相信不少做 Vite 插件开发的同学都踩过这坑:Vite 的 enforce 只能控 “插件顺序”,控不了 “方法级时序” —— 就像两个人同时跑接力赛,第二棒还没等第一棒把接力棒递过来,就先冲出去了,不摔才怪!
二、破局脑洞:把 Rollup 的「API 属性」变成 “接力棒”
既然 Vite 插件是 Rollup 的扩展,那就能利用 Rollup 的 “隐藏技能”:
插件能通过 api 属性对外暴露方法,就像运动员手里的接力棒 —— 前一棒跑完,把棒递出去,后一棒拿到再跑。
核心逻辑用流程图一看就懂:
简单说:用 Promise 当 “接力棒”,前序插件跑完异步操作再 “交棒”,后续插件拿到 “棒” 才启动 —— 彻底根治 “抢跑” 问题!
三、手把手实现:3 步玩明白「插件接力」
Step 1:前序插件(svg-sprite-plugin):打造 “接力棒”
核心是完成 “异步处理 SVG” 和 “暴露 Promise”,确保后续插件能等待:
// svg-sprite-plugin.js
import fs from 'fs/promises';
import path from 'path';
export default function svgSpritePlugin(options = {}) {
const { iconDir = './src/icons', outputPath = './src/assets/sprite.svg' } = options;
let spritePromise;
return {
name: 'vite-plugin-svg-sprite', // 唯一标识,供后续插件定位
api: {
// 暴露方法,返回异步处理的Promise
waitForSpriteReady() {
return spritePromise || Promise.resolve();
}
},
async buildStart() {
console.log(`开始处理SVG图标(目录:${iconDir})`);
// 用Promise包裹异步处理逻辑
spritePromise = new Promise(async (resolve, reject) => {
try {
// 读取并过滤SVG文件
const svgFiles = await fs.readdir(iconDir);
const validSvgs = svgFiles.filter(file => path.extname(file) === '.svg');
if (validSvgs.length === 0) {
console.warn(`${iconDir}目录下没找到SVG文件`);
resolve();
return;
}
// 生成雪碧图内容
let spriteContent = `<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">`;
for (const file of validSvgs) {
const fileName = path.basename(file, '.svg');
const svgPath = path.join(iconDir, file);
const svgContent = await fs.readFile(svgPath, 'utf-8');
const svgWithId = svgContent.replace('<svg', `<symbol id="icon-${fileName}"`).replace('</svg>', '</symbol>');
spriteContent += svgWithId;
}
spriteContent += `</svg>`;
// 写入雪碧图文件
await fs.writeFile(outputPath, spriteContent);
console.log(`雪碧图生成完成!路径:${outputPath}`);
resolve();
} catch (err) {
console.error(`SVG处理失败:${err.message}`);
reject(err);
}
});
}
};
}
Step 2:后续插件(icon-component-plugin):接住 “接力棒”
先定位前序插件、等待异步完成,再生成 Icon 组件:
// icon-component-plugin.js
import fs from 'fs/promises';
import path from 'path';
export default function iconComponentPlugin(options = {}) {
const { outputPath = './src/components/Icon.vue', spritePath = './src/assets/sprite.svg' } = options;
return {
name: 'vite-plugin-icon-component',
async buildStart({ plugins }) {
console.log(`正在寻找SVG雪碧图插件...`);
// 1. 定位前序插件(通过name匹配)
const svgPlugin = plugins.find(
plugin => plugin?.name === 'vite-plugin-svg-sprite'
);
if (!svgPlugin) {
console.error('未找到vite-plugin-svg-sprite,请先在vite.config中注册它');
return;
}
// 2. 等待前序插件异步处理完成
console.log(`等待雪碧图生成...`);
try {
await svgPlugin.api.waitForSpriteReady();
} catch (err) {
console.error(`等待雪碧图失败:${err.message}`);
return;
}
// 3. 生成Icon组件
console.log(`开始生成Icon组件...`);
const componentCode = `
<template>
<svg
class="icon"
:class="{'icon--spin': spin}"
:style="{ width: size + 'px', height: size + 'px', color: color }"
>
<use :xlink:href="`${spritePath}#icon-${name}`" />
</svg>
</template>
<script setup>
import { defineProps, withDefaults } from 'vue';
const props = withDefaults(defineProps({
name: { type: String, required: true },
size: { type: [Number, String], default: 24 },
color: { type: String, default: 'currentColor' },
spin: { type: Boolean, default: false }
}), {});
const spritePath = import.meta.env.VITE_SPRITE_PATH || '${spritePath}';
</script>
<style scoped>
.icon { display: inline-block; vertical-align: middle; fill: currentColor; }
.icon--spin { animation: spin 1s linear infinite; }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
</style>
`.trim();
// 确保目录存在并写入文件
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, componentCode);
console.log(`Icon组件生成完成!路径:${outputPath}`);
}
};
}
Step 3:Vite 配置 + 实测:看 “接力赛” 怎么跑
前序插件需先注册,确保后续插件能定位到:
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import svgSpritePlugin from './svg-sprite-plugin';
import iconComponentPlugin from './icon-component-plugin';
export default defineConfig({
plugins: [
vue(),
// 1. 先注册前序插件(处理SVG)
svgSpritePlugin({
iconDir: './src/icons',
outputPath: './src/assets/sprite.svg'
}),
// 2. 后注册后续插件(生成Icon组件)
iconComponentPlugin({
outputPath: './src/components/Icon.vue'
})
],
// 可选:配置环境变量切换雪碧图路径
define: {
'import.meta.env.VITE_SPRITE_PATH': JSON.stringify('/src/assets/sprite.svg')
}
});
实测效果:在src/icons放 2 个 SVG 文件,执行vite build,控制台会按 “处理 SVG→等待→生成组件” 的顺序打印日志,再也不会报 “文件不存在”!
四、避坑要点:3 个高频问题解决方案
| 坑点 | 症状 | 解决方案 |
|---|---|---|
| 插件 name 重复 | 后续插件找不到前序插件 | 给插件起唯一 name,如加项目前缀 |
| 前序插件没 resolve | 后续插件无限等待 | 异步操作完成后调用resolve(),出错时调用reject() |
| 插件注册顺序反了 | 后续插件定位不到前序插件 | 前序插件放在plugins数组前面 |
五、进阶场景:不止于 buildStart
该方案可覆盖 Vite 全生命周期,举 3 个实用场景:
- transform 阶段:插件 A 转译 Vue 模板后,插件 B 等待并执行代码压缩;
- generateBundle 阶段:插件 A 生成配置文件后,插件 B 等待并注入到index.html;
- 多插件链式依赖:插件 A→B→C,B 暴露 Promise 供 C 等待,形成链式接力。
六、总结
Vite 插件并行 Bug 的核心解法,是用 Rollup 的api属性做 “接力通信”—— 前序插件用 Promise 包裹异步逻辑,后续插件await等待后再执行。这套方案无需额外依赖,仅靠 Rollup 原生能力,就能根治 “方法级时序” 问题,从 “上线前踩坑” 变成 “稳定交付”。
下次遇到插件依赖问题,直接用 “Promise 接力” 思路,就能高效解决!