【Vue-Element-Admin 分析】- 04 图标组件是怎么工作的?__Vue.js
发布于 3 年前 作者 banyungong 1199 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy, hydrogen, condensed-night-purple, greenwillow, v-green, vue-pro, healer-readable, mk-cute, jzman, geek-black, awesome-green, qklhk-chocolate

贡献主题:https://github.com/xitu/juejin-markdown-themes

theme: mk-cute highlight:

前文链接

分析

图标组件的线索是非常多的,这里从 main.js 入手,可以看到引入了一个当前目录下的 icons

img01

直接跟过去即可,代码非常简单:

import Vue from 'vue'
import SvgIcon from '@/components/SvgIcon' // svg component

// register globally
Vue.component('svg-icon', SvgIcon)

const req = require.context('./svg', false, /\.svg$/)
const requireAll = requireContext =>
  requireContext.keys().forEach(requireContext)
requireAll(req)

前面的部分一眼就能明白,注册了一个 vue 的全局组件。而后面的部分设计到 webpack api,我们可以看一下:依赖管理,再结合当前目录文件:

img02

就能得知这段代码的功能是导入 ./svg 下的所有图标文件。

紧接着,跳转到 SvgIcon 看看它又是如何工作的:

<template>
  <div
    v-if="isExternal"
    :style="styleExternalIcon"
    class="svg-external-icon svg-icon"
    v-on="$listeners"
  />
  <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
    <use :xlink:href="iconName" />
  </svg>
</template>

<script>
import { isExternal } from '@/utils/validate'

export default {
  name: 'SvgIcon',
  props: {
    iconClass: {
      type: String,
      required: true
    },
    className: {
      type: String,
      default: ''
    }
  },
  computed: {
    isExternal() {
      return isExternal(this.iconClass)
    },
    iconName() {
      return `#icon-${this.iconClass}`
    },
    svgClass() {
      if (this.className) {
        return 'svg-icon ' + this.className
      } else {
        return 'svg-icon'
      }
    },
    styleExternalIcon() {
      return {
        mask: `url(${this.iconClass}) no-repeat 50% 50%`,
        '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
      }
    }
  }
}
</script>

<style scoped>
.svg-icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}

.svg-external-icon {
  background-color: currentColor;
  mask-size: cover !important;
  display: inline-block;
}
</style>

代码并不复杂,可以看到,它将图标分为了内部和外部两种,具体如下:

  • 它接收两个字符类型的属性,iconClassclassName
  • 通过 iconClass,它会计算出三个计算属性:
    • isExternal:用于判断是否为外部图标
    • iconName:用于指定 usexlink:href 属性
    • styleExternalIcon:设定外部图标的样式
  • 通过 className 则会计算出第四个计算属性:
    • svgClass:显然这是图标类名

💡:这里值得注意的一点是 v-on="$listeners" 将事件进行透传,这在设计通用组件的时候经常会使用到,另外透传 props 的方式是 v-on="$attrs"

整体原理并不算特别复杂,就是利用 Vue.component 注册全局组件和 require.context 加载文件,接下来我们就简单尝试一下将其改为 vue3 版本吧。

Vue3 版本

组件注册

通过文档我们可以知道,vue3 中要注册全局组件需要通过 createApp 创建的实例进行注册,所以这里直接在 main 中引入 SvgIcon 即可:

import { createApp } from "vue";
import App from "./App.vue";
import SvgIcon from "@/components/SvgIcon/index.vue";
import "./icons";

export const app = createApp(App);

app
  .component("svg-icon", SvgIcon)
  .mount("#app");

图标组件

这里其实变化不大,将计算属性改一下即可:

<template>
  <div
    v-if="isExternal"
    :style="styleExternalIcon"
    class="svg-external-icon svg-icon"
    v-on="$attrs"
  />
  <svg v-else :class="svgClass" aria-hidden="true">
    <use :xlink:href="iconName" v-on="$attrs" />
  </svg>
</template>

<script lang="ts">
import { computed, defineComponent, PropType } from "vue";
import { isExternal as isExternalUtil } from "@/utils/validate";

export default defineComponent({
  name: "index",
  props: {
    iconClass: {
      type: String as PropType<string>,
      required: true
    },
    className: {
      type: String as PropType<string>
    }
  },
  setup(props) {
    const iconName = computed(() => `#icon-${props.iconClass}`);
    const isExternal = computed(() => isExternalUtil(props.iconClass));
    const styleExternalIcon = computed(() => {
      return {
        mask: `url(${props.iconClass}) no-repeat 50% 50%`,
        "-webkit-mask": `url(${props.iconClass}) no-repeat 50% 50%`
      };
    });
    const svgClass = computed(() => {
      if (props.className) {
        return "svg-icon " + props.className;
      } else {
        return "svg-icon";
      }
    });
    return { isExternal, styleExternalIcon, iconName, svgClass };
  }
});
</script>

<style scoped lang="scss">
.svg-icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}

.svg-external-icon {
  background-color: currentColor;
  mask-size: cover !important;
  display: inline-block;
}
</style>

💡:仔细的读者可能已经发现了,在 vue3 中,透传事件也写的是 $attrs,具体可以参考文档:移除 $listeners

最后的问题

一切都按部就班完成之后运行项目会发现还是无法使用图标组件,这是为什么呢?

究其原因是因为没有 svg 的 loader,具体可以参考官方文档

这里我们也可以直接把 vue-element-admin 中的配置拷贝过来:

chainWebpack(config) {
    // it can improve the speed of the first screen, it is recommended to turn on preload
    config.plugin("preload").tap(() => [
      {
        rel: "preload",
        // to ignore runtime.js
        // https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171
        fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/],
        include: "initial"
      }
    ]);

    // when there are many pages, it will cause too many meaningless requests
    config.plugins.delete("prefetch");

    // set svg-sprite-loader
    config.module
      .rule("svg")
      .exclude.add(resolve("src/icons"))
      .end();
    config.module
      .rule("icons")
      .test(/\.svg$/)
      .include.add(resolve("src/icons"))
      .end()
      .use("svg-sprite-loader")
      .loader("svg-sprite-loader")
      .options({
        symbolId: "icon-[name]"
      })
      .end();

    config.when(process.env.NODE_ENV !== "development", config => {
      config
        .plugin("ScriptExtHtmlWebpackPlugin")
        .after("html")
        .use("script-ext-html-webpack-plugin", [
          {
            // `runtime` must same as runtimeChunk name. default is `runtime`
            inline: /runtime\..*\.js$/
          }
        ])
        .end();
      config.optimization.splitChunks({
        chunks: "all",
        cacheGroups: {
          libs: {
            name: "chunk-libs",
            test: /[\\/]node_modules[\\/]/,
            priority: 10,
            chunks: "initial" // only package third parties that are initially dependent
          },
          elementUI: {
            name: "chunk-elementUI", // split elementUI into a single package
            priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
            test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
          },
          commons: {
            name: "chunk-commons",
            test: resolve("src/components"), // can customize your rules
            minChunks: 3, //  minimum common number
            priority: 5,
            reuseExistingChunk: true
          }
        }
      });
      // https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk
      config.optimization.runtimeChunk("single");
    });
  }
```<p style="line-height: 20px; color: #ccc">
        版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
        作者: 初心Yearth
        原文链接:<a href='https://juejin.im/post/6919778722719465479'>https://juejin.im/post/6919778722719465479</a>
      </p>
回到顶部