自定义Vite SVG转换插件仅在开发模式生效,构建模式失效
Vite插件在SvelteKit构建时失效:SVG未按预期转换为带元数据的模块
我基于Vite打包的SvelteKit开发应用,编写了一个Vite插件用于导入带?resource查询参数的SVG文件。插件会解析SVG并转换为包含宽高元数据和Data URL的模块:
export default { width: 100, height: 100, dataUrl: "data:image/svg+xml;charset=utf-8,[...]" }
核心需求是将SVG内联到代码中,同时嵌入宽高信息(无法通过浏览器渲染的SVG元素获取,有明确业务理由)。
插件在开发模式(执行vite命令)下完全正常,但执行vite build构建后,生成的文件中SVG被直接转为原始Data URL,插件的转换结果完全被忽略。示例打包代码片段:
var yc = "data:image/svg+xml,%3csvg%20width='100'%20height='100'[...]"
已尝试的无效方法
- 将插件的
transform方法替换为load方法 - 用调试器跟踪Vite执行流程:插件能正确转换文件,结果未被其他插件覆盖,但转换后的代码未进入最终构建产物
配置文件
vite.config.ts
import { sveltekit } from "@sveltejs/kit/vite"; import { svgResourcePlugin } from "./plugins/svgResourcePlugin"; import { defineConfig } from "vitest/config"; const config = defineConfig({ plugins: [svgResourcePlugin(), sveltekit()], worker: { format: "es", }, test: { include: ["src/**/*.{test,spec}.{js,ts}"], }, }); export default config;
svelte.config.js
SvelteKit使用@sveltejs/adapter-static适配器,构建生成静态站点:
import adapter from "@sveltejs/adapter-static"; import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; /** @type {import('@sveltejs/kit').Config} */ const config = { preprocess: vitePreprocess(), kit: { adapter: adapter({ pages: "build", assets: "build", fallback: undefined, precompress: false, strict: true, }), }, }; export default config;
插件代码
import { transformWithEsbuild, type Plugin } from "vite"; import { readFileSync } from "fs"; import { XMLParser } from "fast-xml-parser"; export function svgResourcePlugin(options = {}): Plugin { const { defaultExport = "url", svgoConfig, expandProps, svgo, ref, memo, replaceAttrValues, svgProps, titleProp, } = options; const cache = new Map(); const svgRegex = /\.svg(?:\?(resource|url))?$/; return { name: "svg-resource", async transform(source, id, isBuild) { const result = id.match(svgRegex); if (result) { const type = result[1]; if ((defaultExport === "url" && typeof type === "undefined") || type === "url") { return source; } if ((defaultExport === "resource" && typeof type === "undefined") || type === "resource") { const idWithoutQuery = id.replace(".svg?resource", ".svg"); let result = cache.get(idWithoutQuery); if (!result) { const code = readFileSync(idWithoutQuery, "utf-8"); const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "", }); const jsonObj = parser.parse(code); if (jsonObj && jsonObj.svg) { const width = parseInt(jsonObj.svg.width, 10); const height = parseInt(jsonObj.svg.height, 10); const dataURL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent( Buffer.from(code).toString(), )}`; result = `export default ${JSON.stringify({ width, height, dataURL, })};`; } else { this.error(`Invalid SVG resource file: ${id}`); } if (isBuild) { cache.set(idWithoutQuery, result); } } return result; } } }, }; }
问题原因及修复方案
核心原因
- Vite资源预优化优先级:构建阶段Vite会对SVG这类资源进行预处理,默认转为Data URL,该流程早于普通插件的
transform阶段,导致插件处理被绕过。 - 插件执行顺序:
sveltekit()插件可能在你的插件之后运行,覆盖了转换结果;同时插件未明确标记需要拦截的SVG资源,导致Vite按默认逻辑处理。
具体修复步骤
1. 给插件添加前置执行标记
修改插件定义,添加enforce: 'pre',确保插件在Vite默认资源处理和sveltekit插件之前执行:
return { name: "svg-resource", enforce: 'pre', // 关键:提升插件执行优先级 async transform(source, id, isBuild) { // ... 原插件逻辑 } }
2. 拦截带?resource的SVG导入,阻止默认处理
添加resolveId方法标记目标SVG为自定义模块,避免Vite将其当作普通资源处理:
export function svgResourcePlugin(options = {}): Plugin { // ... 保留原有配置 const svgResourceRegex = /\.svg\?resource$/; return { name: "svg-resource", enforce: 'pre', resolveId(id) { // 给带?resource的SVG添加null前缀,标记为自定义模块 if (svgResourceRegex.test(id)) { return `\0${id}`; } }, async load(id) { // 处理标记过的模块 if (id.startsWith('\0')) { const realId = id.slice(1); const idWithoutQuery = realId.replace(".svg?resource", ".svg"); let result = cache.get(idWithoutQuery); if (!result) { // ... 保留原有SVG解析、生成模块代码的逻辑 const code = readFileSync(idWithoutQuery, "utf-8"); const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "", }); const jsonObj = parser.parse(code); if (jsonObj && jsonObj.svg) { const width = parseInt(jsonObj.svg.width, 10); const height = parseInt(jsonObj.svg.height, 10); // 优化:直接编码字符串,无需转Buffer const dataURL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(code)}`; result = `export default ${JSON.stringify({ width, height, dataURL, })};`; } else { this.error(`Invalid SVG resource file: ${realId}`); } cache.set(idWithoutQuery, result); } return result; } }, // 保留原有transform处理默认导出逻辑 async transform(source, id) { const result = id.match(/\.svg(?:\?(resource|url))?$/); if (result) { const type = result[1]; if ((defaultExport === "url" && typeof type === "undefined") || type === "url") { return source; } // 处理不带查询参数但默认导出为resource的情况 if ((defaultExport === "resource" && typeof type === "undefined")) { const code = readFileSync(id, "utf-8"); // ... 同上解析逻辑 const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "", }); const jsonObj = parser.parse(code); if (jsonObj && jsonObj.svg) { const width = parseInt(jsonObj.svg.width, 10); const height = parseInt(jsonObj.svg.height, 10); const dataURL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(code)}`; return `export default ${JSON.stringify({ width, height, dataURL })};`; } } } } }; }
3. 验证修复效果
执行vite build后,检查生成的代码是否包含预期的{width, height, dataURL}对象,而非单纯的Data URL。
额外优化建议
- 若SVG使用
viewBox而非直接设置width/height,可解析viewBox属性计算宽高:if (jsonObj.svg.viewBox) { const [x, y, w, h] = jsonObj.svg.viewBox.split(' ').map(Number); const width = jsonObj.svg.width ? parseInt(jsonObj.svg.width,10) : w; const height = jsonObj.svg.height ? parseInt(jsonObj.svg.height,10) : h; } - 开发模式下可禁用缓存,避免修改SVG后无法实时更新:
// 移除if (isBuild)判断,或者开发模式下不使用缓存 // cache.set(idWithoutQuery, result);
内容的提问来源于stack exchange,提问作者kettlepot
相关产品推荐
相关产品推荐

