上节课我们成功搭建了公共函数库,这节课我们来搭建公共组件库。

    前期准备

    使用Vue CLI来搭建项目,并在搭建过程中勾选单元测试选项。因为我们要搭建的是公共组件库,这些组件将在多个项目中被广泛使用,所以进行单元测试是必不可少的。

    项目搭建完成后,我们发现node_modules里的依赖还是采用传统的 npm 平铺方式,将所有直接依赖和间接依赖都放在了node_modules中。为了使用 pnpm 的依赖管理方式,我们需要删除原有的node_modulespackage.lock.json文件,然后通过pnpm i重新安装依赖。

    重新安装依赖后,serve运行项目、buildlint都能正常工作,但test:unit却无法运行。原因在于jest-environment-jsdom这个依赖的版本问题。工作空间中安装的版本是 29.5.0,而项目需要的是 27.5.1。解决方法很简单,只需在当前组件库项目中安装 27.5.1 版本的jest-environment-jsdom

    1pnpm add jest-environment-jsdom@27.5.1 -D

    封装组件

    以按钮组件为例,首先在components目录下新建一个Button.vue文件,组件代码如下:

    1<template>
    2  <button>
    3    <slot></slot>
    4  </button>
    5</template>
    6
    7<script lang="ts">
    8import { defineComponent } from "vue";
    9export default defineComponent({
    10  name: "duyi-button"
    11});
    12</script>
    13
    14<style scoped>
    15/* 样式代码 */
    16</style>

    接下来在main.ts中全局注册该组件,以便项目中任何组件都可以直接使用:

    1// main.ts
    2import { createApp } from 'vue';
    3import App from './App.vue';
    4
    5const app = createApp(App);
    6
    7// 引入组件
    8import Button from "./components/Button.vue";
    9
    10// 注册组件
    11app.component(Button.name, Button);
    12
    13app.mount('#app');

    按钮组件参数与事件

    模拟 Element UI 的按钮组件进行封装,该按钮有以下参数和事件:

    参数支持

    参数名 参数描述 参数类型 默认值 type 按钮类型(primary/success/warning/danger/info) string default plain 是否是朴素按钮 boolean false round 是否是圆角按钮 boolean false circle 是否是圆形按钮 boolean false disabled 是否禁用按钮 boolean false icon 图标类名 string 无

    事件支持

    事件名 事件描述 click 点击事件

    使用插槽自定义内容

    凡是希望组件中内容可以灵活设置的地方,都需要用到slot插槽来自定义内容。所以我们使用slot来定义按钮上的文本内容:

    1<template>
    2  <button>
    3    <slot></slot>
    4  </button>
    5</template>

    添加样式

    为按钮添加一定的样式,使用 SCSS 书写样式代码,但项目中没有sasssass-loader的依赖,因此需要安装这两个依赖:

    1pnpm i sass sass-loader -D -w

    实现 type 属性

    主要是对应样式的书写,为不同类型的按钮添加不同的样式类:

    1.duyi-button-primary {
    2  /* 样式代码 */
    3}
    4
    5.duyi-button-success {
    6  /* 样式代码 */
    7}
    8
    9/* 其他类型样式 */

    在组件中接收type属性,并为当前的 button 动态拼接样式的类名:

    1<template>
    2  <button class="duyi-button" :class="[
    3      `duyi-button-${type}`
    4  ]">
    5    <slot></slot>
    6  </button>
    7</template>
    8
    9<script lang="ts">
    10import { defineComponent } from "vue";
    11export default defineComponent({
    12  name: "duyi-button",
    13  props: {
    14    type: {
    15      type: String,
    16      default: "default"
    17    }
    18  }
    19});
    20</script>

    实现其他属性

    plain 属性

    在组件内部接收plain属性,并设置到 button 上:

    1<template>
    2  <button class="duyi-button" :class="[
    3      `duyi-button-${type}`,
    4      {
    5          'is-plain': plain
    6      }
    7  ]">
    8    <slot></slot>
    9  </button>
    10</template>
    11
    12<script lang="ts">
    13import { defineComponent } from "vue";
    14export default defineComponent({
    15  name: "duyi-button",
    16  props: {
    17    type: {
    18      type: String,
    19      default: "default"
    20    },
    21    plain: {
    22      type: Boolean,
    23      default: false
    24    }
    25  }
    26});
    27</script>

    round、circle、disabled 属性

    做法与plain属性类似,在组件中接收这些属性,并动态添加对应的样式类:

    1<template>
    2  <button class="duyi-button" :class="[
    3      `duyi-button-${type}`,
    4      {
    5          'is-plain': plain,
    6          'is-round': round,
    7          'is-circle': circle,
    8          'is-disabled': disabled,
    9      }
    10  ]" :disabled="disabled">
    11    <slot></slot>
    12  </button>
    13</template>
    14
    15<script lang="ts">
    16import { defineComponent } from "vue";
    17export default defineComponent({
    18  name: "duyi-button",
    19  props: {
    20    type: {
    21      type: String,
    22      default: "default"
    23    },
    24    plain: {
    25      type: Boolean,
    26      default: false
    27    },
    28    round: {
    29      type: Boolean,
    30      default: false,
    31    },
    32    circle: {
    33      type: Boolean,
    34      default: false,
    35    },
    36    disabled: {
    37      type: Boolean,
    38      default: false,
    39    },
    40  }
    41});
    42</script>

    图标支持

    首先将fonts目录放入assets目录下,并在main.ts中引入图标相关的样式:

    1import "./assets/fonts/font.scss";

    在组件内部接收icon属性,并使用i标签来显示图标:

    1<template>
    2  <button class="duyi-button" :class="[
    3      `duyi-button-${type}`,
    4      {
    5          'is-plain': plain,
    6          'is-round': round,
    7          'is-circle': circle,
    8          'is-disabled': disabled,
    9      }
    10  ]" :disabled="disabled">
    11    <i v-if="icon" :class="`duyi-icon-${icon}`"></i>
    12    <span v-if="$slots.default">
    13      <slot></slot>
    14    </span>
    15  </button>
    16</template>
    17
    18<script lang="ts">
    19import { defineComponent } from "vue";
    20export default defineComponent({
    21  name: "duyi-button",
    22  props: {
    23    type: {
    24      type: String,
    25      default: "default"
    26    },
    27    plain: {
    28      type: Boolean,
    29      default: false
    30    },
    31    round: {
    32      type: Boolean,
    33      default: false,
    34    },
    35    circle: {
    36      type: Boolean,
    37      default: false,
    38    },
    39    disabled: {
    40      type: Boolean,
    41      default: false,
    42    },
    43    icon: {
    44      type: String,
    45      default: ""
    46    }
    47  }
    48});
    49</script>

    点击事件

    最后,为按钮添加点击事件,触发父组件传递过来的click事件:

    1methods: {
    2  btnClick() {
    3    this.$emit("click");
    4  }
    5}

    测试组件

    接下来对封装的按钮组件进行测试,测试代码如下:

    1import { mount } from "@vue/test-utils";
    2import Button from "@/components/Button.vue";
    3
    4describe("Button.vue", () => {
    5  it("renders button with default type", () => {
    6    const wrapper = mount(Button);
    7    expect(wrapper.classes()).toContain("duyi-button");
    8    expect(wrapper.classes()).toContain("duyi-button-default");
    9  });
    10
    11  it("renders button with correct type", () => {
    12    const wrapper = mount(Button, { props: { type: "primary" } });
    13    expect(wrapper.classes()).toContain("duyi-button");
    14    expect(wrapper.classes()).toContain("duyi-button-primary");
    15  });
    16
    17  it("renders button with plain style", () => {
    18    const wrapper = mount(Button, { props: { plain: true } });
    19    expect(wrapper.classes()).toContain("is-plain");
    20  });
    21
    22  it("renders button with round style", () => {
    23    const wrapper = mount(Button, { props: { round: true } });
    24    expect(wrapper.classes()).toContain("is-round");
    25  });
    26
    27  it("renders button with circle style", () => {
    28    const wrapper = mount(Button, { props: { circle: true } });
    29    expect(wrapper.classes()).toContain("is-circle");
    30  });
    31
    32  it("renders button with disabled state", () => {
    33    const wrapper = mount(Button, { props: { disabled: true } });
    34    expect(wrapper.classes()).toContain("is-disabled");
    35    expect(wrapper.attributes()).toHaveProperty("