第66天:Element Plus 开发流程与代码规范
学习目标
- 了解 Element Plus 的开发流程和贡献指南
- 掌握项目的代码规范和最佳实践
- 学习如何设置开发环境和调试工具
- 理解组件开发的标准流程
知识点概览
1. Element Plus 项目结构
1.1 项目架构概览
element-plus/
├── packages/ # 核心包目录
│ ├── components/ # 组件源码
│ │ ├── button/ # 按钮组件
│ │ ├── input/ # 输入框组件
│ │ └── ...
│ ├── directives/ # 指令
│ ├── hooks/ # 组合式函数
│ ├── locale/ # 国际化
│ ├── theme-chalk/ # 样式主题
│ ├── tokens/ # 设计令牌
│ └── utils/ # 工具函数
├── docs/ # 文档源码
├── play/ # 开发调试环境
├── scripts/ # 构建脚本
├── typings/ # 类型定义
├── .github/ # GitHub 配置
├── .vscode/ # VS Code 配置
├── package.json
├── pnpm-workspace.yaml # pnpm 工作空间配置
├── tsconfig.json # TypeScript 配置
├── vite.config.ts # Vite 配置
└── vitest.config.ts # 测试配置
1.2 开发环境配置
bash
# 克隆项目
git clone https://github.com/element-plus/element-plus.git
cd element-plus
# 安装依赖(使用 pnpm)
pnpm install
# 启动开发服务器
pnpm dev
# 构建项目
pnpm build
# 运行测试
pnpm test
# 代码检查
pnpm lint
# 类型检查
pnpm typecheck
2. 代码规范和风格指南
2.1 TypeScript 代码规范
typescript
// types/button.ts
// 组件类型定义规范
// 1. 导入顺序:第三方库 -> 内部模块 -> 类型导入
import { ExtractPropTypes, PropType } from 'vue'
import { isString } from '@element-plus/utils'
import type { Component } from 'vue'
// 2. 使用 const assertion 定义常量
export const buttonTypes = ['default', 'primary', 'success', 'warning', 'info', 'danger', 'text'] as const
export const buttonSizes = ['large', 'default', 'small'] as const
export const buttonNativeTypes = ['button', 'submit', 'reset'] as const
// 3. Props 定义使用 buildProps 工具函数
export const buttonProps = buildProps({
/**
* @description button size
*/
size: {
type: String as PropType<ButtonSize>,
validator: (val: string): val is ButtonSize => buttonSizes.includes(val as ButtonSize)
},
/**
* @description disable the button
*/
disabled: Boolean,
/**
* @description button type
*/
type: {
type: String as PropType<ButtonType>,
values: buttonTypes,
default: 'default'
},
/**
* @description icon component
*/
icon: {
type: iconPropType
},
/**
* @description determine whether it's loading
*/
loading: Boolean,
/**
* @description customize loading icon component
*/
loadingIcon: {
type: iconPropType,
default: () => Loading
},
/**
* @description native button type
*/
nativeType: {
type: String as PropType<ButtonNativeType>,
values: buttonNativeTypes,
default: 'button'
},
/**
* @description determine whether it's a plain button
*/
plain: Boolean,
/**
* @description determine whether it's a text button
*/
text: Boolean,
/**
* @description determine whether it's a link button
*/
link: Boolean,
/**
* @description determine whether the button is round
*/
round: Boolean,
/**
* @description determine whether the button is circle
*/
circle: Boolean,
/**
* @description custom button color, automatically calculate `hover` and `active` color
*/
color: String,
/**
* @description dark mode, which automatically switches styles
*/
dark: Boolean,
/**
* @description native autofocus attribute
*/
autofocus: Boolean,
/**
* @description custom element tag
*/
tag: {
type: definePropType<string | Component>(String),
default: 'button'
}
} as const)
// 4. 导出类型
export type ButtonProps = ExtractPropTypes<typeof buttonProps>
export type ButtonType = typeof buttonTypes[number]
export type ButtonSize = typeof buttonSizes[number]
export type ButtonNativeType = typeof buttonNativeTypes[number]
// 5. Emits 定义
export const buttonEmits = {
click: (evt: MouseEvent) => evt instanceof MouseEvent
}
export type ButtonEmits = typeof buttonEmits
// 6. 组件实例类型
export type ButtonInstance = InstanceType<typeof Button>
2.2 Vue 组件规范
vue
<!-- Button.vue -->
<!-- 1. 模板结构清晰,使用语义化标签 -->
<template>
<component
:is="tag"
ref="_ref"
:class="[
ns.b(),
ns.m(buttonType),
ns.m(buttonSize),
ns.is('disabled', buttonDisabled),
ns.is('loading', loading),
ns.is('plain', plain),
ns.is('round', round),
ns.is('circle', circle),
ns.is('text', text),
ns.is('link', link),
ns.is('has-bg', hasBackground)
]"
:aria-disabled="buttonDisabled || loading ? 'true' : 'false'"
:disabled="buttonDisabled || loading || undefined"
:autofocus="autofocus"
:type="tag === 'button' ? nativeType : undefined"
:style="buttonStyle"
@click="handleClick"
>
<template v-if="loading">
<slot v-if="$slots.loading" name="loading" />
<el-icon v-else :class="ns.is('loading')">
<component :is="loadingIcon" />
</el-icon>
</template>
<el-icon v-else-if="icon || $slots.icon">
<component :is="icon" v-if="icon" />
<slot v-else name="icon" />
</el-icon>
<span
v-if="$slots.default"
:class="{ [ns.em('text', 'expand')]: shouldAddSpace }"
>
<slot />
</span>
</component>
</template>
<script lang="ts" setup>
// 2. 导入顺序规范
import { computed, inject, ref, useSlots } from 'vue'
import { ElIcon } from '@element-plus/components/icon'
import {
useDisabled,
useFormItem,
useGlobalConfig,
useNamespace,
useSize
} from '@element-plus/hooks'
import { buttonGroupContextKey } from '@element-plus/tokens'
import { TinyColor } from '@ctrl/tinycolor'
import { buttonEmits, buttonProps } from './button'
import type { ButtonEmits, ButtonProps } from './button'
// 3. 定义组件选项
defineOptions({
name: 'ElButton',
inheritAttrs: false
})
// 4. Props 和 Emits
const props = defineProps(buttonProps)
const emit = defineEmits(buttonEmits)
// 5. 组合式函数使用
const slots = useSlots()
const buttonGroupContext = inject(buttonGroupContextKey, undefined)
const globalConfig = useGlobalConfig('button')
const ns = useNamespace('button')
const { form } = useFormItem()
const _size = useSize(computed(() => buttonGroupContext?.size))
const _disabled = useDisabled()
const _ref = ref<HTMLButtonElement>()
// 6. 计算属性
const buttonSize = computed(() => props.size || _size.value || 'default')
const buttonDisabled = computed(() => props.disabled || _disabled.value || false)
const buttonType = computed(() => props.type || buttonGroupContext?.type || 'default')
const autoInsertSpace = computed(
() => props.autoInsertSpace ?? globalConfig.value?.autoInsertSpace ?? false
)
// 检查是否需要在文字两侧添加空格
const shouldAddSpace = computed(() => {
const defaultSlot = slots.default?.()
if (autoInsertSpace.value && defaultSlot?.length === 1) {
const slot = defaultSlot[0]
if (slot?.type === Text) {
const text = slot.children as string
return /^\p{Unified_Ideograph}{2}$/u.test(text.trim())
}
}
return false
})
// 7. 样式计算
const buttonStyle = computed(() => {
let styles: CSSProperties = {}
const buttonColor = props.color
if (buttonColor) {
const color = new TinyColor(buttonColor)
const activeBgColor = props.dark
? color.tint(20).toString()
: color.shade(10).toString()
if (props.plain) {
styles = {
'--el-button-bg-color': props.dark
? color.tint(90).toString()
: color.tint(90).toString(),
'--el-button-text-color': buttonColor,
'--el-button-border-color': props.dark
? color.tint(50).toString()
: color.tint(50).toString(),
'--el-button-hover-text-color': `var(--el-color-white)`,
'--el-button-hover-bg-color': buttonColor,
'--el-button-hover-border-color': buttonColor,
'--el-button-active-bg-color': activeBgColor,
'--el-button-active-text-color': `var(--el-color-white)`,
'--el-button-active-border-color': activeBgColor
}
} else {
const hoverBgColor = props.dark
? color.tint(20).toString()
: color.tint(20).toString()
styles = {
'--el-button-bg-color': buttonColor,
'--el-button-border-color': buttonColor,
'--el-button-hover-bg-color': hoverBgColor,
'--el-button-hover-border-color': hoverBgColor,
'--el-button-active-bg-color': activeBgColor,
'--el-button-active-border-color': activeBgColor
}
}
if (buttonDisabled.value) {
const disabledButtonColor = props.dark
? color.tint(50).toString()
: color.tint(50).toString()
styles['--el-button-disabled-bg-color'] = disabledButtonColor
styles['--el-button-disabled-border-color'] = disabledButtonColor
}
}
return styles
})
const hasBackground = computed(() => {
return (
props.type !== 'text' &&
props.type !== 'link' &&
!props.plain
)
})
// 8. 事件处理
const handleClick = (evt: MouseEvent) => {
if (props.nativeType === 'reset') {
form?.resetFields()
}
emit('click', evt)
}
// 9. 暴露给父组件的方法和属性
defineExpose({
/** @description button html element */
ref: _ref,
/** @description button size */
size: buttonSize,
/** @description button type */
type: buttonType,
/** @description button disabled */
disabled: buttonDisabled
})
</script>
2.3 样式规范 (SCSS)
scss
// button.scss
// 1. 使用 BEM 命名规范
@use 'sass:map';
@use 'mixins/mixins' as *;
@use 'mixins/utils' as *;
@use 'mixins/var' as *;
@use 'common/var' as *;
// 2. 组件变量定义
$button-font-weight: getCssVar('font-weight-primary') !default;
$button-border-width: getCssVar('border-width') !default;
$button-border-style: getCssVar('border-style') !default;
$button-border-color: getCssVar('border-color') !default;
$button-border-radius: getCssVar('border-radius-base') !default;
$button-bg-color: getCssVar('color-white') !default;
$button-font-size: getCssVar('font-size-base') !default;
$button-outline-color: getCssVar('color-primary-light-5') !default;
$button-active-color: getCssVar('color-primary-dark-2') !default;
// 3. 主要样式块
@include b(button) {
// 基础样式
display: inline-flex;
justify-content: center;
align-items: center;
line-height: 1;
min-height: getCssVar('component-size');
white-space: nowrap;
cursor: pointer;
color: getCssVar('button-text-color');
text-align: center;
box-sizing: border-box;
outline: none;
transition: 0.1s;
font-weight: $button-font-weight;
user-select: none;
vertical-align: middle;
-webkit-appearance: none;
background-color: getCssVar('button-bg-color');
border: $button-border-width $button-border-style
getCssVar('button-border-color');
border-radius: $button-border-radius;
font-size: $button-font-size;
padding: getCssVar('button-padding-vertical')
getCssVar('button-padding-horizontal');
// 悬停状态
&:hover,
&:focus {
color: getCssVar('button-hover-text-color');
border-color: getCssVar('button-hover-border-color');
background-color: getCssVar('button-hover-bg-color');
outline: none;
}
// 激活状态
&:active {
color: getCssVar('button-active-text-color');
border-color: getCssVar('button-active-border-color');
background-color: getCssVar('button-active-bg-color');
outline: none;
}
// 焦点状态
&:focus-visible {
outline: 2px solid getCssVar('button-outline-color');
outline-offset: 1px;
}
// 4. 修饰符样式
@include m(primary) {
@include button-variant(
getCssVar('color-white'),
getCssVar('color-primary'),
getCssVar('color-primary')
);
}
@include m(success) {
@include button-variant(
getCssVar('color-white'),
getCssVar('color-success'),
getCssVar('color-success')
);
}
@include m(warning) {
@include button-variant(
getCssVar('color-white'),
getCssVar('color-warning'),
getCssVar('color-warning')
);
}
@include m(danger) {
@include button-variant(
getCssVar('color-white'),
getCssVar('color-danger'),
getCssVar('color-danger')
);
}
@include m(info) {
@include button-variant(
getCssVar('color-white'),
getCssVar('color-info'),
getCssVar('color-info')
);
}
// 5. 尺寸变体
@include m(large) {
@include set-css-var-value('button-size', getCssVar('component-size-large'));
height: getCssVar('button-size');
padding: getCssVar('button-large-padding-vertical')
getCssVar('button-large-padding-horizontal');
font-size: getCssVar('button-large-font-size');
border-radius: getCssVar('button-large-border-radius');
}
@include m(small) {
@include set-css-var-value('button-size', getCssVar('component-size-small'));
height: getCssVar('button-size');
padding: getCssVar('button-small-padding-vertical')
getCssVar('button-small-padding-horizontal');
font-size: getCssVar('button-small-font-size');
border-radius: getCssVar('button-small-border-radius');
}
// 6. 状态样式
@include when(disabled) {
&,
&:hover,
&:focus {
color: getCssVar('button-disabled-text-color');
cursor: not-allowed;
background-image: none;
background-color: getCssVar('button-disabled-bg-color');
border-color: getCssVar('button-disabled-border-color');
}
}
@include when(loading) {
position: relative;
pointer-events: none;
&:before {
// loading 遮罩
z-index: 1;
pointer-events: none;
content: '';
position: absolute;
left: -1px;
top: -1px;
right: -1px;
bottom: -1px;
border-radius: inherit;
background-color: getCssVar('mask-color-extra-light');
}
}
@include when(plain) {
@include css-var-from-global('button-hover-text-color', 'color-primary');
@include css-var-from-global('button-hover-bg-color', 'color-primary-light-9');
@include css-var-from-global('button-hover-border-color', 'color-primary');
}
@include when(round) {
border-radius: getCssVar('border-radius-round');
}
@include when(circle) {
border-radius: 50%;
padding: getCssVar('button-padding-vertical');
width: getCssVar('button-size');
height: getCssVar('button-size');
}
@include when(text) {
color: getCssVar('button-text-color');
border: 0 solid transparent;
background-color: transparent;
@include when(disabled) {
color: getCssVar('button-disabled-text-color');
background-color: transparent !important;
}
&:not(.is-disabled) {
&:hover,
&:focus {
background-color: getCssVar('button-hover-bg-color');
}
&:focus-visible {
outline: 2px solid getCssVar('button-outline-color');
outline-offset: 1px;
border-radius: getCssVar('border-radius-base');
}
&:active {
background-color: getCssVar('button-active-bg-color');
}
}
}
@include when(link) {
border-color: transparent;
color: getCssVar('button-text-color');
background: transparent;
padding: 2px;
height: auto;
&:hover,
&:focus {
color: getCssVar('button-hover-text-color');
}
@include when(disabled) {
color: getCssVar('button-disabled-text-color');
background-color: transparent !important;
border-color: transparent !important;
}
}
// 7. 元素样式
@include e(text) {
@include m(expand) {
letter-spacing: 0.3em;
margin-right: -0.3em;
}
}
// 8. 图标样式
.#{$namespace}-icon {
& + span {
margin-left: getCssVar('button-icon-span-gap');
}
svg {
vertical-align: bottom;
}
@include when(loading) {
animation: rotating 2s linear infinite;
}
}
// 9. 响应式设计
@include res(phone) {
@include m(large) {
padding: getCssVar('button-padding-vertical')
getCssVar('button-padding-horizontal');
font-size: getCssVar('button-font-size');
}
}
}
// 10. 按钮组样式
@include b(button-group) {
@include utils-clearfix;
display: inline-block;
vertical-align: middle;
&::before,
&::after {
display: table;
content: '';
}
&::after {
clear: both;
}
& > .#{$namespace}-button {
float: left;
position: relative;
margin-left: 0;
&:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right-color: getCssVar('button-group-border-color');
}
&:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left-color: getCssVar('button-group-border-color');
}
&:not(:first-child):not(:last-child) {
border-radius: 0;
border-left-color: getCssVar('button-group-border-color');
border-right-color: getCssVar('button-group-border-color');
}
&:not(:last-child) {
margin-right: -1px;
}
&:hover,
&:focus,
&:active {
z-index: 1;
}
@include when(active) {
z-index: 1;
}
}
& > .#{$namespace}-dropdown {
& > .#{$namespace}-button {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left-color: getCssVar('button-group-border-color');
}
}
@each $type in (primary, success, warning, danger, info) {
.#{$namespace}-button--#{$type} {
&:first-child {
border-right-color: getCssVar('button', $type, 'border-color');
}
&:last-child {
border-left-color: getCssVar('button', $type, 'border-color');
}
&:not(:first-child):not(:last-child) {
border-left-color: getCssVar('button', $type, 'border-color');
border-right-color: getCssVar('button', $type, 'border-color');
}
}
}
}
3. 测试规范
3.1 单元测试规范
typescript
// __tests__/button.test.tsx
import { nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import { describe, expect, it, test, vi } from 'vitest'
import { Loading, Search } from '@element-plus/icons-vue'
import Button from '../src/button.vue'
import ButtonGroup from '../src/button-group.vue'
// 1. 测试组织结构
describe('Button', () => {
// 2. 基础功能测试
describe('basic functionality', () => {
it('should render correctly', () => {
const wrapper = mount(Button, {
slots: {
default: 'Test Button'
}
})
expect(wrapper.text()).toBe('Test Button')
expect(wrapper.classes()).toContain('el-button')
})
it('should handle click events', async () => {
const handleClick = vi.fn()
const wrapper = mount(Button, {
props: {
onClick: handleClick
},
slots: {
default: 'Click Me'
}
})
await wrapper.trigger('click')
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('should be disabled when disabled prop is true', async () => {
const wrapper = mount(Button, {
props: {
disabled: true
}
})
expect(wrapper.classes()).toContain('is-disabled')
expect(wrapper.attributes('disabled')).toBeDefined()
})
})
// 3. Props 测试
describe('props', () => {
it('should render different types correctly', () => {
const types = ['primary', 'success', 'warning', 'danger', 'info']
types.forEach(type => {
const wrapper = mount(Button, {
props: { type }
})
expect(wrapper.classes()).toContain(`el-button--${type}`)
})
})
it('should render different sizes correctly', () => {
const sizes = ['large', 'default', 'small']
sizes.forEach(size => {
const wrapper = mount(Button, {
props: { size }
})
if (size !== 'default') {
expect(wrapper.classes()).toContain(`el-button--${size}`)
}
})
})
it('should show loading state', () => {
const wrapper = mount(Button, {
props: {
loading: true
}
})
expect(wrapper.classes()).toContain('is-loading')
expect(wrapper.findComponent(Loading).exists()).toBe(true)
})
it('should render custom loading icon', () => {
const wrapper = mount(Button, {
props: {
loading: true,
loadingIcon: Search
}
})
expect(wrapper.findComponent(Search).exists()).toBe(true)
})
it('should render icon correctly', () => {
const wrapper = mount(Button, {
props: {
icon: Search
}
})
expect(wrapper.findComponent(Search).exists()).toBe(true)
})
})
// 4. 插槽测试
describe('slots', () => {
it('should render icon slot', () => {
const wrapper = mount(Button, {
slots: {
icon: '<i class="custom-icon"></i>'
}
})
expect(wrapper.find('.custom-icon').exists()).toBe(true)
})
it('should render loading slot', () => {
const wrapper = mount(Button, {
props: {
loading: true
},
slots: {
loading: '<span class="custom-loading">Loading...</span>'
}
})
expect(wrapper.find('.custom-loading').exists()).toBe(true)
})
})
// 5. 事件测试
describe('events', () => {
it('should not trigger click when disabled', async () => {
const handleClick = vi.fn()
const wrapper = mount(Button, {
props: {
disabled: true,
onClick: handleClick
}
})
await wrapper.trigger('click')
expect(handleClick).not.toHaveBeenCalled()
})
it('should not trigger click when loading', async () => {
const handleClick = vi.fn()
const wrapper = mount(Button, {
props: {
loading: true,
onClick: handleClick
}
})
await wrapper.trigger('click')
expect(handleClick).not.toHaveBeenCalled()
})
})
// 6. 样式测试
describe('styles', () => {
it('should apply custom color', () => {
const customColor = '#ff0000'
const wrapper = mount(Button, {
props: {
color: customColor
}
})
const style = wrapper.attributes('style')
expect(style).toContain('--el-button-bg-color')
})
it('should add space between Chinese characters', async () => {
const wrapper = mount(Button, {
props: {
autoInsertSpace: true
},
slots: {
default: '按钮'
}
})
await nextTick()
expect(wrapper.find('.el-button__text--expand').exists()).toBe(true)
})
})
// 7. 可访问性测试
describe('accessibility', () => {
it('should have correct aria attributes when disabled', () => {
const wrapper = mount(Button, {
props: {
disabled: true
}
})
expect(wrapper.attributes('aria-disabled')).toBe('true')
})
it('should have correct aria attributes when loading', () => {
const wrapper = mount(Button, {
props: {
loading: true
}
})
expect(wrapper.attributes('aria-disabled')).toBe('true')
})
})
})
// 8. 按钮组测试
describe('ButtonGroup', () => {
it('should render button group correctly', () => {
const wrapper = mount(ButtonGroup, {
slots: {
default: `
<el-button>Button 1</el-button>
<el-button>Button 2</el-button>
`
},
global: {
components: {
'el-button': Button
}
}
})
expect(wrapper.classes()).toContain('el-button-group')
expect(wrapper.findAllComponents(Button)).toHaveLength(2)
})
it('should apply group size to buttons', () => {
const wrapper = mount(ButtonGroup, {
props: {
size: 'large'
},
slots: {
default: '<el-button>Button</el-button>'
},
global: {
components: {
'el-button': Button
}
}
})
const button = wrapper.findComponent(Button)
expect(button.vm.size).toBe('large')
})
})
3.2 E2E 测试规范
typescript
// e2e/button.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Button E2E Tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/components/button')
})
test('should render basic button', async ({ page }) => {
const button = page.locator('.el-button').first()
await expect(button).toBeVisible()
await expect(button).toHaveText('Default')
})
test('should handle click interaction', async ({ page }) => {
const button = page.locator('[data-testid="click-button"]')
const counter = page.locator('[data-testid="click-counter"]')
await expect(counter).toHaveText('0')
await button.click()
await expect(counter).toHaveText('1')
})
test('should show loading state', async ({ page }) => {
const loadingButton = page.locator('[data-testid="loading-button"]')
await loadingButton.click()
await expect(loadingButton).toHaveClass(/is-loading/)
await expect(loadingButton.locator('.el-icon')).toBeVisible()
})
test('should be keyboard accessible', async ({ page }) => {
const button = page.locator('.el-button').first()
// 使用 Tab 键聚焦
await page.keyboard.press('Tab')
await expect(button).toBeFocused()
// 使用 Enter 键激活
await page.keyboard.press('Enter')
// 验证点击效果
})
test('should support different themes', async ({ page }) => {
const primaryButton = page.locator('.el-button--primary').first()
const successButton = page.locator('.el-button--success').first()
await expect(primaryButton).toHaveCSS('background-color', 'rgb(64, 158, 255)')
await expect(successButton).toHaveCSS('background-color', 'rgb(103, 194, 58)')
})
test('should work in button group', async ({ page }) => {
const buttonGroup = page.locator('.el-button-group').first()
const buttons = buttonGroup.locator('.el-button')
await expect(buttons).toHaveCount(3)
// 检查第一个按钮的边框半径
const firstButton = buttons.first()
await expect(firstButton).toHaveCSS('border-top-right-radius', '0px')
await expect(firstButton).toHaveCSS('border-bottom-right-radius', '0px')
})
})
4. 文档规范
4.1 组件文档结构
markdown
# Button 按钮
常用的操作按钮。
## 基础用法
基础的按钮用法。
:::demo 使用 `type`、`plain`、`round` 和 `circle` 属性来定义 Button 的样式。
button/basic
:::
## 禁用状态
按钮不可用状态。
:::demo 你可以使用 `disabled` 属性来定义按钮是否可用,它接受一个 `Boolean` 值。
button/disabled
:::
## 链接按钮
没有边框和背景色的按钮。
:::demo 使用 `link` 属性创建链接按钮,通常用于页面内的功能性链接。
button/link
:::
## 文字按钮
没有边框和背景色的按钮。
:::demo 使用 `text` 属性创建文字按钮。
button/text
:::
## 图标按钮
带图标的按钮可增强辨识度(有文字)或节省空间(无文字)。
:::demo 设置 `icon` 属性即可,icon 的列表可以参考 Element Plus 的 icon 组件,也可以设置在文字右边的 icon ,只要使用 `i` 标签即可,可以使用自定义图标。
button/icon
:::
## 按钮组
以按钮组的方式出现,常用于多项类似操作。
:::demo 使用 `<el-button-group>` 标签来嵌套你的按钮。
button/group
:::
## 加载中
点击按钮后进行数据加载操作,在按钮上显示加载状态。
:::demo 要设置为 loading 状态,只要设置 `loading` 属性为 `true` 即可。
button/loading
:::
## 不同尺寸
Button 组件提供除了默认值以外的三种尺寸,可以在不同场景下选择合适的按钮尺寸。
:::demo 额外的尺寸:`large`、`small`,通过设置 `size` 属性来配置它们。
button/size
:::
## 自定义颜色
您可以自定义按钮颜色。
:::demo 我们将自动计算 hover 和 active 颜色。
button/custom-color
:::
## Button API
### Button Attributes
| 属性名 | 说明 | 类型 | 默认值 |
| --------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | --------- |
| size | 尺寸 | `enum` - `'large'\| 'default' \| 'small'` | — |
| type | 类型 | `enum` - `'primary'\| 'success' \| 'warning' \| 'danger' \| 'info'` | — |
| plain | 是否朴素按钮 | `boolean` | `false` |
| text ^(2.2.0) | 是否文字按钮 | `boolean` | `false` |
| link ^(2.2.1) | 是否链接按钮 | `boolean` | `false` |
| round | 是否圆角按钮 | `boolean` | `false` |
| circle | 是否圆形按钮 | `boolean` | `false` |
| loading | 是否加载中状态 | `boolean` | `false` |
| loading-icon | 自定义加载中状态图标组件 | `string \| Component` | `Loading` |
| disabled | 按钮是否为禁用状态 | `boolean` | `false` |
| icon | 图标组件 | `string \| Component` | — |
| autofocus | 原生 `autofocus` 属性 | `boolean` | `false` |
| native-type | 原生 `type` 属性 | `enum` - `'button' \| 'submit' \| 'reset'` | `button` |
| auto-insert-space | 自动在两个中文字符之间插入空格 | `boolean` | — |
| color | 自定义按钮颜色, 并自动计算 `hover` 和 `active` 触发后的颜色 | `string` | — |
| dark | dark 模式, 意味着自动设置 `color` 为 dark 模式的颜色 | `boolean` | `false` |
| tag ^(2.3.4) | 自定义元素标签 | `string \| Component` | `button` |
### Button Events
| 事件名 | 说明 | 类型 |
| ------ | ---------------- | ------------------------- |
| click | 点击按钮时触发 | `(event: MouseEvent) => void` |
### Button Slots
| 插槽名 | 说明 |
| ------- | -------------- |
| default | 自定义默认内容 |
| loading | 自定义加载中组件 |
| icon | 自定义图标组件 |
### Button Exposes
| 名称 | 说明 | 类型 |
| -------- | -------------- | -------------------- |
| ref | 按钮 html 元素 | `Ref<HTMLButtonElement>` |
| size | 按钮尺寸 | `ComputedRef<string>` |
| type | 按钮类型 | `ComputedRef<string>` |
| disabled | 按钮是否禁用 | `ComputedRef<boolean>` |
## ButtonGroup API
### ButtonGroup Attributes
| 属性名 | 说明 | 类型 | 默认值 |
| ------ | ------------------------------------------------ | ------------------------------------------------- | ------ |
| size | 用于控制该按钮组内按钮的大小 | `enum` - `'large' \| 'default' \| 'small'` | — |
| type | 用于控制该按钮组内按钮的类型 | `enum` - `'primary' \| 'success' \| 'warning' \| 'danger' \| 'info'` | — |
### ButtonGroup Slots
| 插槽名 | 说明 | 子标签 |
| ------- | ----------------------- | ------ |
| default | 自定义按钮组内容 | Button |
实践练习
练习 1:创建自定义按钮组件
基于 Element Plus 的代码规范,创建一个自定义的按钮组件:
- 支持所有标准按钮功能
- 添加自定义动画效果
- 完整的 TypeScript 类型定义
- 完善的测试用例
练习 2:贡献代码到 Element Plus
尝试为 Element Plus 项目贡献代码:
- Fork 项目并设置开发环境
- 找到一个 issue 或提出改进建议
- 按照规范编写代码
- 提交 Pull Request
练习 3:编写组件文档
为自定义组件编写完整的文档:
- API 文档
- 使用示例
- 最佳实践指南
- 常见问题解答
学习资源
作业
- 完成所有实践练习
- 阅读 Element Plus 源码,理解组件实现原理
- 参与 Element Plus 社区讨论
- 尝试修复一个简单的 bug 或添加小功能
下一步学习计划
接下来我们将学习 Element Plus 代码贡献与 Pull Request 流程,深入了解如何参与开源项目的开发和维护。