element源码之el-icon和el-button组件

967 阅读4分钟

el-icon

el-icon是图标组件,看文档描述,直接设置类名就可以了 image.png
我们来看下组件中是如何实现的。打开packages/icon/src/icon.vue,里面很简单,只给了一个name属性去拼接成带el-icon-的类名

<template>
  <i :class="'el-icon-' + name"></i>
</template>

<script>
  export default {
    name: 'ElIcon',
    props: {
      name: String
    }
  };
</script>

再看下对应的icon.scss文件,里面有一系列类名对应的样式,给每个类名添加了一个伪元素,并写入内容。这些内容是在icon.scss文件里面一开头就定义了的字体样式,字体文件会解析对应的字体图标,从而实现通过类名就可以显示出不同的图标。

image.png
image.png\

我们看上面的文档教我们的使用方法是<i class="el-icon-delete-solod"></i>,在项目开发中我们也照着文档所示的方式去使用icon, 并不是通过像el-button那样,通过组件的方式来使用<el-icon name="xxx"></el-icon>,那我们可以以组件的形式来使用吗?
我们像下面那样试一下,页面是可以渲染出来的,我们可以以组件的方式来使用el-icon图标。

<el-icon name="delete-solid" />

image.png

el-button

provide和inject 依赖注入

在阅读el-button的源码前,我们需要先来学习一个vue的知识点组合式API:依赖注入provideinject
provide提供一个值,可以被后代组件注入。接受两个参数,第一个是要注入的key,可以是字符串或者Symbol,第二个参数是要注入的值,可以像下面这样使用

// 方式一
export default {
    provide: {
        message: 'hello!'
    }
}

// 方式二
export default {
    data() {
        return {
            message: 'hello!'
        }
    },
    provide() {
        // 使用函数的形式,可以访问到 `this`
        return {
            message: this.message
        }
    }
}

inject注入上层传过来的值,注入的数据会在组件自身的状态之前被解析,因此可以在data()中访问注入的属性,可以像下面这样使用,更多使用方式见文档

export default {
    // 方式一
    inject: ['message'],
    
    // 方式二,注入别名
    inject: {
        localMessage: { // localMessage是别名
            from: 'message' // from是来源
        }
    },
    
    // 方式三,注入有默认值,此时必须使用对象形式
    inject: {
        message: {
            from: 'message', // 当与原注入名同名时,可以省略
            deafult: 'default value'
        }
    }
}
逻辑

我们接着来看el-button的源码,和文档上写的一样,传入了一些属性,注入了elFormelFormItem两个属性,计算了三个属性,当点击时会传出一个click方法,默认参数为event

<script>
  export default {
    name: 'ElButton',
    inject: {
      elForm: {
        default: ''
      },
      elFormItem: {
        default: ''
      }
    },
    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
    },
    methods: {
      handleClick(evt) {
        this.$emit('click', evt);
      }
    }
  };
</script>

上面的elFormelFormItem从哪里注入的呢?我们可以找到packages/form/src,打开form-item.vueform.vue,可以看到提供了注入,这里的this就是当前的form-itemform组件

// form.vue
export default {
    provide() {
      return {
        elForm: this
      };
    },
}

// form-item.vue
export default {
    provide() {
      return {
        elFormItem: this
      };
    },
}

因此在button组件的计算属性中,this.elFormItemthis.elForm就相当于访问form-itemform组件的elFormItemSizedisabled属性

export default {
    computed: {
      // 当button嵌入在form-item组件中时,没有传递button的size属性
      // buttonSize要判断一下外层的父组件form-item是否有传递size属性,有则用外层的
      _elFormItemSize() {
        return (this.elFormItem || {}).elFormItemSize;
      },
      buttonSize() {
        return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
      },
      // 当button嵌入在form组件中时,没有传递disabled属性或者为false
      // 要判断下外层的父组件form是否有传递disabled属性,有则用外层的
      buttonDisabled() {
        return this.disabled || (this.elForm || {}).disabled;
      }
    },
}

目前我们阅读的源码版本是v2.15.6,但是这样写的buttonDisabled会有个问题:当我们给按钮明确传入false的时候,按钮应该不会被禁用,可是当他发现this.disabled为空或false时就会去找elForm身上的disabled,也就是说如果elForm设置disabled:true,离el-Form最近的那个按钮就会导致失效。

这个问题在v2.15.7版本中被修复了,会先去判断下button组件本身是否有传递disabled属性,没传递再用elForm组件中的。

我们在平时的开发中也会遇到没有考虑完全的时候,这就需要我们在迭代过程中不断测试、完善逻辑。

computed: {
    buttonDisabled() {
        return this.$options.propsData.hasOwnProperty('disabled') ?  this.disabled : (this.elForm || {}).disabled;
     }
 }

再来看下template部分,给button元素绑定了一系列属性,预留了一个插槽

<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>
scss部分

上篇文章我们已经分析过了b、e、m的作用,现在可以直接来分析下buttonscss文件。

下面的代码中有一个&+&scss中的&表示父级元素,即.el-button,所以&+&即为.el-button + .el-button,表示相邻的.el-button元素需要设置左外边距10px

/* button.scss */
@include b(button) {
    display: inline-block;
    ...
    @include utils-user-select(none);
    & + & {
      margin-left: 10px;
    }
}

/* util.scss */ 
/* 禁止用户选中文字 */
@mixin utils-user-select($value) {
  -moz-user-select: $value;
  -webkit-user-select: $value;
  -ms-user-select: $value;
}

button.scss文件中还有一个when混合。
$state-prefixconfig.scss中定义了:is-$state为传进混合的plain,结合起来就是is-plain,这里的&仍是.el-button,用一个@at-root来跳出嵌套

/* button.scss */
@include b(button) {
    ...
    @include when(plain) {
        &:hover,&:focus {
          ...
        }
        &:active {
          ...
        }
    }
}

/* mixins.scss */
@mixin when($state) {
  @at-root {
    &.#{$state-prefix + $state} {
      @content;
    }
  }
}

/* 第一段代码会被编译为 */
.el-button {
    ...
}
.el-button.is-plain {
    &:hover,&:focus {
        ...
    }
}

button.scss中还有一个utils-clearfix混合,这是用于清除浮动的混合

@mixin utils-clearfix {
  $selector: &;

  @at-root {
    #{$selector}::before,
    #{$selector}::after {
      display: table;
      content: "";
    }
    #{$selector}::after {
      clear: both
    }
  }
}

小结

至此,el-iconel-button部分的源码我们就分析完毕了。从el-icon中我们学习到图标是通过字体文件库来实现的,从el-button中学习到了provideinject的用法。