You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义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;
        }
      }
    },
  };
}

问题原因及修复方案

核心原因

  1. Vite资源预优化优先级:构建阶段Vite会对SVG这类资源进行预处理,默认转为Data URL,该流程早于普通插件的transform阶段,导致插件处理被绕过。
  2. 插件执行顺序: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 02:15:55