前言
在前端开发中,大家都接触到设计图。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>
通过上面示例代码可以看出:
<use>
元素可以跨<svg>
元素引用<symbol>
<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-loader
和svgo-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>
评论区