引言
在项目开发中,由于尚未建立统一的UI规范,前端开发同学在拿到UI设计稿后,通常会将图标以SVG格式保存到静态资源文件夹中,并通过<img>
标签加载。然而,这种方法存在诸多问题:对于同一个图标的不同状态(如默认、悬停、激活等),需要保存多份SVG文件,导致资源冗余、网络请求过多,同时样式管理也变得繁琐且难以维护。
为了解决这些问题,SVG Sprite 提供了一种高效且性能友好的解决方案。通过将多个SVG图标合并为一个文件,并利用<symbol>
和<use>
元素实现按需加载,SVG Sprite不仅能减少HTTP请求,还能通过CSS灵活控制图标样式,极大地简化了开发流程。
在实际应用中,现代前端构建工具如 Webpack 和 Vite 等已经提供了丰富的 SVG Sprite 插件(如 svg-sprite-loader
和 vite-plugin-svg-sprite
),用于高效管理和优化 SVG 图标。这些插件通过将多个 SVG 图标合并为一个精灵文件,减少了网络请求,同时支持通过 CSS 灵活控制样式。这些插件的实现思路为我提供了很好的借鉴。因此,我将以 SVG Sprite 的需求为基础,结合现有插件的实现方式,记录自己开发 Vite 插件的学习过程,探索如何更好地集成 SVG 图标管理。
需求分析
为了优化项目中的SVG图标管理,提出以下需求:
-
SVG图标合并与导出
将项目中的SVG图标合并为一个SVG Sprite文件,减少HTTP请求,并支持导出功能。
-
开发模式下的热更新
支持在开发模式下对SVG文件的新增、修改和更新等操作,实现热更新,提升开发效率。
-
虚拟模块加载
支持通过虚拟模块加载SVG Sprite文件,简化开发流程。
-
SVG Sprite内容注入到
index.html
支持将 SVG Sprite 内容直接嵌入到
index.html
中,减少外部资源加载时间
实现细节
定义插件的参数
typescript
interface SVGSpriteGenOptions {
/**
* 包含的SVG文件路径或模式(glob)。用于指定需要处理的SVG文件。
*/
include: string[];
/**
* 排除的SVG文件路径或模式(glob)。用于指定不处理的SVG文件。
*/
exclude: string[];
/**
* 是否启用SVGO优化配置,或直接传入自定义的SVGO配置。
* - 如果为`true`,将使用默认的SVGO优化配置且可以加载svgo的根目录下配置文件。
* - 如果为`false`,将跳过SVGO优化。
* - 如果传入`Config`对象,则使用自定义的SVGO配置且可以加载svgo的根目录下配置文件。
*/
svgoConf: boolean | Config;
/**
* SVG Sprite中`<symbol>`元素的ID命名规则。
* - 可以是字符串模板,例如`"icon-[name]"`,其中`[name]`会被替换为SVG文件名。
*/
symbolId: string;
/**
* SVG Sprite文件的输出路径。
*/
output: string;
/**
* 模式选择:
* - `virtual`:以虚拟模块的形式加载SVG Sprite。
* - `inline`:将SVG Sprite内容直接注入到`index.html`中。
*/
mode: 'virtual' | 'inline';
}
生成 SVG Sprite 工具函数
ts
import { optimize, loadConfig, type Config } from 'svgo';
import fg from 'fast-glob';
import { resolve,basename } from 'path';
import { readFileSync } from 'fs';
const generateSVGSprite = async (options: SVGSpriteGenOptions & { root: string }) => {
// 默认的 SVGO 配置,用于优化 SVG 文件
let svgoConfig: Config = {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false // 不移除 viewBox 属性
}
}
}
]
};
// 加载 SVGO 配置文件(如果存在)
const svgoLoadConfig = await loadConfig();
if (typeof options.svgoConf === 'object') {
svgoConfig = Object.assign(svgoConfig, options.svgoConf); // 合并用户自定义的 SVGO 配置
}
// 使用 fast-glob 查找匹配的 SVG 文件
const SVGFiles = fg.sync(options.include, {
cwd: options.root, // 以项目根目录为基准
ignore: options.exclude, // 排除指定的文件或目录
onlyFiles: true // 只匹配文件
});
// 生成 SVG 精灵内容
const spriteContent = SVGFiles.map((file) => {
const filePath = resolve(options.root, file); // 获取文件的绝对路径
const SVGContent = readFileSync(filePath, 'utf-8'); // 读取 SVG 文件内容
const optimizedSVG = options.svgoConf
? optimize(SVGContent, Object.assign(svgoConfig, svgoLoadConfig)).data // 如果启用了 SVGO,优化 SVG 内容
: SVGContent; // 否则直接使用原始内容
const id = options.symbolId.replace('[name]', basename(file, '.svg')); // 替换 symbolId 中的 [name] 为文件名
return `<symbol id="${id}" viewBox="0 0 24 24">${optimizedSVG}</symbol>`; // 生成 symbol 元素
}).join('\n'); // 将所有 symbol 元素连接成字符串
// 返回完整的 SVG 精灵文件内容
return `<svg xmlns="http://www.w3.org/2000/svg" style="display:none;">${spriteContent}</svg>`;
};
Vite 插件开发
ts
import {createFilter, normalizePath, type ResolvedConfig, type Plugin, ViteDevServer} from 'vite';
import {optimize, loadConfig, type Config} from 'svgo';
import fg from 'fast-glob';
import {readFileSync, writeFileSync} from 'fs';
// 定义插件名称和虚拟模块 ID
const VITE_PLUGIN_NAME = 'vite-plugin-svg-sprite-gen';
const VIRTUAL_MODULE_ID = 'virtual:svg-sprite-gen';
const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID;
// 插件的主函数
export default function SVGSpriteGen(options?: Partial<SVGSpriteGenOptions>): Plugin {
// 默认配置
const defaultOptions: SVGSpriteGenOptions = {
include: ['**/*.svg'], // 默认包含所有 SVG 文件
exclude: ['**/node_modules/**', '**/dist/**'], // 默认排除 node_modules 和 dist 目录
svgoConf: true, // 启用 SVGO 优化
symbolId: '[name]', // 默认 symbolId 使用文件名
output: 'sprite.svg', // 默认输出文件名
mode: 'virtual' // 默认模式为虚拟模块
};
// 合并用户传入的配置和默认配置
const SVGSpriteGenOpts = Object.assign(defaultOptions, options);
const {include, exclude, output, mode} = SVGSpriteGenOpts;
// 创建文件过滤器,用于匹配和排除文件
const filter = createFilter(include, exclude);
let config: ResolvedConfig; // Vite 的配置对象
let SVGSpriteElement = ''; // 存储生成的 SVG 精灵内容
// 文件变化时的处理函数
const handleFileChange = async (file: string, server: ViteDevServer) => {
if (filter(normalizePath(file))) { // 如果文件匹配过滤器
SVGSpriteElement = await generateSVGSprite({...SVGSpriteGenOpts, root: config.root}); // 重新生成 SVG 精灵
// 重新触发虚拟模块的加载
const module = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_MODULE_ID);
if (module) {
server.moduleGraph.invalidateModule(module);
}
server.ws.send({ // 触发页面重新加载
type: 'full-reload',
path: '*'
});
}
};
return {
name: VITE_PLUGIN_NAME, // 插件名称
async configResolved(_config) {
config = _config; // 保存 Vite 的配置对象
},
configureServer(server) {
// 监听文件变化
server.watcher.add(include);
server.watcher.on("add", (file) => handleFileChange(file, server)); // 文件新增
server.watcher.on("change", (file) => handleFileChange(file, server)); // 文件修改
server.watcher.on("unlink", (file) => handleFileChange(file, server)); // 文件删除
},
async buildStart() {
// 构建开始时生成 SVG 精灵
SVGSpriteElement = await generateSVGSprite({...SVGSpriteGenOpts, root: config.root});
if (config.command === 'build') {
// 在构建模式下,将 SVG 精灵作为资产输出
this.emitFile({
type: 'asset',
source: SVGSpriteElement,
fileName: output
});
} else {
// 在开发模式下,将 SVG 精灵写入到输出目录
writeFileSync(resolve(config.build.outDir, output), SVGSpriteElement);
}
},
transformIndexHtml(html) {
// 在 HTML 文件中注入 SVG 精灵
if (mode === 'inline') {
return html.replace('</body>', `${SVGSpriteElement}</body>`);
}
return html;
},
resolveId(id) {
// 解析虚拟模块 ID
if (id === VIRTUAL_MODULE_ID) {
return RESOLVED_VIRTUAL_MODULE_ID;
}
},
load(id) {
// 加载虚拟模块的内容
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
return `const SVGSprite = \`${SVGSpriteElement}\`;
const parser = new DOMParser();
const SVGDoc = parser.parseFromString(SVGSprite, 'image/svg+xml');
const SVGNode = SVGDoc.documentElement;
document.body.appendChild(SVGNode);
export default SVGSprite;`;
}
},
transform(code, id) {
// 如果文件匹配过滤器,则不处理代码
if (filter(id)) return null;
return code;
}
};
}
FAQ
-
在使用虚拟模块时,因为 TypeScript 编译器无法识别这个虚拟模块,从而导致类型检查失败。
在vite-env.d.ts
文件中添加以下内容:ts/// <reference types="vite/client" /> declare module 'virtual:svg-sprite-gen' { const SVGSprite: string; export default SVGSprite; }
后记
SVG:可缩放矢量图形
SVG:<g>
标签
SVG:<symbol>
标签
SVG:<use>
标签
未来必热:SVG Sprites技术介绍-张鑫旭
感谢阅读,敬请斧正!