前言
网页图标展示方式大概可以分为以下几类
- img 标签。最原始的方式,不支持改变颜色
- css sprites。其实质还是使用img标签进行展示
- svg 标签。可以改变颜色,使代码臃肿,不易维护。可使用 svg-inline-loader、vue-svg-loader(目前ant-design-vue使用该方式) 进行优化
- font 图标。常用的生成字体图标的工具:IconFont、IcoMoon
- SVG Sprites。本文重点推荐,IconFont 推荐使用该方式,实质是对svg标签展示方式进行优化,参考文章:未来必热:SVG Sprites技术介绍、svg-sprite-loader 使用教程
前端工程化 - 工程配置 vue-cli3+
在了解 SVG Sprites
技术之后,引出本文的主角:svg-sprite-loader 这是关键
1、雏形
vue-cli
内置的webpack配置会将svg文件使用 file-loader
进行加载,这不符合我们当前的需求
- 解决方案:固定目录下的svg文件使用
svg-srpite-loader
进行加载,其它则继续使用file-loader
,目前网上搜索到的解决方案基本如此
// vue.config.js
const path = require('path');
function Resolve(dir) {
return path.join(__dirname, dir);
}
module.exports = {
chainWebpack: config => {
config.module.rule('svg').exclude.add(Resolve('src/assets/icons')); //让file-loader排除掉这个目录
config.module.rule('icons').test(/\.svg$/).include.add(Resolve('src/assets/icons')).end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]',
}).end()
//使用svgo-loader 进行优化,去除svg本身的填充色
.use('svgo-loader').loader('svgo-loader').options({
plugins: [
{removeAttrs: {attrs: 'path:fill'}},
{ removeXMLNS: true }
]
})
}
}
// main.vue
<template>
<svg>
<use :xlink:href="'#'+iconId"></use>
</svg>
</template>
<script>
import MySvg '@/assets/icons/logo.svg'; // src/assets/icons/logo.svg
export default {
data() {
return {
iconId: MySvg.default.id
}
}
}
</script>
这样做有一个问题,如果图标是页面专属,其它页面不会用到,把svg都放在同一个目录下,没有跟随组件目录,那这样就非常不好管理。
2、优化
在vue-cli的官方文档中看到这么一段话:
不再局限于 src/assets/icons
目录下,而是判断文件名以 .icon.svg
结尾:
// vue.config.js
const path = require('path');
function Resolve(dir) {
return path.join(__dirname, dir);
}
module.exports = {
chainWebpack: config => {
//svg sprites
const svgRule = config.module.rule('svg')
svgRule.uses.clear()
svgRule.oneOf('icon').test(/\.icon\.svg$/)
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon_[hash:base64:5]',
}).end()
.use('svgo-loader').loader('svgo-loader').options({
plugins: [
{removeAttrs: {attrs: 'path:fill'}},
{ removeXMLNS: true }
]
}).end()
//普通svg图片
svgRule.oneOf('img').test(/\.svg$/)
.use('file-loader')
.loader('file-loader').options({
name: 'img/[name].[hash:8].[ext]'
})
}
}
3、和 import
、require
说再见吧
经过上面的优化后,在svg文件引入的方式上还是有些不尽人意,有没有像 img
标签那样舒服的使用方式呢?当然有!首先看看在基于vue-cli
脚手架的项目中是怎么实现:
处理静态资源
当然,这需要
vue-loader
处理,详细文档修改vue-loader后的最终配置:
const path = require('path');
function Resolve(dir) {
return path.join(__dirname, dir);
}
module.exports = {
chainWebpack: config => {
//svg sprites
const svgRule = config.module.rule('svg')
svgRule.uses.clear()
svgRule.oneOf('icon').test(/\.icon\.svg$/)
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon_[hash:base64:5]',
}).end()
.use('svgo-loader').loader('svgo-loader').options({
plugins: [
{removeAttrs: {attrs: 'path:fill'}},
{ removeXMLNS: true }
]
}).end()
//普通svg图片
svgRule.oneOf('img').test(/\.svg$/)
.use('file-loader')
.loader('file-loader').options({
name: 'img/[name].[hash:8].[ext]'
})
//vue-loader
config.module
.rule('vue')
.use('vue-loader')
.loader('vue-loader')
.tap(options => {
// 修改它的选项...
options.transformAssetUrls = {
video: ['src', 'poster'],
source: 'src',
img: 'src',
image: ['xlink:href', 'href'],
use: ['xlink:href', 'href'],
'vc-svg-icon': 'src' //这一项是关键,下面的组件封装有用到
}
return options
})
}
}
上面的 options.transformAssetUrls['vc-svg-icon'] = 'src'
,让vue-loader
识别vc-svg-icon
组件的src
属性,并对其解析为一个 模块依赖
4、组件封装
<template>
<svg class="vc-svg-icon" aria-hidden="true" :icon-href="href">
<use :xlink:href="href"></use>
</svg>
</template>
<script>
export default {
name: 'SvgIcon',
props: {
src: module
},
computed: {
href() {
return `#${this.src.default.id}`
}
},
mounted() {
//初始化时查找是否存在该 symbol
const container = document.querySelector('#__SVG_SPRITE_NODE__')
if (!container.querySelector(this.href)) {
container.insertAdjacentHTML('beforeend', this.src.default.content)
}
},
destroyed() {
//组件销毁后查找是否存在无用的 symbol,没有被vc-svg-icon组件引用则移除对应的symbol
const hasIcon = document.querySelector(`[icon-href="${this.href}"]`)
const symbol = document.querySelector('#__SVG_SPRITE_NODE__').querySelector(this.href)
if (!hasIcon && symbol) {
symbol.remove()
}
}
}
</script>
<style>
.vc-svg-icon {
width: 1em;
height: 1em;
vertical-align: middle;
fill: currentColor;
overflow: hidden;
}
</style>
mounted
和 destroyed
生命周期里的处理是为了“用完即销毁”,可自行斟酌,非必要,因为symbol标签的父级svg#__SVG_SPRITE_NODE__
是隐藏的(display:none
),大量存在无用的symbol标签也不会造成性能问题
5、使用
//*.vue
<template>
<div>
<!-- 一般使用 -->
<vc-svg-icon src="./***/***.icon.svg"></vc-svg-icon>
<!-- src 值在 data 中 -->
<vc-svg-icon :src="myIcon"></vc-svg-icon>
</div>
</template>
<script>
export default {
data() {
return {
myIcon: require('./***/***.icon.svg')
}
}
}
</script>