前言 在前端开发中,大家都接触到设计图。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 后才可展示图形。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 //定义图形 <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
**组件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 <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,它的功能是把组件注册到全局,方便使用:
1 2 3 4 5 6 7 8 9 10 11 import Vue from "vue" ;import SvgIcon from "@/components/SvgIcon" ; Vue .component ("svg-icon" , SvgIcon );const requireAll = (requireContext ) => requireContext.keys ().map (requireContext); const req = require .context ("./" , true , /\.svg$/ );requireAll (req);
在main.js
中引入 这一步就是把文件注册到全局上。
下面是最重要的一步:
主要是修改 loader 和一个去除 SVG 内部默认的 full 属性值。
本来载入和删除 full 对于大部分的项目够用了,但是难免遇到复杂的 SVG,这个时候就不能去掉它的颜色了。所以对配置做了修改。新增了 original 文件夹存放不需要去除配色的文件了
1 2 import "@/assets/icons/index" ;
修改vue.config.js
下面主要用到 2 个插件svg-sprite-loader
和svgo-loader
。所以先安装他们
1 npm i svg-sprite-loader svgo-loader -D
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 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; }; 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 (); 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
的文件,然后在文件上这样使用
1 <svg-icon name ="main" class-name ="icon" > <svg-icon > </svg-icon > </svg-icon >
在 Vite 上使用 安装 svg-sprite-loader
1 npm i svg-sprite-loader --save-dev
SvgIcon component 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <!-- /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 >
1 2 3 4 5 6 7 import SvgIcon from "./icon.vue" ;import type { SvgIconProps } from "./icon.vue" ;export { SvgIcon };export type { SvgIconProps };
新建plugins
文件下新建svgBuilder.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 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
1 2 3 4 5 6 7 { "include" : [ "vite.config.ts" , "./plugins/*" ] }
src 目录下新建icons
文件夹内存放 svg 图标 vite.config.ts 中 1 2 3 4 5 6 7 8 9 10 11 import svgBuilder from "./plugins/svgBuilder" ;import { resolve } from "path" ;plugins : [ svgBuilder (resolve ("./src/icons" )), ];
使用 1 2 3 4 5 6 7 8 9 10 11 12 13 <script setup lang="ts" > import {SvgIcon } from "@/components/SvgIcon" ;</script> <template > <SvgIcon name ="cookie" /> </template >