通过分析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🥰

回复

我来回复
  • 暂无回复内容