侧边栏壁纸
博主头像
MicroMatrix博主等级

曲则全,枉则直,洼则盈,敝则新,少则得,多则惑。是以圣人抱一为天下式。不自见,故明;不自是,故彰;不自伐,故有功;不自矜,故长。夫唯不争,故天下莫能与之争。古之所谓“曲则全”者,岂虚言哉!诚全而归之。

  • 累计撰写 80 篇文章
  • 累计创建 21 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

前端项目优雅使用svg

蜗牛
2022-11-27 / 0 评论 / 0 点赞 / 7 阅读 / 13272 字 / 正在检测是否收录...

前言

在前端开发中,大家都接触到设计图。Figma或者蓝图,在线设计或者看图的网站。UI设计又很喜欢那种花里胡哨的图标,很难找。神奇的是你可以把它们保存为SVG图标。但是SVG的引入又是一大串长长的代码,更麻烦的是有些图标悬浮上去是要改变颜色的。这里我找到了一种让SVG方便引入的方法,而且能像字体文件一样,简单的改变颜色和大小。

SVG Sprite

如果你没听过SVG Sprite,也许听过雪碧图(CSS Sprite),如下图所示。雪碧图是为了减少网络请求次数,将许多小图标整合到一张图片上,然后通过CSS定位技术显示特定位置的图标。雪碧图在使用上存在一些弊端,目前已经很少使用了。

类似的SVG Sprite是通过<symbol><use>实现的。<symbol>元素可以把SVG图标定义成一个图形模板对象,<use>元素通过xlink:href属性引用symbol id展示图形。下面代码定义了三个<symbol>图形模板,此时图形并不会展示到页面上,通过<use>元素引用symbol id后才可展示图形。

//定义图形
<svg width="0" height="0">
    <symbol id="shape1">
        <circle cx="40" cy="40" r="24" style="stroke:#006600; fill:#00cc00"/>
    </symbol>
    <symbol id ="shape2">
        <rect x="10" y="10" height="100" width="100" style="stroke:#006600; fill: #00cc00"/>
    </symbol>
    <symbol id="shape3">
        <polygon points="10,0  60,0  35,50" style="stroke:#660000; fill:#cc3333;"/>
    </symbol>
</svg>
//引用图形
<svg width="500" height="200">
    <use xlink:href="#shape1" x="0" y="25" />
    <use xlink:href="#shape1" x="60" y="25" />
    <use xlink:href="#shape2" x="150" y="0" />
    <use xlink:href="#shape3" x="280" y="10" />
</svg>

通过上面示例代码可以看出:

  1. <use>元素可以跨<svg>元素引用<symbol>
  2. <use>可以重复引用<symbol>.

如果将项目中的SVG图标用<symbol>元素定义成图形模板,并将其组合成一个大的<svg>加载到页面中,如下图所示。那么我们可以在页面的任何位置,只需要一行代码就可以引用这个图标了。

webpack-vue上使用

**src/components下建立一个SvgIcon**组件

<template>
  <svg :class="svgClass" aria-hidden="true">
    <use :xlink:href="iconName"/>
  </svg>
</template>

<script>
export default {
  name: 'SvgIcon',
  props: {
    iconClass: {
      type: String,
      required: true,
    },
    className: {
      type: String,
      default: '',
    },
  },
  computed: {
    iconName () {
      return `#icon-${this.iconClass}`
    },
    svgClass () {
      if (this.className) {
        return 'svg-icon ' + this.className
      } else {
        return 'svg-icon'
      }
    },
  },
}
</script>

<style scoped>
.svg-icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}
</style>

在src/assets/下建立一个icons文件夹,然后下面再建一个svg文件夹

这个SVG文件夹主要存放SVG图标。icons文件夹下再建一个index.js,它的功能是把组件注册到全局,方便使用:

import Vue from 'vue'
import SvgIcon from '@/components/SvgIcon' // svg组件

// 注册到全局
Vue.component('svg-icon', SvgIcon)

const requireAll = requireContext => requireContext.keys().map(requireContext)
// 这里本来是直接找到svg文件夹下的文件了,但是为了不去处一些svg的配色需求,所以改为获取当前目录下所有的svg图片。
const req = require.context('./', true, /\.svg$/)
requireAll(req)

main.js中引入

这一步就是把文件注册到全局上。

下面是最重要的一步:

主要是修改loader和一个去除SVG内部默认的full属性值。

本来载入和删除full对于大部分的项目够用了,但是难免遇到复杂的SVG,这个时候就不能去掉它的颜色了。所以对配置做了修改。新增了original文件夹存放不需要去除配色的文件了

// svg 优雅使用
import "@/assets/icons/index"

修改vue.config.js

下面主要用到2个插件svg-sprite-loadersvgo-loader。所以先安装他们

npm i svg-sprite-loader svgo-loader  -D
module.exports = {
    chainWebpack: config => {
    // 第一步
    const svgRule = config.module.rule("svg-sprite");
    svgRule.uses.clear();
    svgRule
      .test(/\.(svg)(\?.*)?$/)
      .include.add([resolve("src/assets/icons")])
      .end()
      .use("svg-sprite-loader")
      .loader("svg-sprite-loader")
      .options({
        symbolId: "icon-[name]"
      })
      .end();

    // 第二步
    const findFileFolder = (dir, filename) => {
      const files = fs.readdirSync(resolve(dir));
      const result = [];
      files.map(file => {
        const filePath = `${dir}/${file}`;
        if (fs.statSync(filePath).isDirectory()) {
          if (file === filename) {
            result.push(filePath);
          } else {
            result.push(...findFileFolder(filePath, filename));
          }
        }
      });
      return result;
    };

    // svgo-loader 去除svg文件中的fill属性,方便前端更改颜色
    // 对于不需要更改颜色的svg,
    // 在对应文件夹(common/(项目名1)/(项目名2)/...)中创建子文件夹
    // 命名为“original”(!!必须!!)
    // 将不会更改颜色(多颜色)的svg放入original文件夹,默认不loader此文件夹文件
    const svgoRule = config.module.rule("svgo");
    const svgoExcludePaths = findFileFolder("src/assets/icons", "original");
    svgoRule
      .test(/\.(svg)(\?.*)?$/)
      .exclude.add([...svgoExcludePaths.map(path => resolve(path))])
      .end()
      .use("svgo-loader")
      .loader("svgo-loader")
      .tap(options => ({
        ...options,
        plugins: [{ name: "removeAttrs", params: { attrs: "fill" } }]
      }))
      .end();



    // 原有的svg图像处理loader添加exclude
    config.module
      .rule("svg")
      .exclude.add(resolve("src/assets/icons"))
      .end();
    config.resolve.alias
      .set("@", resolve("src"))
      .set("assets", resolve("src/assets"))
      .set("components", resolve("src/components"));
  },
}

使用

之后把SVG导入到src/assets/svg/下,例如你导入了一张main.svg的文件,然后在文件上这样使用

<svg-icon name="main" class-name="icon"> <svg-icon>

在Vite上使用

安装 svg-sprite-loader

npm i svg-sprite-loader --save-dev

SvgIcon component

<!-- /src/components/SvgIcon/icon.vue -->
<script setup lang="ts">
import {computed, useCssModule, useAttrs} from "vue";

export interface SvgIconProps {
  name: string
}

const props = defineProps<SvgIconProps>()

const styles = useCssModule()
const attrs = useAttrs()

const iconName = computed(() => `#icon-${props.name}`);
const svgClass = computed(() => {
  const className = [styles['svg-icon']]
  if (props.name) className.push(`icon-${props.name}`)
  return className
})
</script>

<template>
  <svg :class="svgClass" v-bind="attrs">
    <use :xlink:href="iconName"></use>
  </svg>
</template>

<style module>
.svg-icon {
  width: 1em;
  height: 1em;
  fill: currentColor;
  vertical-align: middle;
  overflow: hidden;
}
</style>
// /src/components/SvgIcon/index.ts
import SvgIcon from './icon.vue'
import type {SvgIconProps} from './icon.vue'

export {
    SvgIcon
}

export type {
    SvgIconProps
}

新建plugins文件下新建svgBuilder.ts

// /plugins/svgBuilder.ts
import {readFileSync, readdirSync} from 'fs'
import {join as pathJoin} from 'path'
import {Plugin} from 'vite'

let idPrefix = ''
const svgTitle = /<svg([^>+].*?)>/
const clearHeightWidth = /(width|height)="([^>+].*?)"/g
const clearFill = /fill="[^>+].*?"/g;

const hasViewBox = /(viewBox="[^>+].*?")/g

const clearReturn = /(\r)|(\n)/g

const findSvgFile = (dir: string) => {
    const svgRes: string[] = []
    const directory = readdirSync(dir, {withFileTypes: true})
    for (const dirent of directory) {
        if (dirent?.isDirectory()) {
            svgRes.push(...findSvgFile(pathJoin(dir, dirent.name, '/')))
        } else {
            const svg = readFileSync(pathJoin(dir, dirent.name))
                .toString()
                .replace(clearReturn, '')
									.replace(clearFill,'')
                .replace(svgTitle, ($1: string, $2: string) => {
                    let width = '0'
                    let height = '0'
                    let content = $2.replace(
                        clearHeightWidth,
                        (s1, s2:string, s3:string) => {
                            if (s2 === 'width') {
                                width = s3
                            } else if (s2 === 'height') {
                                height = s3
                            }
                            return ''
                        }
                    )
                    if (!hasViewBox.test($2)) {
                        content += `viewBox="0 0 ${width} ${height}"`
                    }
                    return `<symbol id="${idPrefix}-${dirent.name.replace(
                        '.svg',
                        ''
                    )}" ${content}>`
                })
                .replace('</svg>', '</symbol>')
            svgRes.push(svg)
        }
    }
    return svgRes
}

const svgBuilder = (path: string, prefix = 'icon'): Plugin => {
    idPrefix = prefix
    const res = findSvgFile(path)
    return {
        name: 'svg-transform',
        transformIndexHtml(html) {
            return html.replace(
                '<body>',
                `<body>
                <svg xmlns="http://www.w3.org/2000/svg" 
                    xmlns:xlink="http://www.w3.org/1999/xlink" 
                    style="position: absolute; width: 0; height: 0">
              ${res.join('')}
              </svg>`
            )
        }
    }
}

export default svgBuilder

更新tsconfig.node.json

// tsconfig.node.json

{

// ...

"include": ["vite.config.ts", "./plugins/*"]

}

src目录下新建icons文件夹内存放svg图标

vite.config.ts中

import svgBuilder from './plugins/svgBuilder'

import {resolve} from "path";

// ...

plugins: [

// ...

svgBuilder(resolve('./src/icons'))

]

使用

<script setup lang="ts">

import {SvgIcon} from "@/components/SvgIcon";

</script>

<template>

<!-- name 为 svg icon 的文件名 -->

<SvgIcon name="cookie"/>

</template>
0

评论区