如何优雅的封装svg-icon并运用到项目中(webpack和vite、vue和react)
先看一下封装好的使用方式: 这里先讲
webpack
项目的相关配置,vite
项目对应的插件和配置教程在下方,需要的小伙伴可以直接往下翻
一. webpack项目配置流程(vite配置流程往下滑)
1. 安装依赖
首先需要在我们的项目中安装svg-sprite-loader这个loader,svg-sprite-loader是一个Webpack loader,用于将多个SVG文件打包成一个雪碧图(sprite)。它可以帮助优化网页性能,减少HTTP请求的数量,同时提供了一种方便的方式来管理和使用SVG图标
优点:
- 预加载 在项目运行时就生成所有图标,只需操作一次 dom
- 高性能 内置缓存,仅当文件被修改时才会重新生成
接下来我们开始安装
(1).安装和配置 svg-sprite-loader:
npm i svg-sprite-loader -D
pnpm install svg-sprite-loader -D
yarn add svg-sprite-loader -D
bun add svg-sprite-loader -D
(2).填写配置
如果是`vue-cli`搭建的项目,则需要在`vue.config.js`中添加:
const path = require('path')
// 项目的其他配置项,这里进行省略......
module.exports = defineConfig({
// 项目的其他配置项,这里进行省略......
chainWebpack: (config) => {
config.module.rule('svg').exclude.add(path.resolve('src/icons')).end()
config.module
.rule('icons')
.test(/\.svg$/)
.include.add(path.resolve('src/icons'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]'
})
.end()
}
})
原生`webpack`的项目,则需要在`webpack.config.js`中添加对应的loader配置:
const path = require('path')
// 其他配置项,这里进行省略......
module.exports = {
// 其他配置项,这里进行省略......
module: {
// 其他配置项,这里进行省略......
rules: [
// 其他loader,这里进行省略......
{
test: /\.svg$/,
loader: 'svg-sprite-loader',
include: [path.resolve('src/icons')],
options: {
symbolId: 'icon-[name]'
}
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
},
exclude: [path.resolve('src/icons')]
}
]
}
}
注意:url-loader 中要将 icons 文件夹排除, 不让 url-loader 处理该文件夹
CRA创建的
react
项目,需要先eject
出webpack.config.js
shellnpm run eject
然后在
module.rules.oneOf
里找到svg对应的loader,在原有的loader配置里添加exclude
,排除我们svgIcon组件存放svg的文件夹,然后新增一项,配置svg-spite-loader
exclude: [path.resolve('src/icons')], // 添加到原有配置里
// 这是需要新增的
{
test: /\.svg$/,
include: [path.resolve('src/icons')],
use: [
{
loader: 'svg-sprite-loader',
options: {
symbolId: 'icon-[name]'
}
}
]
},
2. 新建组件和目录
(1).在src
下icons
文件夹下新建svg
目录和index.js
文件(如果是react
组件,不需要index.js
,只创建目录存放svg
即可)
目录结构:
src/icons
- /svg # svg目录保存图标
- /index.ts # 注册全局组件 (react不需要这个)
(2).封装svg-icon组件
vue3:components 中新建组件 `SvgIcon.vue`:
<template>
<div v-if="isExternalClass" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-bind="$attrs"></div>
<svg v-else :class="svgClass" aria-hidden="true" v-bind="$attrs">
<use :href="iconName" />
</svg>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface Prop {
iconClass: string
className?: string
}
const props = defineProps<Prop>()
/** 用于判断组件传入的iconClass是不是路径或链接, 如果是路径或者链接则使用div加mask遮罩的方式展示 */
const isExternalIcon = (path: string): boolean => {
return /^(\/)+([a-zA-Z0-9\s_\\.\-():/])+(.svg|.png|.jpg)$/g.test(path) || /^(https?:|mailto:|tel:)/.test(path)
}
const isExternalClass = computed(() => isExternalIcon(props.iconClass))
/** 拼接成use标签的href */
const iconName = computed(() => `#icon-${props.iconClass}`)
// svg标签的class
const svgClass = computed(() => {
if (props.className) {
return 'svg-icon ' + props.className
} else {
return 'svg-icon'
}
})
// 传入路径或者链接时div的mask遮罩样式
const styleExternalIcon = computed(() => {
return {
mask: `url(${props.iconClass}) no-repeat 50% 50%`,
'-webkit-mask': `url(${props.iconClass}) no-repeat 50% 50%`
}
})
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
stroke: var(--background-primary);
overflow: hidden;
display: inline;
position: relative;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover !important;
display: inline-block;
}
</style>
如果是vue2项目,可以使用下面这个组件:components 中新建组件 `SvgIcon.vue`:
<template>
<svg :class="svgClass" aria-hidden="true">
<use :xlink:href="iconName"></use>
</svg>
</template>
<script>
export default {
name: 'svg-icon',
props: {
iconClass: {
type: String,
required: true
},
className: {
type: String
}
},
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>
// svgIcon.jsx
import React from 'react';
import './svgIcon.css'
const requireAll = (requireContext) => {
return requireContext.keys().map(requireContext)
}
// 这里的目录对应着存放svg的目录
const req = require.context('./svg', true, /.svg$/)
requireAll(req)
const Icon = ({ iconClass, className }) => {
const svgClass = className ? `svg-icon ${className}` : 'svg-icon';
const iconName = `#icon-${iconClass}`;
return (
<svg className={svgClass} aria-hidden="true">
<use xlinkHref={iconName} />
</svg>
);
};
Icon.defaultProps = {
className: ''
};
export default Icon;
/* svgIcon.css */
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
`react`组件到上面这一步就已经可以了,下面是vue对应的配置
Vue:在icon目录下新建的index.ts
:
// 引入封装好的SvgIcon组件
import SvgIcon from '@/components/SvgIcon.vue' // svg component
import type { App } from 'vue'
// 全局注册
export const registerSvgIcon = (app: App): void => {
app.component('svg-icon', SvgIcon)
const req = require.context('./svg', false, /\.svg$/)
// eslint-disable-next-line
const requireAll = (requireContext: any) =>
requireContext.keys().map(requireContext)
requireAll(req)
}
然后在项目的main.ts中引入:
main.ts
部分代码
// 其他导入,这里省略......
import { createApp } from 'vue'
import App from './App.vue'
import { registerSvgIcon } from '@/icons'
const app = createApp(App).use(router).use(i18n).use(pinia)
registerSvgIcon(app)
app.mount('#app')
/* vue2直接Vue.use(registerSvgIcon)即可 */
Vue.use(registerSvgIcon) // vue2
3. 最后我们就可以在项目中全局使用了
icon-class传入的就是svg目录下文件的文件名,比如目录之放一个有个`goBack.svg`文件,我们就可以按照下面的方式使用。
<svg-icon icon-class="goBack" />
也可以传入路径或者图片链接的方式,当然这种方式上面vue2对应的组件没有进行封装,感兴趣的小伙伴可以自己琢磨一下
<svg-icon icon-class="../images/logo.png" />
react :
import SvgIcon from './icons/svgIcon'
<SvgIcon iconClass="goBack" className="goBack" />
二. vite项目配置流程
1. 安装vite-plugin-svg-icons和fast-glob
安装vite-plugin-svg-icons
yarn add vite-plugin-svg-icons -D
npm i vite-plugin-svg-icons -D
pnpm install vite-plugin-svg-icons -D
bun add vite-plugin-svg-icons -D
安装fast-glob
yarn add fast-glob -D
npm i fast-glob -D
pnpm install fast-glob -D
bun add fast-glob -D
2. 配置插件
- 在
vite.config.ts
中的配置插件
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'
export default () => {
return {
plugins: [
createSvgIconsPlugin({
// 指定需要缓存的图标文件夹
iconDirs: [path.resolve(process.cwd(), 'src/icons')],
// 指定symbolId格式
symbolId: 'icon-[dir]-[name]',
/**
* 自定义插入位置
* @default: body-last
*/
inject?: 'body-last' | 'body-first'
/**
* custom dom id
* @default: __svg__icons__dom__
*/
customDomId: '__svg__icons__dom__',
}),
],
}
}
- 在
src/main.ts
内引入注册脚本
import 'virtual:svg-icons-register'
- 在src目录下新建icons文件夹用来存放svg图表
注意:不同与上面webpack的插件配置方式,这里不需要我们手动创建导入的js文件,在上面配置项的iconDirs里指定目录即可,比如上面配置了`'src/icons'`这个目录,只需要把svg图标全部存放到这个目录即可
3. 封装vue组件
创建svgIcon.vue
组件:
<template>
<svg aria-hidden="true" :style="{ width, height }">
<use :xlink:href="symbolId" :fill="color" />
</svg>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
name: string
prefix?: string
color?: string
width?: string
height?: string
}
const props = withDefaults(defineProps<Props>(), {
prefix: 'icon',
color: '#333',
width: '16px',
height: '16px'
})
const symbolId = computed(() => `#${props.prefix}-${props.name}`)
</script>
和上面wabpack不同的是,目录可以更深层级的嵌套,比如`icon`目录下有个`dir`子目录,子目录之下放`demo.svg`文件,组件内就可以直接使用目录名-文件名的方式,比如:`name="dir-demo"`
icons 目录结构:
# src/icons
- icon1.svg
- icon2.svg
- icon3.svg
- dir/icon1.svg
其他组件中使用:
<template>
<div>
<SvgIcon name="icon1"></SvgIcon>
<SvgIcon name="icon2"></SvgIcon>
<SvgIcon name="icon3"></SvgIcon>
<SvgIcon name="dir-icon1"></SvgIcon>
</div>
</template>
<script setup lang="ts">
import SvgIcon from './components/SvgIcon.vue'
</script>
也可以注册为全局组件,不用每次都导入:
在components目录下新建一个index.ts
文件
// 引入项目中的全部全局组件
import SvgIcon from './svgIcon/index.vue'
//其他组件,这里省略...
// 组装成一个对象
const allGlobalComponents: any = {
SvgIcon,
//其他组件,这里省略...
}
// 对外暴露插件对象,在main.ts中引入后,直接自动调用install方法
export default {
install(app: any) {
// 循环注册所有的全局组件
Object.keys(allGlobalComponents).forEach((componentName) => {
app.component(componentName, allGlobalComponents[componentName])
})
},
}
main.ts
中导入并use:
import { globalComponents } from '@/components'
reateApp(App).use(globalComponents).mount('#app')
4. 封装React组件(vite创建的react项目,vite.config.js配置和上面一样)
- (1).
/src/components
目录下新建svgIcon.jsx
组件:
export default function svgIcon({
name,
prefix = 'icon',
color = '#333',
width = '16px',
height = '16px',
...props
}) {
const symbolId = `#${prefix}-${name}`
return (
<svg { ...props } aria-hidden="true" style={{ width, height }}>
<use href={ symbolId } fill={ color } />
</svg>
)
}
(2). 直接导入使用即可
<SvgIcon name="icon1" width="20px" height="20px"></SvgIcon>
5. 获取所有 SymbolId
import ids from 'virtual:svg-icons-names'
// => ['icon-icon1','icon-icon2','icon-icon3']
6. 配置说明
参数 | 类型 | 默认值 | 说明 |
---|---|---|---|
iconDirs | string[] | - | 需要生成雪碧图的图标文件夹 |
symbolId | string | icon-[dir]-[name] | svg 的 symbolId 格式,见下方说明 |
svgoOptions | boolean|SvgoOptions | true | svg 压缩配置,可以是对象Options |
inject | string | body-last | svgDom 默认插入的位置,可选body-first |
customDomId | string | __svg__icons__dom__ | svgDom 插入节点的 ID |