田间的风吹老了岁月,老舍笔下的茶馆写的是近代史,真的写尽了当时的苦态,可能现在的地铁写的是现代史吧。时光飞逝,很快就工作两三年了。昨天做项目的时候,引入svg图像转换为组件的时候,觉得很麻烦,自己手动转换,效率低,而且无脑,关键还不能直接用,有时候UI切的svg的图是没办法直接转为组件的,还要手动修改,这让我这个底层打工仔很生气。一怒之下,写了个插件,进行转换。然后就篇文章分享一下给大家,同时大家也可以直接使用。
介绍
在现代Web开发中,SVG因其无损缩放和丰富的交互性而受到广泛欢迎。然而,手动将SVG文件转换为React或Vue组件不仅耗时,还容易出错。为了解决这个问题,我开发了wsksvg。这个工具不仅可以优化SVG文件,还能将其转换为格式化的无状态React组件或Vue组件。它支持单文件处理,也支持批量处理,通过一个安装命令和一条指令大大提高开发效率,为底层开发者节省时间。
功能和特点
以下是其主要功能和特点:
1.SVG 优化:
使用 svgo 进行 SVG 文件的优化,包括调整颜色、移除不必要的属性等。
根据 --vue 或 --react 选项,生成 Vue 或 React 组件。
2.组件生成:
Vue 组件:将 SVG 转换为 Vue 单文件组件(.vue)。
React 组件:将 SVG 转换为 React 组件(.tsx)。
3.图像优化:
支持 PNG、JPG 和 JPEG 格式图像的优化。
使用 sharp 进行图像尺寸调整和优化。
4.输入输出处理:
支持处理单个文件或目录下的所有 SVG、PNG、JPG、JPEG 文件。
输出路径可以指定,也可以不指定,默认在输入路径相同目录下生成。
特点
- 灵活配置:根据选项生成 Vue 或 React 组件,或仅优化 SVG 文件。
- 自动路径处理:支持处理文件和目录,并自动创建输出目录。
- 增强 SVG 功能:保留 SVG 原始颜色和尺寸,移除不必要的属性。
- 详细日志:打印原始和优化后的文件大小,帮助用户了解优化效果。
- 错误处理:对不支持的文件类型和处理错误有明确的错误提示。
这个工具为开发人员提供了便捷的方式来优化图像并生成组件,适用于需要处理大量 SVG 文件和图像资源的项目。
使用说明
1.安装
npm install -g wsksvg
2.使用列子
wsksvg audio-file-raw.svg
wsksvg audio-file-raw.jpg
wsksvg audio-file-raw.png
wsksvg ./rawSvg
wsksvg ./rawSvg ./test //默认优化
wsksvg ./rawSvg ./testVue --vue //生成vue组件
wsksvg ./rawSvg ./testReact --react //生成react 组件
wsksvg ./raw //支持模糊匹配文件名称
./rawSvg 输入文件路径 ./test 输出文件路径
灵感来源
做公司项目的时候,不同项目之间总是使用相同的图标,或者只是颜色不同的图标,还有大小不一样的图标,因为不同项目间的,没办法共用,我就开始写个图标组件库,动态的改变图标的颜色,使用svg图片是比较合适的,符合我的项目需求,但是单纯的svg图标无法实现动态图标,那要转化为组件,我用的是react,那我要转化为React组件。刚开始不以为然,命名传参,返回svg图像,但UI切的SVG图,并不什么时候都能用够直接使用的,需要处理,因为React组件正常使用,需要手动修改。
那时候就开始思考,能不能通过工具进行处理,然后进行百度,还别说真的可以,有现成的插件的,可以直接生成,但是插件它只能生成React的组件, 不能生成Vue的组件,那就不能在不同框架下进行使用,有时候我也只想对图片进行优化,有些图标不需要改变颜色,尺寸,也不需要点击交互,单纯的优化。那它就不满足我的需求了。就开始尝试自己写一个。
实现过程
SVG 转React组件
寻找解决方案,发现svgr插件能够进行处理,那就自己写个文件进行处理,首先明白自己的需求,我想把一个文件夹下面的所有的SVG图片,都编译成react组件,那就是读取一个文件夹下的所有svg,那就要用到node fs 实现文件的读取,
然后进行格式转化通过–这个插件来处理,然后查看这个插件的官网,进行编译,然后写到另外一个文件下,汇总到index.ts文件中,共我导出使用。
import { transform } from '@svgr/core';
import { resolve, dirname, extname, basename } from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
import camelCase from 'camelcase';
import { optimize } from 'svgo';
import svgoConfig from '../svgo.config.js';
import svgoRawConfig from '../svgo.raw.config.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const entryDir = resolve(__dirname, '../rawSvg');
const outDir = resolve(__dirname, '../src/ReactIcons');
// Ensure output directory exists
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir, { recursive: true });
}
// Read all SVG files from the input directory
const files = fs.readdirSync(entryDir, 'utf-8');
const indexFileName = 'index.ts';
const prefix = '';
const suffix = '';
// Process SVG files
const batches = files.filter(f => extname(f) === '.svg').map(async file => {
try {
const svgFileName = basename(file, '.svg');
const componentName = `${prefix}${camelCase(svgFileName, { pascalCase: true })}${suffix}`;
const reactFileName = `${componentName}.tsx`;
const svgContent = fs.readFileSync(resolve(entryDir, file), 'utf-8');
// Choose appropriate SVGO configuration
const config = file.includes('-raw.svg') ? svgoRawConfig : svgoConfig;
const result = optimize(svgContent, config);
// Transform SVG to React component
const jsxCode = await transform(result.data, {
plugins: ['@svgr/plugin-jsx', '@svgr/plugin-prettier'],
icon: true,
typescript: true,
}, {
componentName: componentName,
});
// Write transformed SVG to a file
fs.writeFileSync(resolve(outDir, reactFileName), jsxCode, 'utf-8');
return { fileName: reactFileName, componentName };
} catch (error) {
console.error(`Error processing file ${file}:`, error);
throw error;
}
});
// Generate index file with exports
const arr = await Promise.all(batches);
const indexFileContent = arr.map(a => `export { default as ${a.componentName} } from './${a.componentName}';`).join('\n');
fs.writeFileSync(resolve(outDir, indexFileName), indexFileContent, 'utf-8');
console.log('SVG to React components conversion completed successfully.');
React都转了也不差Vue的
思考React的都能实现,React用的是jsx语法,Vue3支持,但Vue2不支持,Vue的语法,不就是模板语法嘛,编写模板语法,然后在把SVG写到模板语法中。
import { optimize } from 'svgo';
import fs from 'fs';
import path from 'path';
import camelCase from 'camelcase';
import { fileURLToPath } from 'url';
import svgoConfig from '../svgo.config.js';
import svgoRawConfig from '../svgo.raw.config.js';
// 获取 __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const entryDir = path.resolve(__dirname, '../rawSvg');
const outDir = path.resolve(__dirname, '../src/VueIcons');
// 确保输出目录存在
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir, { recursive: true });
}
// 读取所有 SVG 文件
const files = fs.readdirSync(entryDir, 'utf-8');
// 处理 SVG 文件
const batches = files.filter(f => path.extname(f) === '.svg').map(async file => {
try {
const svgFileName = path.basename(file, '.svg');
const componentName = camelCase(svgFileName, { pascalCase: true });
const vueFileName = `${componentName}.vue`;
const svgContent = fs.readFileSync(path.resolve(entryDir, file), 'utf-8');
// 选择适当的 SVGO 配置
const config = file.includes('-raw.svg') ? svgoRawConfig : svgoConfig;
const result = optimize(svgContent, config);
// 创建 Vue 组件模板
const vueCode = `
<template>
<svg xmlns="http://www.w3.org/2000/svg" v-html="icon" ></svg>
</template>
<script setup>
const icon = \`${result.data}\`;
</script>
<style scoped>
svg {
width: 1em;
height: 1em;
}
</style>
`;
// 将 Vue 组件写入文件
fs.writeFileSync(path.resolve(outDir, vueFileName), vueCode, 'utf-8');
return { fileName: vueFileName, componentName };
} catch (error) {
console.error(`Error processing file ${file}:`, error);
throw error;
}
});
// 生成 index.ts 文件
const arr = await Promise.all(batches);
const indexFileContent = arr.map(a => `export { default as ${a.componentName} } from './${a.componentName}.vue';`).join('\n');
fs.writeFileSync(path.resolve(outDir, 'index.ts'), indexFileContent, 'utf-8');
console.log('SVG to Vue components conversion completed successfully.');
只想优化SVG图像
有时候我又在想,不是很想转化,这个插件能够进行优化,在转化,那我想只是优化一下,输出的还是svg图片,查看文档,发现配置,进行配置优化,输出。当然一些svg是不需要改变颜色的,写死的,那这个优化配置就不能一样了。就进行判断,通过文件命名吧。
import { optimize } from 'svgo';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
// 获取 __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const entryDir = path.resolve(__dirname, '../rawSvg'); // 输入目录
const outDir = path.resolve(__dirname, '../src/optimizedSvg'); // 输出目录
// 确保输出目录存在
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir, { recursive: true });
}
// 读取所有 SVG 文件
const files = fs.readdirSync(entryDir, 'utf-8');
// 处理 SVG 文件
const processFiles = files.filter(f => path.extname(f) === '.svg').map(async file => {
try {
const filePath = path.resolve(entryDir, file);
const svgContent = fs.readFileSync(filePath, 'utf-8');
// 优化 SVG 内容
const result = optimize(svgContent, {
multipass: true, // 多次优化
// 其他配置选项可以在这里添加
});
// 写入优化后的 SVG 文件
const outputPath = path.resolve(outDir, file);
fs.writeFileSync(outputPath, result.data, 'utf-8');
console.log(`Optimized ${file}`);
} catch (error) {
console.error(`Error processing file ${file}:`, error);
}
});
// 等待所有文件处理完成
await Promise.all(processFiles);
console.log('SVG optimization completed successfully.');
进行自动化实现(写个插件或者服务)
写了三个脚本,文件,但是只能在这个项目中使用,还是写个服务吧,用户上传svg文件,在页面中进行选择,是优化,还是生成react组件代码还是Vue的一点复制,或者下载文件,那就完美了,部署在github的静态管理中,我就能够随时用了(我觉得我这个想法挺好),但是写一个上传需要一个服务,写一个服务简单,但是我的服务器快到期了,没钱续费了(年年打工,年年穷,越干前端,越是穷,只能解决温饱,不能致富啊),再者我想分享一下这个插件,觉得挺不错的,不敢给网友们用,主要担心有些人拿它做测试把我服务器搞溃,本来就不富裕的人,变得更穷了。那就写个npm脚本吧,通过运行脚本执行,方便你我,想用的都可以用,方便你我。
规范输入指令化
之前是通过不同的指令执行不同的js脚本文件,文件路径都是写死的,生成的react,跟vue也是写死的,那样肯定不行,不灵活,而且不方便,那就让用户输入吧,输入要转化的文件,或者文件夹,路径跟名称,还有输出的路径跟文件夹名字,同时也要指定生成React组件,还是Vue组件,还是只进行优化。**然后在测试过程中,觉得,我只想输入个文件名字或者路径,跟给个默认的执行比较好,这样体验感才会上去,那就默认如果没有给一个新的文件夹,在同一个文件下就进行一个.copy的扩展,如果没有指定是生成组件,还是优化,那就默认进行优化。**这样的一个过程。体验感不错,然后就这样规定。
Options:
--vue Generate Vue components from SVG files.
--react Generate React components from SVG files.
-h, --help Display this help message.
Examples:
wsksvg ./rawSvg
wsksvg ./rawSvg ./test
wsksvg ./rawSvg ./testVue --vue
wsksvg ./rawSvg ./testReact --react
./rawSvg Input file path ./test Output file path
懒得拼全名字模糊输入
输入路径的名称的时候,看到哪里只有这一个文件名,懒得打全名称,能不能进行模糊处理,同时也可以指定了摸个命名一样的进行处理,觉得这个想法不错,就进行了处理一下。效果不错。
wsksvg raw 也能够匹配到 wsksvg rawSvg.svg
svg都能优化,png,跟jpg也做一下优化吧
看到我的项目上不仅有svg图片,也有png跟jpg,能不能也把它们一起优化算了。然后引入了sharp 插件,emm,感觉效果可以,这下一个文件下的图片都进行了优化。哈哈哈哈哈哈哈,体验感也上去了,静态文件小了,加载图片的速度也加快了。
sharp 是一个用于图像处理的高性能 Node.js 库。它提供了广泛的功能来处理和优化图像,特别适合在服务器端进行图像操作。
// 处理 PNG 或 JPG 文件
async function processImageFile(filePath: string, output: string) {
const fileName = path.basename(filePath, path.extname(filePath));
const fileDir = path.dirname(filePath);
const originalBuffer = fs.readFileSync(filePath);
// 计算原始文件大小
const originalSize = Buffer.byteLength(originalBuffer);
// 生成输出目录路径
const outputDir = output ? path.resolve(process.cwd(), output) : fileDir;
ensureDirectoryExists(outputDir); // 确保输出目录存在
// 生成输出文件路径
const extname = path.extname(filePath);
let outputPath = path.resolve(outputDir, `${fileName}${extname}`);
// 检查输出路径是否已存在
if (fs.existsSync(outputPath)) {
outputPath = path.resolve(outputDir, `${fileName}.copy${extname}`);
}
// 计算优化后的文件缓冲区
const optimizedBuffer = await sharp(originalBuffer)
.resize({ withoutEnlargement: true }) // 根据需要调整大小
.toBuffer();
// 写入优化后的文件
fs.writeFileSync(outputPath, optimizedBuffer);
// 打印优化信息
const newSize = Buffer.byteLength(optimizedBuffer);
console.log(`Optimized ${path.basename(filePath)} -> ${path.basename(outputPath)}`);
console.log(`Original Size: ${originalSize} bytes`);
console.log(`Optimized Size: ${newSize} bytes`);
}
输出优化提示
用户输入路径,不知道会是那个,那就做个优化,提示一下它吧输出一下目前找的路径,然后发现我插件进行了优化了图片,但是优化了多少却不知道,还要最后去看文件的属性里面的文件大小,那就输出优化前后的文件大小,让用户知道这个插件做的优化,优化的图片数量。
SVG跟组件采用不同过的优化策略
为什么会这样呢,因为如果采用同一种优化策略,SVG图像无法显示
SVG优化
// SVGO 配置
const svgoConfig = {
js2svg: {
indent: 2,
pretty: true,
},
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false, // 保持 viewBox
inlineStyles: {
onlyMatchedOnce: false,
},
},
},
},
{
name: 'convertStyleToAttrs',
params: {
onlyMatchedOnce: false,
},
},
{
name: 'removeAttrs',
params: {
attrs: ['svg:style'], // 可选:移除内联样式,但保留 width 和 height
},
},
{
name: 'addAttributesToSVGElement',
params: {
attributes: [{
width: '1em',
height: '1em',
'aria-hidden': true,
focusable: 'false',
}]
}
}
],
};
组件优化SVG
const svgoComConfig = {
js2svg: {
indent: 2,
pretty: true,
},
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false,
inlineStyles: {
onlyMatchedOnce: false,
},
},
},
},
'removeXMLNS',
'convertStyleToAttrs',
{
name: 'convertColors',
params: {
attrs: ['svg:style'], // 可选:移除内联样式,但保留 width 和 height
},
},
{
name: 'removeAttrs',
params: { attrs: ['opacity'] }, // 移除不需要的 opacity 属性,但保留 width 和 height
},
{
name: 'addAttributesToSVGElement',
params: {
attributes: [{
'aria-hidden': true,
focusable: 'false',
// 不设置 width 和 height,以保持原始大小
}]
}
}
],
};
也许不同的需求不同,后面会进行一个扩展,扩展成可以自定义优化文件。 这是svgo 优化配置文件。
总是报错,那就弄个帮助说明
不小心输入出错了,提示报错,感觉如果没有个帮助说明不太好,别人不知道怎么用,就在做一下优化提示,给出一下例子。让用户知道怎么用。
进行测试
项目中进行使用
执行前
执行后
如果输出的文件中不存在该文件名称,则不加copy,如果存在则在中间加后缀.copy
效果
这是批量处理的效果,以及输出的提示,还有优化的大小
优化前后的大小对比(特地找个大量的图片来过测试)
优化前的总大小
优化后的总大小
优化了将近一半
优化前后的图像对比
是没啥差别点的。
生成React组件
文件大小
生成Vue组件
文件大小
可以看到都比之前少了60%总体上!!!!
最后
其实开发一个插件并不能,很多事情都可以进行自动化的进行处理,这些没什么技术含量的工作,刚开始我也是手动的将SVG图片转为React组件,反正也就那样,无所事事,没觉得有什么,但有时候思考一下,能不能通过工具实现这种无聊的工作呢,而且自己去尝试,去尝试的过程中,会引发自己的大量思考,从而它也是一种提升。 有人肯定会说,能实现就行了,没必要去思考那么多,有那时间还不如去摸鱼。我也想说,哈哈哈,不过真的停下来思考一下,实现过程并不难,这个插件我一天不到就实现了,而且能解决项目这种转化的问题,也剩下更多的摸鱼时间,也更多的时间去做自己喜欢做的事情,反正总工时不能变,打工人倔强。
如果你也觉得这个插件不错的化,帮助到你了,可以给个点赞,给我的github点个Star。
github地址:https://github.com/wskang12138/wsk-icons
总结
将SVG图像转换为组件可以显著提高开发效率,但手动处理这些转换往往耗时且容易出错。为了简化这一过程,wsksvg工具,它不仅能够优化SVG、PNG、JPG图片,还可以自动将SVG文件转换为格式化的无状态React或Vue组件。通过这个工具,用户可以轻松实现单文件或批量处理,减少了手动转换的繁琐步骤,提高了工作效率,同时也支持根据需求选择不同的处理方式。