ElementUI 是一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库。

0、前言

老规矩,带着问题看源码:

  • 组件的两种使用方式是如何做的(Tag模式|API 模式)?
  • scss 文件是如何优雅组织的 ?

1、组件分析

3.1、基本结构

组件的共性分析:
1、结构都是类似的,以Alert为例:

  1. import Alert from './src/main'; // 单文件组件
  2. /* istanbul ignore next */
  3. Alert.install = function(Vue) {
  4. Vue.component(Alert.name, Alert);
  5. };
  6. export default Alert;

这样写的目的在于提供两种方式使用 Alert:

  1. import {Alert} from 'element-ui'
  2. // 方式1:直接使用(每次使用的时候都需要import)
  3. <alert>
  4. // 方式2:注册成全局组件(每次使用的时候不需要import)
  5. Vue.use(Alert)

2、单文件组件的结构
i、只写template和script部分,style部分单独在 theme-chalk 下,在使用按需加载组件时,会配合 babel-plugin-component 插件将style部分引入

  1. {
  2. "presets": [["es2015", { "modules": false }]],
  3. "plugins": [
  4. [
  5. "component",
  6. {
  7. "libraryName": "element-ui",
  8. "styleLibraryName": "theme-chalk"
  9. }
  10. ]
  11. ]
  12. }

ii、template 部分大都以 transition 组件包裹,其name属性指定过渡动画,过渡动画的css代码单独抽出在 theme-chalk/src/common/transition.scss,联合 icon.scss 组成两个所有组件都依赖的公共css文件。template中固定类名写在 class 上,动态类名写在 :class 上,且类名都以 computed 方式通过JS代码提供
iii、template 中较多地方使用 slot 实现内容的动态化
iiii、通过 this.$emit() 的方式向外部触发事件

3.2 组件实现分析

组件数量较多,这里仅做简要分析,详细分析请看 Element非官方分析

  • alert
    和 message-box 中的 alert 区别在于只能以组件的形式调用,不能以 this.$alert 方式调用
  • aside
    对 html5 中的 aside 标签使用默认 slot 进行了一次封装
  • autocomplete
  1. v-bind="[$props,$attrs]" // 这种写法可以将父组件的所有props和attrs绑定到子组件

指令v-clickoutside 的实现原理:将使用了该指令的组件收集到一个数组nodeList ,然后监听document 的 mouseup 事件,遍历nodeList,通过el与event.target来判断是否在外部点击,从而执行对应的处理函数

  • badge
    角标的布局使用了先 absolute 再 transform 的技巧,另外使用了 sup 这个语义化标签,而不是 span
  • breadcrumb
    需要与 breadcrumb-item 一起使用,使用了 provide / inject 机制将 breadcrumb 实例传给 breadcrumb-item
  • button
    使用了 inject 来兼容当作为 form 子元素使用时的情形
  • calendar
    由 button-group 作为 header , date-table 作为 content 组合而成,最麻烦的部分是计算每一行的日期
  • card
    比较简单,由具名slot 提供 header,默认slot部分作为 body
  • carousel
    需要与 carousel-item 一起使用,通过 this.ElementUI-源码分析2---组件篇 - 图1options.name === ‘ElCarouselItem’ 获取传入的 carousel-item 实例,计算 每个 item 的位移比较麻烦
  1. Vue.prototype.$loading = Loading.service;
  2. Vue.prototype.$msgbox = MessageBox;
  3. Vue.prototype.$alert = MessageBox.alert;
  4. Vue.prototype.$confirm = MessageBox.confirm;
  5. Vue.prototype.$prompt = MessageBox.prompt;
  6. Vue.prototype.$notify = Notification;
  7. Vue.prototype.$message = Message;
  • Loading
    loading 既可以以指令的形式使用,也可以用服务的方式,如果全局引入Element,则还可以用 this.$loading使用
  • MessageBox
    alert,confirm,prompt均由MessageBox 实现,实现:一个模板+消息队列,当同时调用多个alert时,只会创建一个模板,多个alert的文案等信息会存在消息队列中,本质上也是单例模式
  • Message
    分全局引入和局部引入两种方式调用
  1. // 全局
  2. this.$message({type:'success',message: '恭喜你,这是一条成功消息'})
  3. // 局部
  4. Message.success('恭喜你,这是一条成功消息')

当多次调用时,仍会创建多个实例

  • Notification
    和 Message 的实现基本一致,有个不同的地方在于组件销毁的处理:Message 是监听 after-leave 后销毁,Notification 是监听 transitionend 后销毁,这样做的原因在于Notification 可以手动关闭,而 Message 不能

2.4、样式分析

ElamentUI 以 BEM 的方式组织样式,以 Alert.vue 分析:

  1. <template>
  2. <transition name="el-alert-fade">
  3. <div
  4. class="el-alert"
  5. :class="[typeClass, center ? 'is-center' : '', 'is-' + effect]"
  6. v-show="visible"
  7. role="alert"
  8. >
  9. <i class="el-alert__icon" :class="[ iconClass, isBigIcon ]" v-if="showIcon"></i>
  10. <div class="el-alert__content">
  11. <span class="el-alert__title" :class="[ isBoldTitle ]" v-if="title || $slots.title">
  12. <slot name="title">{{ title }}</slot>
  13. </span>
  14. <p class="el-alert__description" v-if="$slots.default && !description"><slot></slot></p>
  15. <p class="el-alert__description" v-if="description && !$slots.default">{{ description }}</p>
  16. <i class="el-alert__closebtn" :class="{ 'is-customed': closeText !== '', 'el-icon-close': closeText === '' }" v-show="closable" @click="close()">{{closeText}}</i>
  17. </div>
  18. </div>
  19. </transition>
  20. </template>

再看对应的 alert.scss:

  1. // 省略了部分
  2. @import "mixins/mixins";
  3. @import "common/var";
  4. @include b(alert) {
  5. width: 100%;
  6. padding: $--alert-padding;
  7. margin: 0;
  8. box-sizing: border-box;
  9. border-radius: $--alert-border-radius;
  10. position: relative;
  11. background-color: $--color-white;
  12. overflow: hidden;
  13. opacity: 1;
  14. display: flex;
  15. align-items: center;
  16. transition: opacity .2s;
  17. @include when(center) {
  18. justify-content: center;
  19. }
  20. @include m(success) {
  21. &.is-light {
  22. background-color: $--alert-success-color;
  23. color: $--color-success;
  24. .el-alert__description {
  25. color: $--color-success;
  26. }
  27. }
  28. &.is-dark {
  29. background-color: $--color-success;
  30. color: $--color-white;
  31. }
  32. }
  33. @include e(closebtn) {
  34. font-size: $--alert-close-font-size;
  35. opacity: 1;
  36. position: absolute;
  37. top: 12px;
  38. right: 15px;
  39. cursor: pointer;
  40. @include when(customed) {
  41. font-style: normal;
  42. font-size: $--alert-close-customed-font-size;
  43. top: 9px;
  44. }
  45. }
  46. }

其中的 b,e,m,when都是定义好的 mixin;这种实现让代码显得异常整洁。且大部分组件都是这种模式,维护起来也是非常方便,值得借鉴!

3、小结 & 收获

小结

回答开始的问题

  • 组件的两种使用方式是如何做的(Tag模式|API 模式)?
    通过 .vue 形式提供 tag 模式调用,通过将构造函数挂在Vue原型上实现 API 模式调用,这种方式本质上也利用了 .vue 文件,只不过通过 Vue.extend() 方法做了一次内容提取,然后搞些单例模式处理实例等等。
  • scss 文件是如何优雅组织的 ?
    var.scss 命名所有变量;mixin.scss 定义通用 @mixin;icon.scss 存放所有icon;common.scss 定义通用 scss;其他一个组件对应一个scss。优雅的地方在于 mixin 的巧妙设计,如 @mixin b/e/m/when 等

收获

  • 组件的设计技巧
  • scss 的高级用法和文件组织技巧