前端项目优雅使用svg
前言
在前端开发中,大家都接触到设计图。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></svg-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>