# 我的 Element-ui 源码学习 --- el-alert

作者:Horace 介绍:我的 Element-ui 源码学习,持续不定时更新~ ps:新人小白一枚,如有错误,欢迎指出~

笔者现在是一个大三快要实习的学生,最近在学习 Vue 的时候感觉自己学了很多的东西但是没有什么实质性的进步,感觉达到了一个瓶颈,但自认为还没有达到可以深挖 Vue 源码的功力,所以打算开始剖析 Element-ui 的源码,希望可以借此深入 Vue,后面我也会开始 Vue 的源码学习。

因为笔者刚开始 Element-ui 的源码学习不是很久,本文会从 <el-alert> 这个组件开始,并且会以一个小白的角度尽可能地把每一个细节都抠出来,所以在大佬看来可能会觉得很简单,文章可能不适合大佬食用~

# 写在前面

笔者的 Element-ui 源码的解析不会涉及它的 css 样式,这里我在项目中下载了 Element-ui,只引入了它的样式,其余的部分由自己来写,然后将组件引入到自己的页面中。

我会更加的专注于其中应用到的 Vue 语法、代码风格以及其中的小技巧,以达到更好的加强自己的 Vue 和 JS 功底,为将来解构 Vue 的源码做一个缓冲~

在开始之前看看效果

el-alert效果图

组件设计的精髓中复用性是不可不提的,Element-ui 的 <el-alert> 组件可以说是复用性能很好,能够适用于大部分的消息提示场景。接下来我们就来解构它的源码,通过源码来加强对 Vue 的一些学习。

# 结构设计

组件都是有一个通用的结构,通过 Vue 的动态绑定和 slot 等等语法达到千人千面,实现组件复用的效果,我们先来解析它的模板结构

<template>
  <div>
    <!-- ElAlert -->
    <transition name="el-alert-fade">
      <div
        class="el-alert"
        :class="[ typeClass, center ? 'is-center' : '', 'is-' + effect ]"
        v-show="visible"
        >
        <i class="el-alert__icon" :class="[ iconClass, isBigIcon ]" v-if="showIcon"></i>
        <div class="el-alert__content">
          <span class="el-alert__title" :class="[ isBoldTitle ]" v-if="title || $slots.title">
            <!-- {{title}} -->
            <!-- 属性和具名插槽两种方式显示 -->
            <slot name="title">{{title}}</slot>
          </span>
          <!-- 通过 slot:default 显示 description -->
          <p class="el-alert__description" v-if="$slots.default && !description"><slot></slot></p>
          <!-- 通过属性显示 description -->
          <p class="el-alert__description" v-if="!$slots.default && description">{{ description }}</p>
          <!-- <p class="el-alert__description" v-if="description || $slots.default">{{ description }}</p> -->
          <!-- 关闭按钮 -->
          <i class="el-alert__closebtn" :class="{ 'is-customed' : closeText!== '', 'el-icon-close': closeText === '' }" v-show="closable" @click="close()">{{ closeText }}</i>
        </div>  
      </div>
    </transition>
  </div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

在代码之中的一些可能会有点复杂的地方我也写上了详细的注释,下面我们一起来看看能够从这里能学到些什么东西。

# 代码风格

首先第一个,抛开所有的语法,我们可以学到的就是一个代码的书写习惯和风格,要适当的用空格和回车,可以使代码更加的清晰,对自己和别人都更友好

其次就是类名的命名规范,尽可能的清晰明了,Element-ui 的源码中 css 方面使用了 BEM 的命名规范---Block(块)、Element(元素)、Modifier(修饰符)这样有助于我们更好的去理解每个类名的作用。

# 动态类名

这里用到了比较多的动态类名,平时我们可能写动态类名更多的是直接使用 :class="" 这样的一个形式,但是这里有几个地方可能对于新手来说有点疑惑的地方,比如:
:class="[ typeClass, center ? 'is-center' : '', 'is-' + effect ]"
:class="[ isBoldTitle ]" v-if="title || $slots.title"

这两个地方的动态类名使用了 [] 和 {},原因是因为要绑定多个动态类名,并且可能要对类名进行一定的修改和根据命名规范格式化,使用 [] 和 {} 可以一次性的做完这些事情,更加的方便。这在我们平时写 demo 之类的情境下可能用的会稍微少一点,所以我也拎出来讲一下。

在 Element-ui 中动态类名还有一种巧妙的用法,后面我会讲到。

# 提供多种选择

Element-ui 之所以这么受欢迎,在看完源码之后我觉得除了它很好用之外,还有一点就是它对于用户比较友好,给用户提供了多种选择。我们从源码这两个地方来看看:

<span class="el-alert__title" :class="[ isBoldTitle ]" v-if="title || $slots.title">
  <!-- 属性和具名插槽两种方式显示 -->
  <slot name="title">{{title}}</slot>
</span>
1
2
3
4
<!-- 通过 slot:default 显示 description -->
<p class="el-alert__description" v-if="$slots.default && !description"><slot></slot></p>
<!-- 通过属性显示 description -->
<p class="el-alert__description" v-if="!$slots.default && description">{{ description }}</p>

1
2
3
4
5

这里我在代码中也给出了详细注释,title 和 description 通过 v-if 的判断,可以实现属性传值和插槽填充两种方式显示到页面上,这对于使用它的人来说比较便利。

可能有人看到了我在 desctiption 的显示后面有一段注释的代码:

<!-- <p class="el-alert__description" v-if="description || $slots.default">{{ description }}</p> -->
1

这段代码是我在看到 title 的显示方式控制只用一个 v-if 判断就做到了,所以想自己尝试也只用一个 v-if 来实现效果,可惜的是这条语句并不能达到想要的效果,原因是 description 的插槽是默认插槽,通过 $slots.default 并不能判断到它,但是如果你换成下面这样的格式就可以实现了:

<p class="el-alert__description" v-if="description || $slots.description"><slot name="description">{{ description }}</slot></p>
1

这样虽然可以实现效果,但是你增加了一个具名插槽,Element-ui 官方不这么做肯定是有自己相应的考虑吧。

这里是更正: 笔者在上面说想尝试 description 只用一个 v-if 无法实现,经过评论区的大佬指点,才发现原来是自己忘记在 的外面套上一个 <slot> 标签,其实用一个 v-if 是可以实现的:

<p class="el-alert__description" v-if="description || $slots.default"><slot>{{ description }}</slot></p>
1

还是那句话,官方没有这么做应该是有自己的原因的,不过我们自己在写的时候可以进行一些改变,不能只会是照搬。

我没有把上面那段直接删掉的原因是我想把自己的问题暴露出来,警示自己以后更加的仔细谨慎,以后不再犯同样的错误。欢迎大家一起监督~

# 数据设计

看完了结构,下面来看数据和方法部分的设计。老规矩,先上代码看看:

<script>
// 常量 
const TYPE_CLASS_MAP = {
  'success': 'el-icon-success',
  'warning': 'el-icon-warning',
  'error': 'el-icon-error'
}
export default {
  name: 'ElAlert',
  props: {
    type: { // 类型      
      type: String,
      default: 'info' // 默认值
    },
    title: { // 标题      
      type: String,
      default: ''
    },
    description: {
      type: String,
      default: ''
    },
    center: Boolean,
    effect: { // 提供的主题      
      type: String,
      default: 'light',
      validator: function(value) { // 验证 effect 的值必须二选一
        // 其中之一        
        return ['light', 'dark'].indexOf(value) !== -1;
      }
    },
    showIcon: Boolean,
    closeText: {
      type: String,
      default: ''
    },
    closable: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      visible: true
    }
  },
  computed: {
    // 计算属性
    typeClass() {
      return `el-alert--${ this.type }`
    },
    iconClass() {
      return TYPE_CLASS_MAP[this.type] || 'el-icon-info'
    },
    isBigIcon() {
      return this.desciption || this.$slots.default ? 'is-big' : ''
    },
    isBoldTitle() {
      return this.description || this.$slots.default ? 'is-bold' : ''
    }
  },
  methods: {
    close() {
      this.visible = false;
      // 给父组件传递一个 close 方法,便于在用户关闭提示的时候进行一些操作      
      this.$emit('close')
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

# computed 计算类名

先来补上之前留下的动态类名的坑,在这里有四个类名是通过 computed 属性来动态确定的:
typeClassiconClassisBigIconisBoldTitle
computed 属性的使用场景大致是下面这样:

  1. 某个数据受多个数据影响
  2. 需要对其他数据进行 JS 处理再显示

使用 computed 来计算类名的好处是可以不用在 data 中添加相应的数据,并且可以根据实际情况来改变类名的形式,进行动态改变或是根据规范格式化。当然这里不能直接放在 :class 中,因为判断比较复杂,而且 :class 也无法完全满足这里的需求。

# 数据的巧妙设计

在这里的数据有一个比较巧妙地设计,那就是常量。

// 常量 
const TYPE_CLASS_MAP = {
  'success': 'el-icon-success',
  'warning': 'el-icon-warning',
  'error': 'el-icon-error'
}
1
2
3
4
5
6

因为这里 icon 地类名是固定不变的,它是一个可遍历的同一类型的数据,并且要在多个地方使用,在使用过程中也不会去改变它,所以可以考虑作为常量来使用。这里和大文件上传过程中将文件上传状态作为常量对象来使用是同样的道理,可以加强代码的可维护性,做到一改全改。

# props 接收验证

我们在业务要求不是很高的情况下父子组件用 props 通信传值,一般是写成数组的格式,而这里使用了对象的形式,在这里使用对象形式可以实现这些方面的功能:

  1. 声明类型进行检验
  2. 声明 validate 方法进行检验
methods: {
  close() {
    this.visible = false;
    // 给父组件传递一个 close 方法,便于在用户关闭提示的时候进行一些操作
    this.$emit('close')
  }
}
1
2
3
4
5
6
7

在这里的数据源中还设置了一个 visible 属性,方便用户可以关闭这个提示框组件。这里方法的设计考虑的也是很全面,在点击关闭的时候使用 $emit 给父组件提交一个 close 事件,父组件就可以通过 @close 进行绑定一个事件,实现用户在关闭提示的时候进行一些业务操作。

# 注意的点

在自己写 Element-ui 组件的时候如果是和我一样在 App.vue 中使用一定要注意把 #app 的样式去掉,不然会有样式冲突导致样式显示不正确。

# 写在最后

正所谓不看源码的程序员不是好程序员,如果感觉自己达到了一个瓶颈阶段,看源码是一个很好的选择。本文可能对于很多人来说可能比较的基础,技术含量也不是很高,但是我会不断的深入 Elment-ui 的源码学习,把其中更多的知识点挖掘出来,以便也能使自己有所提升。这个系列我也会持续的更新下去,如果你觉得对你有帮助的话,欢迎点赞和在评论区和我交流~ 我把我的学习记录都记录在了我的 github 并且会持续的更新下去,有兴趣的小伙伴可以看看~

github

Last Updated: 5/6/2020, 11:48:16 AM