svg-sprite-loader

使用 svg-sprite-loader 实现 svg 图标按需加载

参考文档:

https://cloud.tencent.com/developer/article/1764171

https://www.cnblogs.com/zys2019/p/15093222.html

使用 svg-sprite-loader 在 Vue3 项目中实现图标按需加载

直接使用svg的问题

参考文档: https://www.cnblogs.com/Leophen/p/13201907.html

一般用法在 Vue 中直接使用 svg,但既然已经是用 Vue来组件化开发项目了,那么在组件中穿插着一大段的 svg 也显得过于杂乱;

这里可以通过 svg 的 use 标签,将 svg 的一大段绘制代码封装在 symbol 中,然后通过 use 调用。

例如,将所有绘制 svg 的代码放到 svg-icon.vue 文件中,所有图标的绘制代码使用 symbol 标签分隔开并单独命名,然后将这个文件当做组件导出,在主页面中引入此组件,接着在需要使用 svg 图标的地方通过 use 标签将其引入, svg-icon.vue 代码示例如下:

<template>
  <svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink"
    style={{position:'absolute',width:0,height:0}}>
    <defs>
      <symbol viewBox="0 0 26 31" id="location">
        <path xmlns="http://www.w3.org/2000/svg"
          d="M512.571517 65.907059c-204.736964 0-370.715183 165.979242-370.715183 370.724393 0 94.440929 35.320437 180.625824 93.462648 246.083651 1.572822 2.690272 3.50994 5.225001 5.817496 7.531534l240.297878 251.597225c1.279133 1.864464 2.736321 3.64297 4.393054 5.298679 2.111081 2.111081 4.418636 3.90596 6.856152 5.402033 14.458293 10.06524 34.491559 8.658194 47.393403-4.242627 3.26537-3.263323 5.78782-6.987135 7.582699-10.960633L783.610536 690.24766c1.867534-1.866511 3.489474-3.88447 4.876054-6.010901 58.951647-65.640999 94.819552-152.431691 94.819552-247.604284C883.305119 231.886301 717.325877 65.907059 512.571517 65.907059zM512.390391 588.611865c-82.734306 0-149.814074-67.087954-149.814074-149.842727 0-82.753749 67.079768-149.833517 149.814074-149.833517 82.772168 0 149.851936 67.079768 149.851936 149.833517C662.242328 521.523911 595.161536 588.611865 512.390391 588.611865z"
          fill="#d81e06" />
      </symbol>
    </defs>
  </svg>
</template>

这里将整个 vue 组件导出一个大的 svg,此 svg 中包含了许多小图标,类似于精灵图,每个图标使用 symbol 分隔,并单独命名以方便调用。

在 index.vue 中使用示例:

... 
<svg class="location-icon"> 
  <use xlink:href="#location"></use> 
</svg> 
...

不过还有个问题,如果当一个页面需要用到的 svg 图标很多,势必就造成 svg-icon.vue 这个文件非常大,当另一个页面只需要用到其中一个 svg 图标时,就需要把包含几百个图标的 svg 组件加载进去,明显不太友好; 最好是能够实现按需加载,当前页面需要哪些图标就加载哪些。

通过 vue-svg-icon 插件实现按需加载

安装

npm install vue-svg-icon --save-dev

在项目的 main.js 入口引入 vue-svg-icon 以便全局调用

import Icon from 'vue-svg-icon/Icon.vue'; 
Vue.component('icon', Icon);

在组件中按需加载需要的图标

例如 pen.svg 放到了 /src/svg 目录中,在 vue 组件按需加载:

<template> 
  <icon name="pen" scale="1"></icon> 
</template>

这里可以通过修改 scale 属性值或在使用的页面中修改 svg 样式来调整图标大小

安装

npm install svg-sprite-loader --save-dev

svg图片导入项目

示例: email.svg

<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  <path d="M4 4H20C21.1 4 22 4.9 22 6V18C22 19.1 21.1 20 20 20H4C2.9 20 2 19.1 2 18V6C2 4.9 2.9 4 4 4Z" stroke="currentColor"
    stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
  <path d="M22 6L12 13L2 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>

这里将 svg 图标中对应的图标颜色值改为字符串 currentColor,方便使用时控制图标实时颜色 将图标放在特定文件夹下,这里以 @/assets/svg 中导入的 svg 为例。

配置 vue.config.js

const path = require("path");

module.exports = {
  // 配置使用stylus全局变量
  chainWebpack: config => {
    const svgRule = config.module.rule("svg");
    svgRule.uses.clear();
    svgRule
      .use("svg-sprite-loader")
      .loader("svg-sprite-loader")
      .options({
        symbolId: "icon-[name]"
      })
      .end();
  }
};

我的vue.config.js

const { defineConfig } = require('@vue/cli-service')
const CompressionPlugin = require('compression-webpack-plugin');
const { resolve } = require('path');

module.exports = defineConfig({
  devServer: {
    allowedHosts: ['wl.wl_dev']
  },
  lintOnSave: false,
  transpileDependencies: [
    'quasar'
  ],

  pluginOptions: {
    quasar: {
      importStrategy: 'kebab',
      rtlSupport: false
    }
  },
  productionSourceMap: false, 
  chainWebpack(config) {
    delete config.plugins['prefetch'];
    if(process.env.NODE_ENV !== 'development') {
      config.plugin('compressionPlugin').use(
        new CompressionPlugin({
          test: /\.(js|css|html)$/,
          threshold: 10240,
        })
      )
    }

    config.module.rules.delete("svg")
    const svgRule = config.module.rule("svg-sprite-loader");
    svgRule.test(/\.svg$/).include.add(resolve('src/assets/svg')).end()
    svgRule.use("svg-sprite-loader")
      .loader("svg-sprite-loader")
      .options({
        symbolId: "icon-[name]"
      })
      .end();
  }
})

新建 SvgIcon.vue 文件

新建 SvgIcon.vue 文件,这里可传入 name 属性控制图标类型,传入 size 属性控制图标大小,传入 color 属性控制图标颜色。

views/app/components/SvgIcon.vue

<template>
  <svg class="svg-icon" :style="{
    width: props.size + 'px',
    height: props.size + 'px',
    color: props.color
  }" @mousedown="clickIcon">
    <use :xlink:href="`#icon-${props.name}`" :fill="props.color" />
  </svg>
</template>

<script>
import { defineComponent } from "vue";

export default defineComponent({
  name: "SvgIcon",
  props: {
    name: {
      type: String,
      required: true,
      default: "email"
    },
    size: {
      type: Number,
      default: 32
    },
    color: {
      type: String,
      default: "#000"
    }
  },
  setup(props) {
    return {
      props
    };
  }
});
</script>

SvgIcon.vue添加为插件

src/plugins 目录下新建 svgIcon.js

import SvgIcon from "@/views/app/components/SvgIcon.vue";

const componentPlugin = {
  install: function(vue, options) {
    if (
      options &&
      options.imports &&
      Array.isArray(options.imports) &&
      options.imports.length > 0
    ) {
      const { imports } = options;
      imports.forEach((name) => {
        require(`@/assets/svg/${name}.svg`);
      });
    } else {

      const ctx = require.context("@/assets/svg", false, /\.svg$/);
      ctx.keys().forEach(path => {
        const temp = path.match(/\.\/([A-Za-z0-9\-_]+)\.svg$/);
        if (!temp) return;
        const name = temp[1];
        require(`@/assets/svg/${name}.svg`);
      });
    }
    vue.component(SvgIcon.name, SvgIcon);
  }
};
export default componentPlugin;

main.js 引入插件

在 main.js(或 main.ts)中引入上面的 plugin 文件

import plugin from "./plugin";

createApp(App)
  .use(plugin, {
    imports: []
  })

我的main.js

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import { Quasar } from 'quasar'
import quasarOpt from '@/plugins/quasar'
import Axios from './plugins/axios'
import * as Expand from './utils/expand'
// import 'default-passive-events'
import svgIconPlugin from './plugins/svgIcon'

try{
  store.commit('Auth/loadJwt')
} catch(err) {
  console.log(err)
}

const app =createApp(App)
app.config.globalProperties.Axios = Axios

app.use(Quasar, quasarOpt)
.use(store)
.use(router)
.use(svgIconPlugin)
.mount('#app')

图标组件的使用

<SvgIcon name="email" :size="24" color="green" />

报错解决

启动报错

参考文档: https://segmentfault.com/q/1010000037660061

报错信息:

Compiled with problems:X

ERROR in ./src/assets/desktop/bg-dark.svg

Module build failed (from ./node_modules/svg-sprite-loader/lib/loader.js):
ExtractPluginMissingException: svg-sprite-loader exception. svg-sprite-loader in extract mode requires the corresponding plugin
    at Object.loader (/data/Project/GitLab/WL_Ngrok_V2/src/frontend/admin/node_modules/svg-sprite-loader/lib/loader.js:47:13)


ERROR in ./src/assets/desktop/bg.svg

Module build failed (from ./node_modules/svg-sprite-loader/lib/loader.js):
ExtractPluginMissingException: svg-sprite-loader exception. svg-sprite-loader in extract mode requires the corresponding plugin
    at Object.loader (/data/Project/GitLab/WL_Ngrok_V2/src/frontend/admin/node_modules/svg-sprite-loader/lib/loader.js:47:13)


ERROR

Cannot read properties of undefined (reading 'get')
during rendering of asset asset/resource|/data/Project/GitLab/WL_Ngrok_V2/src/frontend/admin/node_modules/svg-sprite-loader/lib/loader.js??clonedRuleSet-6.use[0]!/data/Project/GitLab/WL_Ngrok_V2/src/frontend/admin/src/assets/desktop/bg-dark.svg

原因: webpack配置问题。

解决方法: 修改vug.config.js

const { defineConfig } = require('@vue/cli-service')
const CompressionPlugin = require('compression-webpack-plugin');
const { resolve } = require('path');
const path = require("path");

module.exports = defineConfig({
  devServer: {
    allowedHosts: ['wl.wl_dev']
  },
  lintOnSave: false,
  transpileDependencies: [
    'quasar'
  ],

  pluginOptions: {
    quasar: {
      importStrategy: 'kebab',
      rtlSupport: false
    }
  },
  productionSourceMap: false, 
  chainWebpack(config) {
    delete config.plugins['prefetch'];
    if(process.env.NODE_ENV !== 'development') {
      config.plugin('compressionPlugin').use(
        new CompressionPlugin({
          test: /\.(js|css|html)$/,
          threshold: 10240,
        })
      )
    }

    const svgRule = config.module.rule("svg");
    svgRule.uses.clear();
    svgRule.test(/\.svg$/).include.add(resolve('src/assets/svg')).end()
      .use("svg-sprite-loader")
      .loader("svg-sprite-loader")
      .options({
        symbolId: "icon-[name]"
      })
      .end();
  }
})

添加: svgRule.test(/\.svg$/).include.add(resolve('src/assets/svg')).end()

使用SvgIcon 不显示图片

参考文档: https://blog.csdn.net/weixin_45732235/article/details/123895025

将:

const svgRule = config.module.rule("svg");
svgRule.uses.clear();
svgRule.test(/\.svg$/).include.add(resolve('src/assets/svg')).end()
  .use("svg-sprite-loader")
  .loader("svg-sprite-loader")
  .options({
    symbolId: "icon-[name]"
  })
  .end();

改为:

config.module.rules.delete("svg")
const svgRule = config.module.rule("svg-sprite-loader");
svgRule.test(/\.svg$/).include.add(resolve('src/assets/svg')).end()
svgRule.use("svg-sprite-loader")
  .loader("svg-sprite-loader")
  .options({
    symbolId: "icon-[name]"
  })
  .end();