从0搭建Vue3组件库:Button组件

651 阅读6分钟

Button按钮组件

button组件几乎是每个组件库都有的;其实实现一个button组件是很简单的。本篇文章将带你一步一步的实现一个button组件。(样式部分参造了element plus 组件库)

任务

  • 需求分析
  • 初始化项目
  • 确定项目文件结构
  • 规范基础写法
  • 样式解决方案以及色彩系统

需求分析

Button组件大部分关注样式,没有交互。那么这时就可以通过给组件添加不同的属性进而动态的添加class以实现不同样式的按钮:

根据分析可以得到具体的属性列表:

  • type: 不同的样式(Primary,Danger, Info, Success, Warning)
  • plain: 样式的不同展现模式boolean
  • round: 圆角boolean
  • circle: 圆形按钮,适合匿标boolean -disabled: 禁用boolean
  • 图标:后面再添加
  • loading: 后面再添加
//types.ts

import type { PropType } from "vue";

// 定义按钮类型
export type ButtonType =
  | "primary"
  | "success"
  | "warning"
  | "danger"
  | "info"
  | "default";

// 定义按钮尺寸
export type ButtonSize = "large" | "default" | "small";

// 定义按钮原生类型
export type ButtonNativeType = "button" | "submit" | "reset";

// 定义组件属性
export interface ButtonProps {
  /**
   * 按钮类型
   */
  type?: ButtonType;
  /**
   * 按钮尺寸
   */
  size?: ButtonSize;
  /**
   * 朴素类型
   */
  plain?: boolean;
  /**
   * 圆角型
   */
  round?: boolean;
  /**
   * 圆形
   */
  circle?: boolean;
  /**
   * 禁用状态
   */
  disabled?: boolean;
  /**
   * 是否加载中
   */
  loading?: boolean;
  /**
   * 按钮点击事件
   */
  onClick?: () => void;
  /**
   * button原生属性
   */
  nativeType?: ButtonNativeType;
  /**
   * 是否自动聚焦
   */
  autofocus?: boolean;
  /**
   * 图标
   */
  icon?: string;
}

// 定义组件实例
export interface ButtonInstance {
  ref: HTMLButtonElement;
}

Button组件的本质

//就是 class 名称的组合
class="jd-button jd-button--primary jd-button--large is-plain is-round is-disabled" 

初始化项目

另外vue官方基千vite的封装工具-create-vue

github.com/vuejs/creat…

Vite+ Vue3 +Typescript+ ESlint

npm create vue@3 

确定项目文件结构

从简单入手, 没有必要过度设计

  • components
  • Button
  • Button.vue -组件
  • style.css -样式
  • types.ts--- 一些辅助的typescript类型
  • Button.test.tsx -测试文件

image.png

动态Class绑定:

cn.vuejs.org/guide/essen…

遇到的问题:

组件实现

//Button.vue

<template>
  <button
    ref="_ref"
    class="jd-button"
    :class="{
      //动态样式
      [`jd-button--${type}`]: type,
      [`jd-button--${size}`]: size,
      'is-plain': plain,
      'is-round': round,
      'is-circle': circle,
      'is-disabled': disabled,
      'is-loading': loading,
    }"
    :disabled="disabled || loading"
    :autofocus="autofocus"
    :type="nativeType"
  >
    <Icon v-if="loading" icon="spinner" spin />
    <Icon v-if="icon" :icon="icon" />
    <span>
      <slot />
    </span>
  </button>
</template>

<script setup lang="ts">
import { ref } from "vue";
import type { ButtonProps } from "./types";
import Icon from "../Icon/Icon.vue";
// 定义组件属性
// defineProps<ButtonProps>();
//为组件属性设置默认值
withDefaults(defineProps<ButtonProps>(), {
  // type: "default",
  nativeType: "button",
});
// 组件名称,用于调试或作为全局组件名
defineOptions({
  name: "JdButton",
});

// 定义组件实例,指向按钮的 DOM 元素
const _ref = ref<HTMLButtonElement>();
// 暴露组件实例,方便父组件通过 $ref 调用该组件时可以直接操作按钮实例。
defineExpose({
  ref: _ref,
});
</script>

可以通过 defineExpose 编译器宏来显式指定在 <script setup> 组件中要暴露出去的属性:详情见Vue3官方文档 defineExpose,我们可以使用组件暴露的属性,当你需要的时候:

<script>
import type { ButtonInstance } from './components/Button/types'
const buttonRef = ref<ButtonInstance | null>(null)

const testRef = () => {
 if (buttonRef.value) {
    console.log('buttonRef', buttonRef.value.ref)
  } 
} 
</script>

<template>
  <Button ref="buttonRef" @click="testRef">Test Button</Button>
</template>

image.png

type实现

我们的type可以传入的值可以是primary, success, info,warning, danger分别展示不同按钮颜色,type传入text显示文字按钮(没有边框和背景色的按钮)

这里只展示了一个primary的样式,因为其它值的样式实现是一样的。

所以在button/types.ts文件中我们定义一下type的类型:

// 定义按钮类型
export type ButtonType =
  | "primary"
  | "success"
  | "warning"
  | "danger"
  | "info"
  | "default";
  
  
 export interface ButtonProps {
  /**
   * 按钮类型
   */
  type?: ButtonType;
  }

接下来在Button.vue中实现传入不同值赋予不同类名,从而实现显示不同效果。

<template>
  <button
    ref="_ref"
    class="jd-button"
    :class="{
      //动态样式
      [`jd-button--${type}`]: type,
    }"
  >
    <span>
      <slot />
    </span>
  </button>
</template>

这样一来传入primary组件就会有个类名jd-button--primary吗,传入其它值也一样。然后我们就可以给它们写样式了。进入style/index.css:

.jd-button {
  display: inline-flex;
  justify-content: center;
  align-items: center;
  line-height: 1;
  height: 32px;
  white-space: nowrap;
  cursor: pointer;
  color: var(--jd-button-text-color);
  text-align: center;
  box-sizing: border-box;
  outline: none;
  transition: 0.1s;
  font-weight: var(--jd-button-font-weight);
  user-select: none;
  vertical-align: middle;
  -webkit-appearance: none;
  background-color: var(--jd-button-bg-color);
  border: var(--jd-border);
  border-color: var(--jd-button-border-color);
  padding: 8px 15px;
  font-size: var(--jd-font-size-base);
  border-radius: var(--jd-border-radius-base);
    & + & {
    margin-left: 12px;
  }
  &:hover,
  &:focus {
    color: var(--jd-button-hover-text-color);
    border-color: var(--jd-button-hover-border-color);
    background-color: var(--jd-button-hover-bg-color);
    outline: none;
  }
  &:active {
    color: var(--jd-button-active-text-color);
    border-color: var(--jd-button-active-border-color);
    background-color: var(--jd-button-active-bg-color);
    outline: none;
  }
  
  }
  
@each $val in primary, success, warning, info, danger {
  .jd-button--$(val) {
    --jd-button-text-color: var(--jd-color-white);
    --jd-button-bg-color: var(--jd-color-$(val));
    --jd-button-border-color: var(--jd-color-$(val));
    --jd-button-outline-color: var(--jd-color-$(val)-light-5);
    --jd-button-active-color: var(--jd-color-$(val)-dark-2);
    --jd-button-hover-text-color: var(--jd-color-white);
    --jd-button-hover-bg-color: var(--jd-color-$(val)-light-3);
    --jd-button-hover-border-color: var(--jd-color-$(val)-light-3);
    --jd-button-active-bg-color: var(--jd-color-$(val)-dark-2);
    --jd-button-active-border-color: var(--jd-color-$(val)-dark-2);
    --jd-button-disabled-text-color: var(--jd-color-white);
    --jd-button-disabled-bg-color: var(--jd-color-$(val)-light-5);
    --jd-button-disabled-border-color: var(--jd-color-$(val)-light-5);
  }

plain(朴素按钮)和round(圆角按钮)

我们可以通过传入plain和round来决定这个按钮是否为朴素按钮和圆角按钮,很显然它们是个布尔类型

// 定义组件属性
export interface ButtonProps {
  /**
   * 按钮类型
   */
  type?: ButtonType;
  /**
   * 按钮尺寸
   */
  size?: ButtonSize;
  /**
   * 朴素类型
   */
  plain?: boolean;
  /**
   * 圆角型
   */
  round?: boolean;
  /**
   * 圆形
   */
  circle?: boolean;

然后在Button.vue定义我们的styleClass

<template>
  <button
    ref="_ref"
    class="jd-button"
    :class="{
      //动态样式
      [`jd-button--${type}`]: type,
      [`jd-button--${size}`]: size,
      'is-plain': plain,
      'is-round': round,
      'is-circle': circle,
      'is-disabled': disabled,
      'is-loading': loading,
    }"
  >
    <span>
      <slot />
    </span>
  </button>
</template>

最后写上我们的样式

  &.is-plain {
    --jd-button-hover-text-color: var(--jd-color-primary);
    --jd-button-hover-bg-color: var(--jd-fill-color-blank);
    --jd-button-hover-border-color: var(--jd-color-primary);
  }
  /*round*/
  &.is-round {
    border-radius: var(--jd-border-radius-round);
  }
  /*circle*/
  &.is-circle {
    border-radius: 50%;
    padding: 8px;
  }

禁用按钮

同样的,在types.ts定义disabled的类型

// 定义按钮类型
export type ButtonType =
  | "primary"
  | "success"
  | "warning"
  | "danger"
  | "info"
  | "default";
  
// 定义组件属性
export interface ButtonProps {
  /**
   * 按钮类型
   */
  type?: ButtonType;
  /**
   * 朴素类型
   */
  plain?: boolean;
  /**
   * 圆角型
   */
  round?: boolean;
  /**
   * 圆形
   */
  circle?: boolean;
  /**
   * 禁用状态
   */
  disabled?: boolean;

然后定义我们的styleClass

<template>
  <button
    ref="_ref"
    class="jd-button"
    :class="{
      //动态样式
      [`jd-button--${type}`]: type,
      [`jd-button--${size}`]: size,
      'is-plain': plain,
      'is-round': round,
      'is-circle': circle,
      'is-disabled': disabled,
      'is-loading': loading,
    }"
    :disabled="disabled || loading"
  >
    <span>
      <slot />
    </span>
  </button>
</template>

最后添加上我们的样式

  /*disabled*/
  &.is-disabled,
  &.is-disabled:hover,
  &.is-disabled:focus,
  &[disabled],
  &[disabled]:hover,
  &[disabled]:focus {
    color: var(--jd-button-disabled-text-color);
    cursor: not-allowed;
    background-image: none;
    background-color: var(--jd-button-disabled-bg-color);
    border-color: var(--jd-button-disabled-border-color);
  }
  
  @each $val in primary, success, warning, info, danger {
  .jd-button--$(val) {
    --jd-button-text-color: var(--jd-color-white);
    --jd-button-bg-color: var(--jd-color-$(val));
    --jd-button-border-color: var(--jd-color-$(val));
    --jd-button-outline-color: var(--jd-color-$(val)-light-5);
    --jd-button-active-color: var(--jd-color-$(val)-dark-2);
    --jd-button-hover-text-color: var(--jd-color-white);
    --jd-button-hover-bg-color: var(--jd-color-$(val)-light-3);
    --jd-button-hover-border-color: var(--jd-color-$(val)-light-3);
    --jd-button-active-bg-color: var(--jd-color-$(val)-dark-2);
    --jd-button-active-border-color: var(--jd-color-$(val)-dark-2);
    --jd-button-disabled-text-color: var(--jd-color-white);
    --jd-button-disabled-bg-color: var(--jd-color-$(val)-light-5);
    --jd-button-disabled-border-color: var(--jd-color-$(val)-light-5);
  }

size

通过size我们可以控制按钮的大小,组件接收的size值有:large, small, default。实现方式和上面差不多,这里就直接展示部分代码了

// 定义按钮类型
export type ButtonType =
  | "primary"
  | "success"
  | "warning"
  | "danger"
  | "info"
  | "default";

// 定义按钮尺寸
export type ButtonSize = "large" | "default" | "small";

// 定义按钮原生类型
export type ButtonNativeType = "button" | "submit" | "reset";

// 定义组件属性
export interface ButtonProps {
  /**
   * 按钮类型
   */
  type?: ButtonType;
  /**
   * 按钮尺寸
   */
  size?: ButtonSize;
  /**
   * 朴素类型
   */
  plain?: boolean;
  /**
   * 圆角型
   */
  round?: boolean;
  /**
   * 圆形
   */
  circle?: boolean;
  /**
   * 禁用状态
   */
  disabled?: boolean;
  }
  • button.vue
<template>
  <button
    ref="_ref"
    class="jd-button"
    :class="{
      //动态样式
      [`jd-button--${type}`]: type,
      [`jd-button--${size}`]: size,
      'is-plain': plain,
      'is-round': round,
      'is-circle': circle,
      'is-disabled': disabled,
      'is-loading': loading,
    }"
    :disabled="disabled || loading"
    :autofocus="autofocus"
    :type="nativeType"
  >
    <Icon v-if="loading" icon="spinner" spin />
    <Icon v-if="icon" :icon="icon" />
    <span>
      <slot />
    </span>
  </button>
</template>

<script setup lang="ts">
import { ref } from "vue";
import type { ButtonProps } from "./types";
import Icon from "../Icon/Icon.vue";
// 定义组件属性
// defineProps<ButtonProps>();
//为组件属性设置默认值
withDefaults(defineProps<ButtonProps>(), {
  // type: "default",
  nativeType: "button",
});
// 组件名称,用于调试或作为全局组件名
defineOptions({
  name: "JdButton",
});

// 定义组件实例,指向按钮的 DOM 元素
const _ref = ref<HTMLButtonElement>();
// 暴露组件实例,方便父组件通过 $ref 调用该组件时可以直接操作按钮实例。
defineExpose({
  ref: _ref,
});
</script>

<style></style>

  • style.css
.jd-button {
  display: inline-flex;
  justify-content: center;
  align-items: center;
  line-height: 1;
  height: 32px;
  white-space: nowrap;
  cursor: pointer;
  color: var(--jd-button-text-color);
  text-align: center;
  box-sizing: border-box;
  outline: none;
  transition: 0.1s;
  font-weight: var(--jd-button-font-weight);
  user-select: none;
  vertical-align: middle;
  -webkit-appearance: none;
  background-color: var(--jd-button-bg-color);
  border: var(--jd-border);
  border-color: var(--jd-button-border-color);
  padding: 8px 15px;
  font-size: var(--jd-font-size-base);
  border-radius: var(--jd-border-radius-base);
  }
.jd-button--large {
  --jd-button-size: 40px;
  height: var(--jd-button-size);
  padding: 12px 19px;
  font-size: var(--jd-font-size-base);
  border-radius: var(--jd-border-radius-base);
}
.jd-button--small {
  --jd-button-size: 24px;
  height: var(--jd-button-size);
  padding: 5px 11px;
  font-size: 12px;
  border-radius: calc(var(--jd-border-radius-base) - 1px);
}

关于组件样式

组件样式比较多,本组件库大部分样式借鉴于Element Plus,详见 Element Plus官方

我们的type可以传入的值可以是primary, success, info,warning, danger分别展示不同按钮颜色,type传入text显示文字按钮(没有边框和背景色的按钮)


.jd-button {
  --jd-button-font-weight: var(--jd-font-weight-primary);
  --jd-button-border-color: var(--jd-border-color);
  --jd-button-bg-color: var(--jd-fill-color-blank);
  --jd-button-text-color: var(--jd-text-color-regular);
  --jd-button-disabled-text-color: var(--jd-disabled-text-color);
  --jd-button-disabled-bg-color: var(--jd-fill-color-blank);
  --jd-button-disabled-border-color: var(--jd-border-color-light);
  --jd-button-hover-text-color: var(--jd-color-primary);
  --jd-button-hover-bg-color: var(--jd-color-primary-light-9);
  --jd-button-hover-border-color: var(--jd-color-primary-light-7);
  --jd-button-active-text-color: var(--jd-button-hover-text-color);
  --jd-button-active-border-color: var(--jd-color-primary);
  --jd-button-active-bg-color: var(--jd-button-hover-bg-color);
  --jd-button-outline-color: var(--jd-color-primary-light-5);
  --jd-button-active-color: var(--jd-text-color-primary);
}
/* & 代表当前作用的父选择器。
当嵌套样式时,& 会被替换为外层父选择器。 */
.jd-button {
  display: inline-flex;
  justify-content: center;
  align-items: center;
  line-height: 1;
  height: 32px;
  white-space: nowrap;
  cursor: pointer;
  color: var(--jd-button-text-color);
  text-align: center;
  box-sizing: border-box;
  outline: none;
  transition: 0.1s;
  font-weight: var(--jd-button-font-weight);
  user-select: none;
  vertical-align: middle;
  -webkit-appearance: none;
  background-color: var(--jd-button-bg-color);
  border: var(--jd-border);
  border-color: var(--jd-button-border-color);
  padding: 8px 15px;
  font-size: var(--jd-font-size-base);
  border-radius: var(--jd-border-radius-base);
  & + & {
    margin-left: 12px;
  }
  &:hover,
  &:focus {
    color: var(--jd-button-hover-text-color);
    border-color: var(--jd-button-hover-border-color);
    background-color: var(--jd-button-hover-bg-color);
    outline: none;
  }
  &:active {
    color: var(--jd-button-active-text-color);
    border-color: var(--jd-button-active-border-color);
    background-color: var(--jd-button-active-bg-color);
    outline: none;
  }
  &.is-plain {
    --jd-button-hover-text-color: var(--jd-color-primary);
    --jd-button-hover-bg-color: var(--jd-fill-color-blank);
    --jd-button-hover-border-color: var(--jd-color-primary);
  }
  /*round*/
  &.is-round {
    border-radius: var(--jd-border-radius-round);
  }
  /*circle*/
  &.is-circle {
    border-radius: 50%;
    padding: 8px;
  }
  /*disabled*/
  &.is-disabled,
  &.is-disabled:hover,
  &.is-disabled:focus,
  &[disabled],
  &[disabled]:hover,
  &[disabled]:focus {
    color: var(--jd-button-disabled-text-color);
    cursor: not-allowed;
    background-image: none;
    background-color: var(--jd-button-disabled-bg-color);
    border-color: var(--jd-button-disabled-border-color);
  }
  [class*="jd-icon"] + span {
    margin-left: 6px;
  }
}
@each $val in primary, success, warning, info, danger {
  .jd-button--$(val) {
    --jd-button-text-color: var(--jd-color-white);
    --jd-button-bg-color: var(--jd-color-$(val));
    --jd-button-border-color: var(--jd-color-$(val));
    --jd-button-outline-color: var(--jd-color-$(val)-light-5);
    --jd-button-active-color: var(--jd-color-$(val)-dark-2);
    --jd-button-hover-text-color: var(--jd-color-white);
    --jd-button-hover-bg-color: var(--jd-color-$(val)-light-3);
    --jd-button-hover-border-color: var(--jd-color-$(val)-light-3);
    --jd-button-active-bg-color: var(--jd-color-$(val)-dark-2);
    --jd-button-active-border-color: var(--jd-color-$(val)-dark-2);
    --jd-button-disabled-text-color: var(--jd-color-white);
    --jd-button-disabled-bg-color: var(--jd-color-$(val)-light-5);
    --jd-button-disabled-border-color: var(--jd-color-$(val)-light-5);
  }
  .jd-button--$(val).is-plain {
    --jd-button-text-color: var(--jd-color-$(val));
    --jd-button-bg-color: var(--jd-color-$(val)-light-9);
    --jd-button-border-color: var(--jd-color-$(val)-light-5);
    --jd-button-hover-text-color: var(--jd-color-white);
    --jd-button-hover-bg-color: var(--jd-color-$(val));
    --jd-button-hover-border-color: var(--jd-color-$(val));
    --jd-button-active-text-color: var(--jd-color-white);
  }
}
.jd-button--large {
  --jd-button-size: 40px;
  height: var(--jd-button-size);
  padding: 12px 19px;
  font-size: var(--jd-font-size-base);
  border-radius: var(--jd-border-radius-base);
}
.jd-button--small {
  --jd-button-size: 24px;
  height: var(--jd-button-size);
  padding: 5px 11px;
  font-size: 12px;
  border-radius: calc(var(--jd-border-radius-base) - 1px);
}

最后我们在项目中引用(examples)来查看效果;

    <Button ref="buttonRef" @click="open">Test Button</Button>
    <Button>默认按钮</Button>
    <Button type="primary">主要按钮</Button>
    <Button type="success">成功按钮</Button>
    <Button type="info">信息按钮</Button>
    <Button type="warning">警告按钮</Button>
    <Button type="danger">危险按钮</Button>
    <Button type="text">文字按钮</Button>
    <br />
    <br />
    <Button plain>朴素按钮</Button>
    <Button type="primary" plain>主要按钮</Button>
    <Button type="success" plain>成功按钮</Button>
    <Button type="info" plain>信息按钮</Button>
    <Button type="warning" plain>警告按钮</Button>
    <Button type="danger" plain>危险按钮</Button>
    <br />
    <br />
    <Button round>圆角按钮</Button>
    <Button type="primary" round>主要按钮</Button>
    <Button type="success" round>成功按钮</Button>
    <Button type="info" round>信息按钮</Button>
    <Button type="warning" round>警告按钮</Button>
    <Button type="danger" round>危险按钮</Button>
    <br />
    <br />
    <Button disabled>禁用按钮</Button>
    <Button type="primary" disabled>主要按钮</Button>
    <Button type="success" disabled>成功按钮</Button>
    <Button type="info" disabled>信息按钮</Button>
    <Button type="warning" disabled>警告按钮</Button>
    <Button type="danger" disabled>危险按钮</Button>
    <br />
    <br />
    <Button disabled>禁用按钮</Button>
    <Button type="primary" disabled plain>主要按钮</Button>
    <Button type="success" disabled plain>成功按钮</Button>
    <Button type="info" disabled plain>信息按钮</Button>
    <Button type="warning" disabled plain>警告按钮</Button>
    <Button type="danger" disabled plain>危险按钮</Button>
    <br />
    <br />
    <Button>默认按钮</Button>
    <Button size="large">大型按钮</Button>
    <Button size="small">小型按钮</Button>
    <Button size="default">默认按钮</Button>
    <Button size="large" loading>Loading</Button>
    <Button size="large" icon="user" plain>Icon</Button><br /><br />

image.png