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
Vite+ Vue3 +Typescript+ ESlint
npm create vue@3
确定项目文件结构
从简单入手, 没有必要过度设计
- components
- Button
- Button.vue -组件
- style.css -样式
- types.ts--- 一些辅助的typescript类型
- Button.test.tsx -测试文件
动态Class绑定:
遇到的问题:
- BetterDefine, 可以从外部文件导入类型
- https://vue-mac「os.sxzz.moe/features/better-define.html
- defineOptions, 可以在setup中定义一些组件属性
- vue-macros.sxzz.moe/macros/defi…
- button的原生腾性
- developer.mozilla.org/en-US/docs/… defineExpose
- 显式的指定要暴露出去的属性
- cn.vuejs.org/api/sfc-scr…
组件实现
//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>
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 />