我就说看文档可以学到新知识吧。我在unocss的官网找到了将svg文件变为css样式的方法。

557 阅读6分钟

前言

大家都知道,我们在做前端项目的时候,会有很多的定制化样式,其中图标就有很多。有些图标还有不同颜色的版本,但是ui设计用的又不是字体图标,如下载为png在不同的分辨率下效果又不好。使用svg倒是一个好方法,不会失真。但是不同颜色又不好处理。

用图片引入svg图标是不可以改变颜色的,只能不同颜色使用不同的svg图片,这样就不优雅了。在vue2项目当中,我们可以通过组件的方式统一使用svg图标,不管是自定义的还是图标库下载的,不管是单色的还是彩色的都可以。

但是在vue3中使用这个方法好像不行。

起因

目前,我们使用svg图标的方法在vue3中大概就以下几种方法:

  1. 将svg文件代码复制到vue文件中,做为一个组件使用。
  2. 使用插件-vite-plugin-svg-icons
  3. 使用img用图片的方式使用
  4. 变为base64代码使用
  5. 直接在页面当中使用

在做项目的时候,我当时使用插件来使用,但是当时出了一点问题,我比较菜嘛很正常,就死活不能将svg图标显示出来。

然后我就想,之前在xxx项目当中,那个svg图标就直接用组件的方式使用的,我在这个项目当中也自己搞一个组件引用不就行了。说干就干,开始。

项目太多找不到了,ε=(´ο`*)))唉!

幸亏我有everything,大概想起来名字应该是有一个icon,然后加了svg,是一个vue组件。最终还是让我在备份盘中找到了这个组件。

但是这是vue2的项目,而且也是使用插件来支撑的。

复制到vue3当中,有很多报错,还获取不到svg的内容,还是无法显示。

然后就是这个想法就搁置啦,我又不是架构师,我只是个打工的。还有很多bug要修改呢,先用img应付一下。

转机来了

是什么呢?就是我在看unocss文档的时候,看到图标那一个地方,有一个tip:

image.png

推荐阅读纯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,组件完结啦,

007339AE.gif

最终成果

<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还是挺好用的,嘿嘿。