前言
大家都知道,我们在做前端项目的时候,会有很多的定制化样式,其中图标就有很多。有些图标还有不同颜色的版本,但是ui设计用的又不是字体图标,如下载为png在不同的分辨率下效果又不好。使用svg倒是一个好方法,不会失真。但是不同颜色又不好处理。
用图片引入svg图标是不可以改变颜色的,只能不同颜色使用不同的svg图片,这样就不优雅了。在vue2项目当中,我们可以通过组件的方式统一使用svg图标,不管是自定义的还是图标库下载的,不管是单色的还是彩色的都可以。
但是在vue3中使用这个方法好像不行。
起因
目前,我们使用svg图标的方法在vue3中大概就以下几种方法:
- 将svg文件代码复制到vue文件中,做为一个组件使用。
- 使用插件-
vite-plugin-svg-icons
- 使用img用图片的方式使用
- 变为base64代码使用
- 直接在页面当中使用
在做项目的时候,我当时使用插件来使用,但是当时出了一点问题,我比较菜嘛很正常,就死活不能将svg图标显示出来。
然后我就想,之前在xxx项目当中,那个svg图标就直接用组件的方式使用的,我在这个项目当中也自己搞一个组件引用不就行了。说干就干,开始。
项目太多找不到了,ε=(´ο`*)))唉!
幸亏我有everything,大概想起来名字应该是有一个icon,然后加了svg,是一个vue组件。最终还是让我在备份盘中找到了这个组件。
但是这是vue2的项目,而且也是使用插件来支撑的。
复制到vue3当中,有很多报错,还获取不到svg的内容,还是无法显示。
然后就是这个想法就搁置啦,我又不是架构师,我只是个打工的。还有很多bug要修改呢,先用img应付一下。
转机来了
是什么呢?就是我在看unocss文档的时候,看到图标那一个地方,有一个tip:
推荐阅读纯css图标,哎,我就在想这到底是个什么东西,点进去看看嘿嘿。
进入只是发现还是英语的,幸好还有对应的中文文章。不然使用浏览器的翻译,那有时候真是牛头不对马嘴的。
方便大家阅读,我这个给个地址,可以去看看:antfu.me/posts/icons…
在这篇文章里面,我看到了大佬在做出一个成果的时候,也是经历多种曲折的过程的,而且,我还发现了让我解决不借助插件的情况下,将svg变为组件的方法。
// https://bl.ocks.org/jennyknuth/222825e315d45a738ed9d6e04c7a88d0
function encodeSvg(svg: string) {
return svg.replace('<svg', (~svg.indexOf('xmlns') ? '<svg' : '<svg xmlns="http://www.w3.org/2000/svg"'))
.replace(/"/g, ''')
.replace(/%/g, '%25')
.replace(/#/g, '%23')
.replace(/{/g, '%7B')
.replace(/}/g, '%7D')
.replace(/</g, '%3C')
.replace(/>/g, '%3E')
}
const dataUri = `data:image/svg+xml;utf8,${encodeSvg(svg)}`
就是将svg转码,然后使用背景来使用,对于彩色图标使用背景图片,会变成一坨,又添加了新的方案:
// 如果 SVG 的图标包含 `currentColor` 的值
// 它大概率是一个单色图标
const mode = svg.includes('currentColor')
? 'mask'
: 'background-img'
const uri = `url("data:image/svg+xml;utf8,${encodeSvg(svg)}")`
// 单色图标
if (mode === 'mask') {
return {
'mask': `${uri} no-repeat`,
'mask-size': '100% 100%',
'background-color': 'currentColor',
'height': '1em',
'width': '1em',
}
}
// 彩色图标
else {
return {
'background': `${uri} no-repeat`,
'background-size': '100% 100%',
'background-color': 'transparent',
'height': '1em',
'width': '1em',
}
}
就是判断svg当中是否包含currentColor
字符串,这是一个可以让svg随着上下文改变自身颜色的东西。
就是当你需要将svg图标的颜色随着字体改变的话,将里面的颜色值改为这个字段就可以了。
现在,我的构想已经完成一大半了,就是将svg的内容用文本的形式显示出来,不仅可以改变大小,还可以改变颜色,不仅纯色图标可以显示,彩色图标也可以显示。下面看看我的成果。
成果
大方向不变,我现在就只要拿到对应svg文件的文本内容就行了,这我第一想到的就是nodejs,使用fs模块读取svg文件。
但是他瞄的,在浏览器中使用nodejs还有许多问题,还挺麻烦,而且读取文件这个操作是不是有点那个撒。
我想不到其他地方,但是可以问ai啊,现在遍地都是ai,还真给我了一个方案,在浏览器中可以使用fatch来请求图片地址,然后返回text就行。你还别说,这个方法还真好哎。
demo.vue
<!--demo文件-->
<script setup lang="ts">
import menu1 from '@/assets/icons/menu1.svg'
import { svgToCss } from '@/icons/svg-utils'
const bg = ref({
"background": "url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z'/%3E%3C/svg%3E") no-repeat",
"background-size": "100% 100%",
"background-color": "transparent",
"height": "1em",
"width": "1em"
})
fetch(menu1)
.then(response => response.text())
.then(data => {
bg.value = svgToCss(data)
console.log(data)
console.log(svgToCss(data))
})
.catch(error => {
console.error('Error fetching SVG:', error);
});
</script>
<template>
<div class="text-center">
<div class="font-600 text-40px w-40px h-40px">
<i :style="bg" class="inline-block"/>
</div>
</div>
</template>
<style scoped>
</style>
menu1.svg
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1690356706537" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="27569" width="64" height="64" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M508.928 589.824c-149.504 0-270.336-121.856-270.336-270.336S359.424 48.128 508.928 48.128s270.336 121.856 270.336 270.336-120.832 271.36-270.336 271.36z m0-463.872c-106.496 0-192.512 86.016-192.512 192.512S403.456 512 508.928 512c106.496 0 192.512-86.016 192.512-192.512s-86.016-193.536-192.512-193.536zM772.096 989.184H246.784c-97.28 0-176.128-78.848-176.128-176.128s78.848-176.128 176.128-176.128h525.312c97.28 0 176.128 78.848 176.128 176.128s-78.848 176.128-176.128 176.128zM246.784 711.68c-56.32 0-102.4 46.08-102.4 102.4s46.08 102.4 102.4 102.4h525.312c56.32 0 102.4-46.08 102.4-102.4s-46.08-102.4-102.4-102.4H246.784z" fill="#4C4C4C" p-id="27570"></path><path d="M756.736 836.608h-93.184c-21.504 0-38.912-17.408-38.912-38.912s17.408-38.912 38.912-38.912h93.184c21.504 0 38.912 17.408 38.912 38.912s-17.408 38.912-38.912 38.912z" fill="#FFA028" p-id="27571"></path></svg>
svg-utils.ts
/**
* @param svg svg字符串
* @returns svg 转义后的svg字符串
* @description 处理svg文件,避免存在换行,或者符号问题
*/
function encodeSvg(svg: string) {
return svg.replace('<svg', (~svg.indexOf('xmlns') ? '<svg' : '<svg xmlns="http://www.w3.org/2000/svg"'))
.replace(/"/g, ''')
.replace(/%/g, '%25')
.replace(/#/g, '%23')
.replace(/{/g, '%7B')
.replace(/}/g, '%7D')
.replace(/</g, '%3C')
.replace(/>/g, '%3E')
}
/**
* @param svg svg字符串
* @returns css样式
* @description 将svg转换为css样式
*/
export function svgToCss(svg: string) {
console.log('svgToCss', encodeSvg(svg))
const mode = svg.includes('currentColor') ? 'mask' : 'background-img'
const uri = `url("data:image/svg+xml;utf8,${encodeSvg(svg)}")`
if (mode === 'mask') {
// 单色图标,颜色随文字颜色变化
return {
'mask': `${uri} no-repeat`,
'mask-size': '100% 100%',
'background-color': 'currentColor',
'height': '1em',
'width': '1em'
}
} else {
// 彩色图标。不改变颜色
return {
'background': `${uri} no-repeat`,
'background-size': '100% 100%',
'background-color': 'transparent',
'height': '1em',
'width': '1em'
}
}
}
你是不是以为就完成了,哪有这么容易,在我实验的过程中,我发现,有许多的svg根本就显示不了,经过不断排查,发现tm的有换行,\n,\r
都有,导致转换之后,代码在css中就不能起效果。
然后就修改encodeSvg
方法,加上.replace(/[\r\n]/g, '')
哎,可以显示了,然后就是将需要改变颜色的svg将svg的颜色改为currentColor
,发现还是不行,显示一坨。
经过再次排查,发现还有隐藏的换行符,然后修改方法,改为加上.replace(/[\r\n\t]/g, '')
你是不是以为到这里就完成了,其实还没有。
优化
我的目标是,不借助插件,做出一个组件,使用svg图标,可以修改颜色大小,就跟原生图标一样的效果。
现在就把这些代码提出做为一个组件:
fetch在每次使用的时候都会请求图标,如果相同的也会请求,我不知道这会不会影响ui的渲染。
然后就找找看,有没有其他的方法,ai给出的方案是通过import动态导入
const module = await import(`@/assets/icons/${props.name}.svg?raw`);
style.value = svgToCss(module.default)
await import(@/assets/icons/${props.name}.svg?raw);
这行代码使用了动态导入(dynamic import
)来异步加载 SVG 文件。
props.name
是组件的 prop
,表示要加载的 SVG 文件的名称。
raw
是一个特殊的查询参数,告诉 Vite 或 Webpack 以文本形式(而不是模块形式)加载文件内容。这样,module.default
将包含 SVG 文件的原始文本内容。
ok,组件完结啦,
最终成果
<template>
<i :style="style" class="icon"/>
</template>
<script lang="ts" setup>
import { CSSProperties } from 'vue'
const props = defineProps({
name: {
type: String,
required: true,
},
className: {
type: String,
default: '',
},
})
const style = ref<CSSProperties>({})
onMounted(async () => {
// 根据名称获取到svg文件内容,raw模式可以获取文本内容
const module = await import(`@/assets/icons/${props.name}.svg?raw`);
style.value = svgToCss(module.default)
});
/**
* @param svg svg字符串
* @returns svg 转义后的svg字符串
* @description 处理svg文件,避免存在换行,或者符号问题
*/
function encodeSvg(svg: string) {
return svg.replace('<svg', (~svg.indexOf('xmlns') ? '<svg' : '<svg xmlns="http://www.w3.org/2000/svg"'))
.replace(/"/g, ''')
.replace(/%/g, '%25')
.replace(/#/g, '%23')
.replace(/{/g, '%7B')
.replace(/}/g, '%7D')
.replace(/</g, '%3C')
.replace(/>/g, '%3E')
.replace(/[\r\n\t]/g, '')
}
/**
* @param svg svg字符串
* @returns css样式
* @description 将svg转换为css样式
*/
function svgToCss(svg: string) {
console.log('svgToCss', encodeSvg(svg))
const mode = svg.includes('currentColor') ? 'mask' : 'background-img'
const uri = `url("data:image/svg+xml;utf8,${encodeSvg(svg)}")`
if (mode === 'mask') {
// 单色图标,颜色随文字颜色变化
return {
'mask': `${uri} no-repeat`,
'mask-size': '100% 100%',
'background-color': 'currentColor',
'height': '1em',
'width': '1em'
}
} else {
// 彩色图标。不改变颜色
return {
'background': `${uri} no-repeat`,
'background-size': '100% 100%',
'background-color': 'transparent',
'height': '1em',
'width': '1em'
}
}
}
</script>
<style scoped>
icon{
display:inline-block;
}
</style>
结语
真正的成熟,不是你经历了什么,而是你怎么去理解你的经历。
这虽然只是一个小组件,可能还有许多的问题,但确实是我现在能够想到的好方案了。希望对大家有点帮助,也希望大家有好的方案在评论区不吝指教。
最后,其实vite-plugin-svg-icons
还是挺好用的,嘿嘿。