Element UI 组件源码分析之Button按钮

1,543 阅读4分钟

0x00 简介

组件 Button 标记了一个(或封装一组)操作命令,响应用户点击行为,触发相应的业务逻辑。 本文将深入分析源码,剖析其实现原理,耐心读完,相信会对您有所帮助。源码实现详见packages/button/src/ 文件夹下  button.vue、 button-group.vue 等组件实现。 🔗 组件文档 Button 🔗 gitee源码

更多组件分析详见 👉 📚 Element UI 源码剖析组件总览

本专栏的 gitbook 版本地址已经发布 📚《learning element-ui》 ,内容同步更新中!


按钮组件有两部分 <button><button-group>,组件源码都在 packages/button/src/ 文件夹下。在项目工程化机制下,每个组件对应各自的文件夹 component-name, 定义导出组件并为其扩展 install 方法,以 commonjs2 规范对每个组件单独打包构建,支持按需引入。

0x01 button-group 按钮组

button-group.vue 组件是包裹button按钮的容器。 组件创建一个 class 名为el-button-group<div>元素容器,提供了匿名插槽,用于分发button组件使用内容。

<template>
  <div class="el-button-group">
    <slot></slot>
  </div>
</template>
<script>
  export default {
    name: 'ElButtonGroup'
  };
</script>

0x02 button 组件

template 模板内容

组件创建一个class 名为el-button的原生button元素,内部由loading 加载图标icon 图标以及自定义按钮文本等三部分组成。

<template>
  <button
    class="el-button"
    @click="handleClick"
    :disabled="buttonDisabled || loading"
    :autofocus="autofocus"
    :type="nativeType"
    :class="[
      type ? 'el-button--' + type : '',
      buttonSize ? 'el-button--' + buttonSize : '',
      {
        'is-disabled': buttonDisabled,
        'is-loading': loading,
        'is-plain': plain,
        'is-round': round,
        'is-circle': circle
      }
    ]"
  >
    <i class="el-icon-loading" v-if="loading"></i>
    <i :class="icon" v-if="icon && !loading"></i>
    <span v-if="$slots.default"><slot></slot></span>
  </button>
</template>

button元素根节点

  • 监听click事件,绑定了handleClick回调函数。

  • 禁用状态由计算属性buttonDisabled、按钮加载状态loading属性值判定。

  • 通过 autofocus 属性值设定当页面加载时按钮是否有输入焦点。

  • 原生 button 类型设置。可选值 button / submit / reset

    • submit:  此按钮将表单数据提交给服务器。如果未指定属性,或者属性动态更改为空值或无效值,则此值为默认值。
    • reset:  此按钮重置所有组件为初始值。
    • button: 此按钮没有默认行为。它可以有与元素事件相关的客户端脚本,当事件出现时可触发。
  • 动态添加样式:

    • 根据 type 属性值添加类型样式el-button--[primary/success/warning/danger/info/text]
    • 根据计算属性buttonSize 添加尺寸样式 el-button--[medium/small/mini]
    • 根据计算属性buttonDisabled和prop的 loadingplainroundcircle等属性,设置 is-disabled按钮禁用状态、 is-loading按钮加载状态、 is-plain朴素按钮、 is-round圆角按钮、 is-circle圆形按钮。

内部元素节点

  • loading 加载图标 :若 loading 属性值为 true,按钮为加载状态。 渲染一个使用名称 el-icon-loading 的 Icon 图标。
  • icon 图标 :若 icon属性设定了图标类名(truthy),且按钮不是加载状态,渲染该类名图标。
  • 自定义按钮文本 :提供了匿名插槽的 <span>元素,只有分发内容时才会渲染v-if="$slots.default"

通过设置 Button 的属性来产生不同的按钮样式,推荐顺序为:type -> plain -> round/circle -> size -> loading -> disabled

attributes 属性

组件 prop 定义如下:

props: {
  type: {
    type: String,
    default: 'default'
  },
  size: String,
  icon: {
    type: String,
    default: ''
  },
  nativeType: {
    type: String,
    default: 'button'
  },
  loading: Boolean,
  disabled: Boolean,
  plain: Boolean,
  autofocus: Boolean,
  round: Boolean,
  circle: Boolean
},
参数说明类型可选值默认值
size尺寸stringmedium / small / mini
type类型stringprimary / success / warning / danger / info / text
plain是否朴素按钮booleanfalse
round是否圆角按钮booleanfalse
circle是否圆形按钮booleanfalse
loading是否加载中状态booleanfalse
disabled是否禁用状态booleanfalse
icon图标类名string
autofocus是否默认聚焦booleanfalse
native-type原生 type 属性stringbutton / submit / resetbutton

click 事件

监听组件的 click 事件 。当点击触发 click 事件,执行 handleClick(evt) 方法,调用 this.$emit('click', evt); 触发当前实例上的click事件 。

methods: {
  handleClick(evt) {
    // 触发当前实例上的事件 
    this.$emit('click', evt);
  }
}

计算属性

buttonSize

用来获取按钮的尺寸,根据 size属性、使用Form 表单时FormItem的 计算属性 elFormItemSize,还有注册组件时全局属性设置 this.$ELEMENT.size

使用依赖注入的 inject 选项接收FormItem实例,获取其计算属性 elFormItemSize

// form-item 组件
inject: {
  elFormItem: {
    default: ''
  }
},

computed: {
  _elFormItemSize() {
    return (this.elFormItem || {}).elFormItemSize;
  },
  buttonSize() {
    return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
  },
},

同样form-item组件中使用inject 选项接收form实例,用于计算元素尺寸 elFormItemSize,由 form-itemform各自的 size属性值计算可得 。

// packages\form\src\form-item.vue
provide() {
  return {
    elFormItem: this
  };
}, 
inject: ['elForm'],

props: { 
  size: String
}, 
computed: { 
  _formSize() {
    return this.elForm.size;
  },
  elFormItemSize() {
    return this.size || this._formSize;
  }, 
},

// packages\form\src\form.vue 
provide() {
  return {
    elForm: this
  };
},

props: { 
  size: String, 
}, 

buttonDisabled

用来判断按钮的禁用状态,根据 disabled属性、Form 表单的disabled属性判定。

inject: {
  elForm: {
    default: ''
  },
},

computed: {
  buttonDisabled() {
    return this.disabled || (this.elForm || {}).disabled;
  }
},

使用inject 选项接收form组件实例,获取其 prop disabled属性。

// packages\form\src\form.vue
provide() {
  return {
    elForm: this
  };
},

props: { 
  disabled: Boolean, 
},

0x03 组件样式

src/button.scss

组件buttonbutton-group 样式的源码都定义在 packages\theme-chalk\src\button.scss 使用混合指令 bemwhen、 utils-user-selectbutton-sizebutton-variant 嵌套生成组件样式。

// packages\theme-chalk\src\button.scss

// 生成 .el-button
@include b(button) {
  // ... 
  // @include utils-user-select(none); 控制用户能否选中文本
  // @include button-size(...);
  
  // 生成 .el-button+.el-button 
  & + & { /* ...  */ }
  // 生成 .el-button:focus,.el-button:hover
  &:hover, &:focus { /* ...  */ }
  // 生成 .el-button:active
  &:active { /* ...  */ }
  // 生成 .el-button::-moz-focus-inner 
  &::-moz-focus-inner { /* ...  */ }
  
  & [class*="el-icon-"] {
    // 生成 .el-button [class*=el-icon-]+span
    & + span { /* ...  */ }
  }
  
  @include when(plain) {
    // 生成 .el-button.is-plain:focus,.el-button.is-plain:hover
    &:hover,&:focus { /* ...  */ }
    // 生成 .el-button.is-plain:active
    &:active { /* ...  */ }
  }
  
  // 生成 .el-button.is-active
  @include when(active) { /* ...  */ }

  @include when(disabled) {
    // 生成 .el-button.is-disabled,.el-button.is-disabled:focus,.el-button.is-disabled:hover
    &,&:hover,&:focus { /* ...  */ }
    // 生成 .el-button.is-disabled.el-button--text
    &.el-button--text { /* ...  */ }

    &.is-plain {
      // 生成.el-button.is-disabled.is-plain, .el-button.is-disabled.is-plain:focus, .el-button.is-disabled.is-plain:hover
      &,&:hover,&:focus { /* ...  */ }
    }
  }
  // 生成 .el-button.is-loading
  @include when(loading) {
    // ... 
    
    // 生成 .el-button.is-loading:before
    &:before { /* ...  */ }
  }
  // 生成 .el-button.is-round
  @include when(round) { /* ...  */ }
  
  // 生成 .el-button.is-circle 
  @include when(circle) { /* ...  */ }
  
  /* ------------  */
  // 生成主题样式 primary/success/warning/danger/info
  @include m(primary) {
    // @include button-variant ...
  }
  // success/warning/danger/info ...
  /* ------------  */
  // 生成不同尺寸样式  medium/small/mini
  @include m(medium) {
    // @include button-size ...
    
    @include when(circle) { /* ...  */ }
  }
  // small/mini ...
  /* ------------  */
  
  // 生成 .el-button--text
  @include m(text) {
    // ...
    
    // 生成.el-button--text:focus,.el-button--text:hover
    &:hover,
    &:focus {
      // ...
    } 
    // 生成 .el-button--text:active
    &:active {
      // ...
    } 
    // 生成.el-button--text.is-disabled,.el-button--text.is-disabled:focus,.el-button--text.is-disabled:hover
    &.is-disabled,
    &.is-disabled:hover,
    &.is-disabled:focus {
      // ...
    }
  }
}

// 生成 .el-button-group 
@include b(button-group) {
  // ...
  // 生成 .el-button-group>.el-button
  & > .el-button {
    // ...
    
    // 生成 .el-button-group>.el-button+.el-button
    & + .el-button { /* ...  */ }
    // 生成 .el-button-group>.el-button.is-disabled
    &.is-disabled{ /* ...  */ }
    // 生成 .el-button-group>.el-button:first-child
    &:first-child { /* ...  */ }
    // 生成 .el-button-group>.el-button:last-child
    &:last-child { /* ...  */ }
    // 生成 .el-button-group>.el-button:first-child:last-child
    &:first-child:last-child {
      // ...
      
      // 生成 .el-button-group>.el-button:first-child:last-child.is-round
      &.is-round { /* ...  */ }
      // 生成 .el-button-group>.el-button:first-child:last-child.is-circle 
      &.is-circle { /* ...  */ }
    }
    // 生成 .el-button-group>.el-button:not(:first-child):not(:last-child)  
    &:not(:first-child):not(:last-child) { /* ...  */ }
    // 生成 .el-button-group>.el-button:not(:last-child)  
    &:not(:last-child) { /* ...  */ }
    // 生成 .el-button-group>.el-button:active,.el-button-group>.el-button:focus,.el-button-group>.el-button:hover
    &:hover,&:focus,&:active { /* ...  */ }
    // 生成 .el-button-group>.el-button.is-active
    @include when(active) { /* ...  */ }
  }
  
  & > .el-dropdown {
    // 生成 .el-button-group>.el-dropdown>.el-button
    & > .el-button { /* ...  */ }
  }

  @each $type in (primary, success, warning, danger, info) {
    .el-button--#{$type} {
      // 生成 .el-button-group .el-button--[primary/success/warning/danger/info]:first-child
      &:first-child { /* ...  */ }
      // 生成 .el-button-group .el-button--[primary/success/warning/danger/info]:last-child
      &:last-child { /* ...  */ }
      // 生成 .el-button-group .el-button--[primary/success/warning/danger/info]:not(:first-child):not(:last-child)
      &:not(:first-child):not(:last-child) { /* ...  */ }
    }
  }
}

button 组件样式定义的混合指令。

// packages\theme-chalk\src\mixins\_button.scss

@mixin button-plain($color) {
  // ...

  &:hover,
  &:focus {
    // ...
  }

  &:active {
    // ...
  }

  &.is-disabled {
    &,
    &:hover,
    &:focus,
    &:active {
      // ...
    }
  }
}

@mixin button-variant($color, $background-color, $border-color) {
  // ...

  &:hover,
  &:focus {
    // ...
  }
  
  &:active {
    // ...
  }

  &.is-active {
    // ...
  }

  &.is-disabled {
    &,
    &:hover,
    &:focus,
    &:active {
      // ...
    }
  }

  &.is-plain {
    @include button-plain($background-color);
  }
}

@mixin button-size($padding-vertical, $padding-horizontal, $font-size, $border-radius) {
  // ...
  
  &.is-round {
    padding: $padding-vertical $padding-horizontal;
  }
}

📚参考&关联阅读

"button",MDN
"依赖注入",vuejs.org

关注专栏

如果本文对您有所帮助请关注➕、 点赞👍、 收藏⭐!您的认可就是对我的最大支持!

此文章已收录到专栏中 👇,可以直接关注。