Skip to content

组件的编写

这个想必大家都已经非常熟悉了,这里我们只以一个 Button 组件为例,来介绍如何编写一个组件,然后再在后续「迭代增强」中不断添加新功能。

一、需求拆解

一般说来写组件的目标肯定是要满足一些指定的需求,所以我们要对需求进行一个拆解。

  1. 基础功能

    • 支持不同状态(正常、悬浮(hover)、按下(focus/active)、禁用(disable))。
    • 可通过 props 控制样式变体(primary / secondary / danger 等)。
    • 支持不同尺寸 size(小sm 、中md、大lg)。
    • 支持点击事件。
  2. 可插槽(slot)

    • 默认 slot 渲染按钮文本或自定义内容。
  3. 扩展性

    • 支持图标(icon)和加载态(loading)。
    • 支持加载中禁用。
    • 增加动画(比如过渡、点击水波纹效果(antd就做了这个效果))。
  4. 主题化 & 样式隔离

    • 使用 CSS 变量或预处理器便于全局定制。
    • scoped 样式避免冲突。
  5. (进阶)可访问性(a11y)

    • role="button"aria-pressedaria-disabled 等属性。
    • 键盘可聚焦与回车/空格触发。

二、初始版本:BaseButton.vue

vue
<script setup lang="ts">
import { computed } from 'vue'

export interface Props {
  label?: string
  variant?: 'primary' | 'secondary' | 'danger' | 'warning' | 'default'
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
  loading?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  disabled: false,
  loading: false,
  label: '',
  size: 'md',
  variant: 'default',
})

const emit = defineEmits<{
  (e: 'click', event: MouseEvent): void
}>()

const sizes = {
  sm: 'px-2 py-1 text-sm',
  md: 'px-4 py-2 text-base',
  lg: 'px-6 py-3 text-lg',
}

const variants = {
  primary: 'bg-blue-600 text-white hover:bg-blue-700',
  secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
  danger: 'bg-red-600 text-white hover:bg-red-700',
  warning: 'bg-yellow-600 text-white hover:bg-yellow-700',
  default: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
}

const buttonClasses = computed(() => [
  'inline-flex items-center justify-center font-medium rounded transition duration-150',
  sizes[props.size],
  variants[props.variant],
  (props.disabled || props.loading) && 'opacity-50 cursor-not-allowed',
])

function handleClick(e: MouseEvent) {
  if (props.disabled || props.loading) {
    return
  }
  emit('click', e)
}
</script>

<template>
  <button
    :class="buttonClasses" :disabled="disabled || loading" role="button" :aria-disabled="disabled || loading"
    @click="handleClick"
  >
    <slot>{{ label }}</slot>
  </button>
</template>

<style scoped>
button:focus {
  outline: none;
  box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.6);
}
</style>

说明

  • 使用 <script setup> + TypeScript。
  • 通过 computed 动态拼接类名,利用 Tailwind CSS 实现快速样式。
  • 已考虑禁用与加载态。

三、迭代增强

接下来,我们分几轮不断扩展:

  1. 图标 & 加载指示
  2. 多主题 & 配色
  3. 波纹 / 动画效果
  4. 更多可定制接口

1. 添加 Icon 与 Loading

css
/* loading 动画 */
.spinner {
  border: 2px solid rgba(255,255,255,0.3);
  border-top-color: white;
  border-radius: 50%;
  width: 1em; height: 1em;
  animation: spin 0.8s linear infinite;
  display: inline-block;
}
@keyframes spin { to { transform: rotate(360deg); } }

2. 支持多主题与全局定制

  • 在全局引入一组 CSS 变量(例如 --btn-bg-primary--btn-text-primary)。
  • 组件切换为引用变量,用户可在根节点或主题包里覆盖。
css
:root {
  --btn-bg-primary: #3b82f6;
  --btn-text-primary: #fff;
  /* … */
}
vue
<!-- Button.vue 中 -->
:style="{
  '--bg': `var(--btn-bg-${variant})`,
  '--text': `var(--btn-text-${variant})`
}"
class="bg-[var(--bg)] text-[var(--text)]"

3. 动画与波纹效果

利用 CSS 或简单 JS 在点击时添加一个水波纹动画:

css
.btn-ripple {
  position: relative;
  overflow: hidden;
}
.btn-ripple .ripple {
  position: absolute;
  border-radius: 50%;
  transform: scale(0);
  animation: ripple 0.6s linear;
  background: rgba(255,255,255,0.7);
}
@keyframes ripple {
  to { transform: scale(4); opacity: 0; }
}

在点击时往按钮元素中插入 .ripple 元素并自动清理。

4. 更多可定制接口

  • as 属性:支持渲染为不同标签(<a><router-link>)。
  • loadingText:加载时显示不同文案。
  • block:一行占满宽度。
  • rounded:不同圆角等级。

下一步

在完成组件开发之后,我们就要进入下一步,组件的构建

Released under the CC BY-NC-SA 4.0 License.