web前端

在vue项目中使用 SVG Sprites

2020-11-27  本文已影响0人  雨翼195

前言

网页图标展示方式大概可以分为以下几类

前端工程化 - 工程配置 vue-cli3+

在了解 SVG Sprites 技术之后,引出本文的主角:svg-sprite-loader 这是关键

1、雏形

vue-cli 内置的webpack配置会将svg文件使用 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的官方文档中看到这么一段话:

2020-11-27_114659.jpg

不再局限于 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、和 importrequire 说再见吧

经过上面的优化后,在svg文件引入的方式上还是有些不尽人意,有没有像 img 标签那样舒服的使用方式呢?当然有!首先看看在基于vue-cli脚手架的项目中是怎么实现:
处理静态资源

2020-11-27_120854.jpg
当然,这需要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>

mounteddestroyed 生命周期里的处理是为了“用完即销毁”,可自行斟酌,非必要,因为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>
上一篇 下一篇

猜你喜欢

热点阅读