通过分析ElementUI的源码,我学到了这些知识点(上)

吐槽君 分类:javascript

前言

本例以element-ui@2.14.0作为参考,从一些常用的组件中根据其中涉及的Vue知识点为大家分析其在实际开发中的作用。

目录结构介绍

element-ui.png
element-ui/lib存放webpack打包之后的文件。
element-ui/packages存放element-ui提供的所有组件。
src中提供了一些全局需要的工具JS方法、混入、指令等(后面会挑其中一个或几个进行选讲)。
element-ui/transitions提供的是element-ui所提供的一个全局的动画组件。
element-ui/utils提供了一些常用的工具方法(后面会挑一个或几个进行选讲)。

1、CSS与SCSS

element-ui使用SCSS构建的,其中每个组件的CSS分别位于packages下面的theme-chalk下,lib是打包编译之后的结果,src则是源码。除开公共scss文件和index.scss,每个scss文件名称则对应着相应的组件。在index.scss中引入了这些组件scss文件。

1.1 config.scss

进入到element-ui/packages/theme-chalk/src/mixins/config.scss中,我们可以看到如下内容:

$namespace: 'el';
$element-separator: '__';
$modifier-separator: '--';
$state-prefix: 'is-';
 

这儿引入一个概念叫做CSS BEM。BEM的意思就是块(block)、元素(element)、修饰符(modifier),是由Yandex团队提出的一种CSS Class 命名方法。通过阅读Element-UI的源码,我了解到了BEM的基本概念与使用场景。

1.2、mixins.scss

进入到element-ui/packages/theme-chalk/src/mixins/mixins.scss中,我们可以看到这个文件定义了组件常用的一些混入。大家熟知的混入在这儿就不贴出来了,我们来看几个关键的混入:

@import "function";
/*已省略部分代码*/

/* BEM
 -------------------------- */
 //块混入
@mixin b($block) {
  $B: $namespace+'-'+$block !global;

  .#{$B} {
    @content;
  }
}
//元素混入
@mixin e($element) {
  $E: $element !global;
  $selector: &;
  $currentSelector: "";
  @each $unit in $element {
    $currentSelector: #{$currentSelector + "." + $B + $element-separator + $unit + ","};
  }

  @if hitAllSpecialNestRule($selector) {
    @at-root {
      #{$selector} {
        #{$currentSelector} {
          @content;
        }
      }
    }
  } @else {
    @at-root {
      #{$currentSelector} {
        @content;
      }
    }
  }
}
//修饰符混入
@mixin m($modifier) {
  $selector: &;
  $currentSelector: "";
  @each $unit in $modifier {
    $currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ","};
  }

  @at-root {
    #{$currentSelector} {
      @content;
    }
  }
}
//状态混入
@mixin when($state) {
  @at-root {
    &.#{$state-prefix + $state} {
      @content;
    }
  }
}
//伪类混入
@mixin pseudo($pseudo) {
  @at-root #{&}#{':#{$pseudo}'} {
    @content
  }
}
 

笔者有幸在知乎查到了ElementUI团队对于这段代码的设计思路,因此此处不再赘述,详情。
我在这之前对SASS的使用主要停留在嵌套语法的使用,最多不过再使用SASS的mixin、插值,&简写等语法特性而已。通过研读这些代码,我学到了SCSS的高级使用,主要知识点包括:使用@at-root指令可以使得嵌套的SASS代码生成到文档的最顶层,在保持代码原有的可读性的基础上,并不降低浏览器的解析速度, @content指定了额外导入内容的插入位置。在同路径的_button.scss,笔者比较好奇,我通过检索发现了element-ui都是使用的是mixins/button.scss,于是我查阅SASS的文档得知其使用了另一个特性叫做“分音”,具体解释来源与SASS的文档:

如果需要导入 SCSS 或者 Sass 文件,但又不希望将其编译为 CSS,只需要在文件名前添加下划线,这样会告诉 Sass 不要编译这些文件,但导入语句中却不需要添加下划线。

当我们具备这些知识点以后,我们开始尝试用这套规则来编写自己的css代码

//假设我配置的命名空间是my
@include b(app) {
    display: flex;
    @include e(header) {
        height: 200px;
        width: 100%;
    }
    
    @include e(body) {
       height: 1000px;
       width: 100%;
    }
    
    @include m(mobile) {
        width:100%
    }
    @include m(pc) {
            width: 1200px;
            margin:0px auto;
    }
    @include when(error) {
        border: 1px solid red;
    }
    
    @include pseudo(before){
        content: '';
        width: 10px;
        height: 10px;
    }
}
 

生成的代码如下:

.my-app {
   display: flex;  
}
.my-app__header{
   height: 200px;
   width: 100%;
}

.my-app.is-error{
    border: 1xp solid red;
}

.my-app--mobile{
      width:100%
}

.my-app--pc{
     width: 1200px;
     margin:0px auto;
}

.my-app__body{
     height: 1000px;
      width: 100%;
}
 

这样写的好处是我们的样式都是生成在根目录的,在保证SCSS在代码层面具备层级结构的同时,又兼顾了浏览器的解析性能,实在堪称是一个较好的CSS组织方式。从在ElementUI团队发布在知乎的文章可以看出,其在设计mixin的时候已经考虑了很多复杂的边界情况,因此我们此时直接实施拿来主义,哈哈哈,妈妈以后也再也不用担心我的CSS了。
我在实际开发中对于CSS一个最大的困惑就是由于对CSS缺乏合理的组织,所以CSS写的比较乱(可能是我不太注重于CSS能力的培养所导致),对于一些相似的CSS可能会编写多次,代码冗余,相信在以后将会对我的CSS编码风格有非常大的提升。

2、broadcast和dispatch

broadcast这个语法特性存在与Vue1.x的版本中,在Vue2中是被废弃的一个语法特性,但是有些时候我们确实有这样的一个需求,需要对我们的组件树进行广播事件,此时我们就需要利用broadcast这样的语法特性。
ElementUI实现这样一个mixin,位于element-ui/src/mixins/emitter.js中,代码如下:

function broadcast(componentName, eventName, params) {
  this.$children.forEach(child => {
    var name = child.$options.componentName;

    if (name === componentName) {
      child.$emit.apply(child, [eventName].concat(params));
    } else {
      broadcast.apply(child, [componentName, eventName].concat([params]));
    }
  });
}
export default {
  methods: {
      //向上级组件广播事件
    dispatch(componentName, eventName, params) {
      var parent = this.$parent || this.$root;
      var name = parent.$options.componentName;
    
      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent;

        if (parent) {
          name = parent.$options.componentName;
        }
      }
      if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params));
      }
    },
    //向子组件广播特征事件
    broadcast(componentName, eventName, params) {
      broadcast.call(this, componentName, eventName, params);
    }
  }
};
 

这儿追加的两个VUE知识点是$options、$children和$parent,$options在VUE官方文档中的释义:

用于当前 Vue 实例的初始化选项。需要在选项中包含自定义 property 时会有用处

因此我们在使用的时候,只需要提供componentName即可。
$parent在VUE官方文档中的释义:

父实例,如果当前实例有的话。

$children在VUE官方文档中的释义:

当前实例的直接子组件。需要注意 $children 并不保证顺序,也不是响应式的。如果你发现自己正在尝试使用 $children 来进行数据绑定,考虑使用一个数组配合 v-for 来生成子组件,并且使用 Array 作为真正的来源。

举个栗子?:

<script>
  //已省略无关代码
  import emitter from 'element-ui/src/mixins/emitter';
  export default {
    name: 'ElInput',
    componentName: 'ElInput',
    mixins: [emitter],
    created() {
      //监听来自外界的事件
      this.$on('inputSelect', this.select);
    },
  }
 

在需要使用这样的通信方式的时候,我们只需要混入Emitter即可。在实际开发中我个人建议大家不要使用$children和$parent直接修改子组件或者父组件方法或属性,因为这样会形成强耦合,如果我们在模板中组件结构一但进行调整,几乎100%会造成我们的代码修改。因此对于组件的通信方式,读者可以参考论坛采用推荐的方法。但是这个方式对于我们在实现自己的组件库的时候,有一定的参考价值。

3、inject与provide

provide和inject在VUE官方文档中的释义如下:

provide 选项允许我们指定我们想要提供给后代组件的数据/方法。然后在任何后代组件里,我们都可以使用 inject 选项来接收指定的我们想要添加在这个实例上的。
优点:后代组件不需要知道被注入的 property 来自哪里,祖先组件不需要知道哪些后代组件使用它提供的 property。
缺点:它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的 property 是非响应式的。

我在实际开发中使用最多的方式便是使用provide和inject进行组件通信。通过在上层组件直接注入组件的引用(不管你是否响应式,对程序并没有实质性的影响)相比较于$children和$parent,在设计的时候总会考虑到组件的层级关系,因为不至于因为组件的嵌套层级变深而导致代码的修改,因此具有一定的实际价值。
在ElementUI中,出现较多的便是在Form和FormItem以及FormItem中的输入控件。

<script>
   //已省略非必要代码
  export default {
    name: 'ElForm',

    componentName: 'ElForm',

    provide() {
      return {
        elForm: this
      };
    },
  }
 
<script>
   //已省略非必要代码
  import AsyncValidator from 'async-validator';
  export default {
    name: 'ElFormItem',

    componentName: 'ElFormItem',

    mixins: [emitter],

    provide() {
      return {
        elFormItem: this
      };
    },

    inject: ['elForm'],
    methods: {
      validate(trigger, callback = noop) {
        this.validateDisabled = false;
        const rules = this.getFilteredRule(trigger);
        if ((!rules || rules.length === 0) && this.required === undefined) {
          callback();
          return true;
        }

        this.validateState = 'validating';

        const descriptor = {};
        if (rules && rules.length > 0) {
          rules.forEach(rule => {
            delete rule.trigger;
          });
        }
        descriptor[this.prop] = rules;

        const validator = new AsyncValidator(descriptor);
        const model = {};

        model[this.prop] = this.fieldValue;

        validator.validate(model, { firstFields: true }, (errors, invalidFields) => {
          this.validateState = !errors ? 'success' : 'error';
          this.validateMessage = errors ? errors[0].message : '';

          callback(this.validateMessage, invalidFields);
          this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null);
        });
      },
     }
}
 

这儿我以ElInput举例:

<script>
   //已省略非必要代码
  export default {
    name: 'ElInput',

    componentName: 'ElInput',

    inheritAttrs: false,

    inject: {
      elForm: {
        default: ''
      },
      elFormItem: {
        default: ''
      }
    },
    computed: {
      _elFormItemSize() {
        return (this.elFormItem || {}).elFormItemSize;
      },
      validateState() {
        return this.elFormItem ? this.elFormItem.validateState : '';
      },
      needStatusIcon() {
        return this.elForm ? this.elForm.statusIcon : false;
      },
    }
  }
</script>
 

在这样的使用场景下,依赖注入的组件因为不需要额外关联外部依赖(如外部props或Vuex亦或EventBus),最多就是加入对依赖注入数据的判断(只要外部提供的依赖我就能正常运行,如果你提供的依赖不正确,我当前组件不对外提供此功能,但并不影响我整体的运行),对于当前组件的影响比较小。在恰当的时机使用,不失为一种优雅的组件间通信方式。

4、总结

本文通过ElementUI阐述了一些SCSS的一些高级用法以及CSS BEM和Vue的API常见用法,由于其涵盖的知识点多,如果放在一篇进行阐述的话,可能会造成内容过于冗长,枯燥乏味。我将会在下篇对另一些涉及的知识点进行分析介绍。由于笔者水平有限,写作过程中难免出现错误,若有纰漏,请各位读者指正,你们的意见将会帮助我更好的进步。本文乃笔者原创,若转载,请联系作者本人,邮箱404189928@qq.com?

回复

我来回复
  • 暂无回复内容