上节课我们成功搭建了公共函数库,这节课我们来搭建公共组件库。
前期准备
使用Vue CLI来搭建项目,并在搭建过程中勾选单元测试选项。因为我们要搭建的是公共组件库,这些组件将在多个项目中被广泛使用,所以进行单元测试是必不可少的。
项目搭建完成后,我们发现node_modules
里的依赖还是采用传统的 npm 平铺方式,将所有直接依赖和间接依赖都放在了node_modules
中。为了使用 pnpm 的依赖管理方式,我们需要删除原有的node_modules
和package.lock.json
文件,然后通过pnpm i
重新安装依赖。
重新安装依赖后,serve
运行项目、build
和lint
都能正常工作,但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 书写样式代码,但项目中没有sass
和sass-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("