你不知道的 Vue 的图标组件

2,333 阅读2分钟
原文链接: github.com

封装一个图标组件,可以配置某一个图标,可以设置是否旋转,可以追加字体图标。特点简单实用,灵活可配。

一个 Vue 的文件

共有 class

封装一个图标组件首先从一个 vue 文件开始,其主要的结构就是一个 i 标签。最关键的就是这个 class 名的配置。有一个共有的 w-icon 类名负责自定义字体,这个字体就是从字体图标工具中下载的包括自定义字体图标的字体,还包括一些共有的样式。

定义字体图标的 class

光有一个共有的类名可不行,不同的字体图标是通过不同的 class 名配置的,我们得需要一个 prop 来设置显示的字体图标,我们把这个 prop 设定为 type ,类型为 String 。如果不传类型的时候,我们就不显示这个 icon 。

设定是否旋转的 class

这里有一个高级功能那就是可配置的旋转动画。我们需要一个 prop 来设置是否需要旋转动画,我们把这个 prop 设定为 spin ,类型为 Boolean 。如果为 true ,那么我们就让它转起来。

设定 class 的前缀参数

w-icon 默认前缀为 w ,如果您需要扩展字体图标,或者需要自定义样式,我们为您提供了一个 prop 。专为此事而行,这个 prop 我们设定为 prefix ,类型为 String ,默认为 w

vue 组件源码奉上

<template>
  <i
    v-if="type"
    :class="[`${prefix}-font`,`${prefix}-${type}`, {
      'w-spin': !!spin,
    }]"
  ></i>
</template>

<script>
export default {
  name: 'WIcon',
  props: {
    type: String,
    spin: Boolean,
    prefix: {
      type: String,
      default: 'w',
    },
  },
  data() {
    return {};
  },
};
</script>

一个 SCSS 的文件

我们已经有一个像样的 vue 文件,那么我们还需要让 vue 的 icon 组件美起来的样式文件。这里我们选择的预处理器为 SCSS 。在这个样式文件中,我们需要做三件事 ⤵️

  • 声明字体图标的字体
  • 设置共有样式 w-font
  • 设置字体图片类型的众多样式,如 w-loading 等
  • 设置旋转动画类型 w-spin
  • 设置旋转动画

声明字体图标的字体

这里我们利用 CSS3 的自定义字体的属性来完成这项使命。需要注意的是根据不同浏览器支持的字体后缀来做好适配。

设置共有样式 w-font

声明好字体之后,就可以用了。用法和普通设置字体是一样的,名字就用之前咱们设置好的就可以了。除了设置字体之外,我们还需要做一些特殊处理。如居中等。设置 text-rendering 为 optimizeLegibility 。它使得对于某些字体(例如,Microsoft的Calibri,Candara,Constantia和Corbel或DejaVu字体系列),文本中的连字(ff,fi,fl等)小于20px。

设置字体图片类型的众多样式

这个就是根据导出的字体图标,在 before 伪元素中设置字体图标的内容即可。如果有共同特点,如 w-loading1 , w-loading2 等,可以实用 sass 中的循环来优化写法。

设置旋转动画类型 w-spin

这里使用了 CSS3 中的 动画属性 (animation) 。让它执行 loadingCircle 动画,并且无线的匀速执行下去,每次动画时间为 1 秒。

设置旋转动画

利用 @keyframes 关键字声明一个动画,名为 loadingCircle 。在最开始的时候旋转基点定为图标中心点,并旋转角度为 0 度。在最结束的时候定义旋转基点定为图标中心点,并旋转角度为 1 圈( trun )。

scss 样式源码奉上

$font-name: "iconfont";
$font-version: 1524475512080;
$font-prefix: 'w-';
$font-path: './icon/font/';

@font-face {
  font-family: $font-name;
  src: url('#{$font-path}iconfont.eot?t=#{$font-version}');
  src:
    url('#{$font-path}iconfont.eot?t=#{$font-version}#iefix') format('embedded-opentype'),
    url("#{$font-path}iconfont.woff?t=#{$font-version}") format("woff"),
    url('#{$font-path}iconfont.ttf?t=#{$font-version}') format('truetype'),
    url('#{$font-path}iconfont.svg?t=#{$font-version}#iconfont') format('svg');
}

.#{$font-prefix}font {
  font-family: $font-name !important;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  display: inline-block;
  font-style: normal;
  vertical-align: baseline;
  text-align: center;
  text-transform: none;
  line-height: 1;
  text-rendering: optimizeLegibility;
}

.#{$font-prefix}back::before { content: "\e934"; }

.#{$font-prefix}forward::before { content: "\e94d"; }

$font-list: "\e66a" "\e668" "\e669" "\e667";
// 循环 $font-list 并设置类名
// 其中 index 方法是获取索引
@each $font-item in $font-list {
  .#{$font-prefix}loading#{index($font-list, $font-item)}::before {
    content: $font-item;
  }
}

.#{$font-prefix}spin::before {
  display: inline-block;
  animation: loadingCircle 1s infinite linear;
}

@keyframes loadingCircle {
  0% {
    transform-origin: 50% 50%;
    transform: rotate(0deg);
  }

  to {
    transform-origin: 50% 50%;
    transform: rotate(1turn);
  }
}

注册 w-icon 组件

要注册一个组件,必须这个组件中拥有 install 方法。有了这个方法,在其回调中的 Vue 参数中调用 component 方法,即可。方法如下 ⤵️

import WIcon from './Icon';

WIcon.install = (Vue) => {
  Vue.component(WIcon.name, WIcon);
};

export default WIcon;

写一个图标组件的单元测试

单元测试一个图标组件,我们需要测试其属性是否真正用到了,是否与预期的一样。我们需要测试其快照的 UI 是否是我们想要的 UI 。我们不传 type 属性不显示的特殊处理是否真的生效。我们自定义前缀是否真的自定义了。

我们在一开始需要实例化组件,我们可以用 vue-test-utils 中的 mount 方法。当 DOM 更新之后, this 绑定到当前实例上之后( $nextTick ) 做一些事情。

具体测试代码 ⤵️

import { mount } from 'vue-test-utils';
import Icon from './Icon';

describe('Icon.vue', () => {
  let wrapper = null;
  let wrapperNo = null;
  let wrapperPrefix = null;
  let wrapperSpin = null;

  // 实例化
  beforeEach(() => {
    // 传入 loading1 ,然后去检测实例化之后 type 是否是 loading1 。
    wrapper = mount(Icon, {
      propsData: {
        type: 'loading1',
      },
    });
    // 不向其传任何参数,去检测针对 type 为空不显示的特殊处理是否生效。
    wrapperNo = mount(Icon);
    // 传前缀 test ,测试实例化之后的前缀是否完全改变
    wrapperPrefix = mount(Icon, {
      propsData: {
        type: 'loading1',
        prefix: 'test',
      },
    });
    // 实例化之后图标是否拥有旋转动画
    wrapperSpin = mount(Icon, {
      propsData: {
        type: 'loading1',
        spin: true,
      },
    });
  });

  it('验证 prop 值是否正确', (done) => {
    wrapper.vm.$nextTick(() => {
      try {
        expect(wrapper.props().type).toBe('loading1');
        done();
      } catch (err) {
        done.fail(err);
      }
    });
  });

  it('验证 class 值是否正确', (done) => {
    wrapper.vm.$nextTick(() => {
      try {
        expect(wrapper.is('i')).toBe(true);
        expect(wrapper.classes().toString()).toBe('w-font,w-loading1');
        done();
      } catch (err) {
        done.fail(err);
      }
    });
  });

  it('验证 prop 为空不显示', (done) => {
    wrapperNo.vm.$nextTick(() => {
      try {
        expect(wrapperNo.is('i')).toBe(false);
        done();
      } catch (err) {
        done.fail(err);
      }
    });
  });

  it('验证 自定义前缀 prefix', (done) => {
    wrapperPrefix.vm.$nextTick(() => {
      try {
        expect(wrapperPrefix.is('i')).toBe(true);
        expect(wrapperPrefix.classes().toString()).toBe('test-font,test-loading1');
        done();
      } catch (err) {
        done.fail(err);
      }
    });
  });

  it('验证 spin 字段转动', (done) => {
    wrapperSpin.vm.$nextTick(() => {
      try {
        expect(wrapperSpin.is('i')).toBe(true);
        expect(wrapperSpin.classes().toString()).toBe('w-font,w-loading1,w-spin');
        done();
      } catch (err) {
        done.fail(err);
      }
    });
  });

  it('检测快照是否一样。', (done) => {
    wrapper.vm.$nextTick(() => {
      try {
        // 检测 UI 是否改变
        expect(wrapper.text()).toMatchSnapshot();
        done();
      } catch (err) {
        done.fail(err);
      }
    });
  });
});

源码

更完整的源码,请移步 w-icon