IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> Element-ui源码分析 -> 正文阅读

[JavaScript知识库]Element-ui源码分析

分析Element-ui封装思想

在平时写业务或者是写玩具的时候为了方便,我们会使用各种各样的组件库。虽然说基本需求看文档就可以了,但是文档中提供的方法和业务需求相比肯定是有一定差距的,这时候就需要自己封装组件了;并且,在写了一些代码后感觉,其实在不同的项目中写过功能差不多相同的代码,那为什么不封装一下方便以后、或者是其他人使用呢?

写这篇博客的时候非常感谢b站up主樱满空,他的分析非常的清晰!可以去看看这位up主的视频讲解

https://space.bilibili.com/1842032?spm_id_from=333.788.b_765f7570696e666f.2

文章内容会不断的更新,每一节内容分为

  • props属性分析
  • 样式分析
  • 重新封装

希望可以在明年寒假开学前完成。

目录结构分析

假设现在你已经在项目中安装了element-ui,此时打开node_modules目录往下翻,可以看到一个名为element-ui的文件夹。
在这里插入图片描述

  • lib文件夹存放element-ui打包后的文件,也就是项目实际依赖了的文件
  • packages文件夹存放组件相关的源代码,也是之后源码分析的主要目标。
  • src文件夹存放了如指令、混入、工具方法等源代码
  • types文件夹存放了ts的类型声明文件,方便引入 typescript 写的项目中,需要在 package.json 中指定 typing 字段的值为 声明的入口文件才能生效。

入口文件

在分析packegs文件夹中的各个组件源码之前,我们先看看src中的入口文件index.js。

// 部分引入
import Pagination from '../packages/pagination/index.js';
import Dialog from '../packages/dialog/index.js';
import Autocomplete from '../packages/autocomplete/index.js';
import Dropdown from '../packages/dropdown/index.js';
import DropdownMenu from '../packages/dropdown-menu/index.js';

// 将引入的文件名放在一个数组中
const components = [
  Pagination,
  Dialog,
  Autocomplete,
  Dropdown, 
  DropdownMenu
]

// Element暴露出去一个install函数,Element本身就是一个插件
const install = function(Vue, opts = {}) {
  locale.use(opts.locale);
  locale.i18n(opts.i18n);
  // 通过对组件使用forEach方法,将所有的组件进行注册
  components.forEach(component => {
    Vue.component(component.name, component);
  });
 
  Vue.use(InfiniteScroll);
  Vue.use(Loading.directive);

  Vue.prototype.$ELEMENT = {
    size: opts.size || '',
    zIndex: opts.zIndex || 2000
  };

  Vue.prototype.$loading = Loading.service;
  Vue.prototype.$msgbox = MessageBox;
  Vue.prototype.$alert = MessageBox.alert;
  Vue.prototype.$confirm = MessageBox.confirm;
  Vue.prototype.$prompt = MessageBox.prompt;
  Vue.prototype.$notify = Notification;
  Vue.prototype.$message = Message;
};

我们在调用Vue.use(ElementUI)注册时,本质上就是调用这个install函数。由于Vue.use接收一个对象,这个对象必须具有install方法,Vue.use函数内部会调用参数的install方法。如果插件没有被注册过,那么注册成功之后会给插件添加一个installed的属性值为true。Vue.use方法内部会检测插件的installed属性,从而避免重复注册插件。

插件的install方法将接收两个参数,第一个是参数是Vue,第二个参数是配置项options(就是这里的opts)对象。

Vue.prototype.$ELEMENT这一句来看,传入的参数可以有sizezIndex属性,size 用于改变组件的默认尺寸,zIndex 设置弹框的初始 z-index(默认值:2000)。我们可以手动向options中传入size和zInde,保存到Vue.prototype.$ELEMENT全局配置中,这样在组件中我们就可以根据size和zIndex进行不同组件尺寸的展示。

import Element from 'element-ui';
Vue.use(Element, { size: 'small', zIndex: 3000 });

在入口文件中我们可以通过forEach循环遍历进行大部分组件的注册,小部分如InfiniteScrollLoading在全局注册指令,通过v-infinite-scrollv-loading等指令式来调用;也有如msgboxalert等在全局Vue.prototype添加方法,可以通过函数进行调用。

组件分析

在这一节中我将按照Element-ui官网上的顺序逐一分析组件。

Layout布局

先看看基础布局里面提供的代码,对于Layout布局的部分,我们需要使用到el-row和el-col的嵌套子组件。

<el-row>
  <el-col :span="24"><div class="grid-content bg-purple-dark"></div></el-col>
</el-row>
<el-row>
  <el-col :span="12"><div class="grid-content bg-purple"></div></el-col>
  <el-col :span="12"><div class="grid-content bg-purple-light"></div></el-col>
</el-row>

el-row

  • 打开路径node_modules -> element-ui -> package -> el-row -> src -> row.js查看组件的逻辑和页面部分

  • 打开路径node_modules -> element-ui -> packages -> theme-chalk -> src -> row.scss查看el-row的样式部分

  • 查阅官方文档查看el-row有那些属性
    在这里插入图片描述
    和传统的vue文件中template模板不同的是,el-row组件是以渲染函数的方式编写的

export default {
  // 组件名
  name: 'ElRow',
  // 这个选项并非Vue官方提供的API,而是Element团队自定义的属性
  // 在查阅 Vue2 官方文档的时候可以看到,vm.$options的api可以用于当前Vue实例的初始化选项
  // 所有我们写的Vue选项都会放到Vue实例属性$options中
  // 比如之后可以通过this.$options.componentName获取到这里的属性值
  componentName: 'ElRow',
  props: {
    tag: {
      type: String,
      default: 'div'
    },
    gutter: Number,
    type: String,
    justify: {
      type: String,
      default: 'start'
    },
    align: String
  },

  computed: {
    style() {
      const ret = {};
      if (this.gutter) {
        ret.marginLeft = `-${this.gutter / 2}px`;
        ret.marginRight = ret.marginLeft;
      }
      return ret;
    }
  },

  render(h) {
    return h(this.tag, {
      class: [
        'el-row',
        this.justify !== 'start' ? `is-justify-${this.justify}` : '',
        this.align ? `is-align-${this.align}` : '',
        { 'el-row--flex': this.type === 'flex' }
      ],
      style: this.style
    }, this.$slots.default);
  }
};

props属性分析

  • tag:用来自定义元素标签,默认是div。我们可以看到tag属性用在了render函数中,render函数的参数h就是createElement函数的别名,也就是说默认情况下每个渲染出来的el-row是一个div。

  • type、justify、align:这三个属性都与flex布局相关,type属性可选flex布局,后面两个属性用于垂直水平的布局,在render函数中查看class,可以通过垂直水平的属性判断元素所在的位置。顺便介绍下render函数的最后一个参数,this.$slots.default用的是default插槽的内容,也就是在el-row标签中写的内容。

  • gutter:列间距。这个属性用在了computed中计算style,在我们手动传入了gutter的情况下,会给el-row左右两侧各添加一个gutter值除以2的负外边距。这么做是因为 el-col 的左右两侧都会添加一个gutter除以2的内边距。如果不追加这个负外边距的话会导致行的左右两侧也有间距,导致el-col无法和外层元素边缘对齐。

    至于为什么需要这样做,我们可以看看这个例子

<div class="box">
    <div class="son"></div>
    test
    <div class="son"></div>
</div>
<style>
    body{
            background-color: coral;
    }
    .box {
        width: 100%;
        height: 300px;
        background-color: aquamarine;
    }
    .son {
        height: 100px;
        background-color: black;
        
    }
</style>

在这里插入图片描述
现在为son设置一个margin-top:100px看看,可以明显的看到,父元素的box元素也被强制向下移动了100px
在这里插入图片描述
现在为父元素设置margin-top: -100px,整体元素就成功上移了。所以说在el-row中添加负外边距是为了保证子元素设置外边距时,不会影响整体行位置上的改变。
在这里插入图片描述

样式分析

打开row.scss文件

@import "common/var";
@import "mixins/mixins";
@import "mixins/utils";

@include b(row) {
  position: relative;
  box-sizing: border-box;
  @include utils-clearfix;

  @include m(flex) {
    display: flex;
    &:before,
    &:after {
      display: none;
    }

    @include when(justify-center) {
      justify-content: center;
    }
    @include when(justify-end) {
      justify-content: flex-end;
    }
    @include when(justify-space-between) {
      justify-content: space-between;
    }
    @include when(justify-space-around) {
      justify-content: space-around;
    }

    @include when(align-top) {
      align-items: flex-start;
    }

    @include when(align-middle) {
      align-items: center;
    }
    @include when(align-bottom) {
      align-items: flex-end;
    }
  }
}

在第四行中我们可以看到使用了@include指令,这个指令是搭配mixin使用的,现在它混入了一个名为b的mixin,并且传递了一个参数值为row,接下来通过@import "mixins/mixins"点进去查看,这个名为b的mixin做了什么工作。

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

  .#{$B} {
    @content;
  }
}

在这个混入中首先定义了一个$B的变量,值是namespace + ‘-’ + 传入的变量。这个namespace在config.scss中有定义

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

对于这些定义需要了解一下采用BEM的Class命名风格

  • 组件名放在最前面,比如el-row,el-col,el-input
  • 如果说是组件的子元素样式,会用组件名加上两个下划线再加上元素名,比如el-input__inner
  • 修饰符接在最后,用两个中横线与前面的隔开,比如el-button–primary
  • 表示状态的前缀 is-,比如表示禁用状态的样式is-disabled

现在我们回到b的mixin中,#{}是使用变量定义的意思,这里用$B的值定义了一个class,其中使用了@content来将我们使用混入时写在大括号中间的内容放到这个class中。

@include b(row) {
  position: relative;
  box-sizing: border-box;
  ...
}

最终渲染到页面上名为el-row的class中。

在这里插入图片描述
接着row.scss往下看,又include了一个混入 @include utils-clearfix,首部utils-名称表示我们需要到utils.scss中来看。

@mixin utils-clearfix {
  $selector: &;

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

从display:tabel和clear:both可以明显的看出来该方法用于清除浮动。

回到row.scss,发现存在一个名为m的混入@include m(flex) ,通过mixin.scss中我们可以看到,这个是生成修饰符class用的混入,使用@each遍历我们传入的修饰符,生成class名,然后拼接每个修饰符class,最后把给混入传递的内容放到这些class中。

@mixin m($modifier) {
  $selector: &;
  $currentSelector: "";
  @each $unit in $modifier {
    $currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ","};
  }

  @at-root {
    #{$currentSelector} {
      @content;
    }
  }
}

这里的@at-root是让后面的样式跳出目前层级到顶层,由于我们之前在el-row的class下调用m混入,所以默认会把这些样式添加父类选择器el-row。使用@at-root后,这些修饰符选择器就可以和el-row class平级了。
在这里插入图片描述
最后还剩下一堆when的混入,其实就是用来生成is-开头表示状态用的class

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

在这里插入图片描述

el-col

el-col和el-row一样,都是通过渲染函数编写的。
在这里插入图片描述

props属性分析

  • span表示列宽,默认情况下一列的宽度为24占一整行

  • offset、pull、push表示栅格在一行中的便宜位置

  • xs、sm、md、lg、xl用于表示响应式布局,他们可以接收Number或者是Object类型。接收Number类型的时候相当于对应画面大小时的span;接收Object类型时对象的键可以是span、offset、pull、push

在计算属性中有一个gutter属性,通过this.$parend获取到当前el-col的父组件,下面的while循环表示,在el-col的祖先结点中查找到离当前节点距离最近的el-row组件。如果找到了最近的el-col祖先组件,就返回父组件身上所绑定的gutter值,否则为0,Element经常使用这个方法去查找某个组件的最近祖先元素,比如el-from,el-form-item。

 computed: {
    gutter() {
      let parent = this.$parent;
      while (parent && parent.$options.componentName !== 'ElRow') {
        parent = parent.$parent;
      }
      return parent ? parent.gutter : 0;
    }
}

通过props属性以及计算属性,可以在render函数中渲染结点

render(h) {
    let classList = [];
    let style = {};

    if (this.gutter) {
      style.paddingLeft = this.gutter / 2 + 'px';
      style.paddingRight = style.paddingLeft;
    }

    ['span', 'offset', 'pull', 'push'].forEach(prop => {
      if (this[prop] || this[prop] === 0) {
        classList.push(
          prop !== 'span'
            ? `el-col-${prop}-${this[prop]}`
            : `el-col-${this[prop]}`
        );
      }
    });

    ['xs', 'sm', 'md', 'lg', 'xl'].forEach(size => {
      // 如果传递进来进行响应式处理的是具体的数字
      if (typeof this[size] === 'number') {
        classList.push(`el-col-${size}-${this[size]}`);
      } 
      // 如果传递进来的是一个数组
      else if (typeof this[size] === 'object') {
        let props = this[size];
        // 通过Object.keys()拿到键值对,其实感觉用for...in...会更方便
        Object.keys(props).forEach(prop => {
          classList.push(
            prop !== 'span'
              ? `el-col-${size}-${prop}-${props[prop]}`
              : `el-col-${size}-${props[prop]}`
          );
        });
      }
    });

    return h(this.tag, {
      class: ['el-col', classList],
      style
    }, this.$slots.default);
}

样式分析

在theme-chalk文件夹下找到col.scss文件,在文件的起始部分为每一个el-col开头的元素设置了左浮动和border-box属性

[class*="el-col-"] {
  float: left;
  box-sizing: border-box;
}

接下来通过一个循环从0-24设置span、offset、pull、push的样式

// span为0的样式会额外设置一个display:none
.el-col-0 {
  display: none;
}

@for $i from 0 through 24 {
  .el-col-#{$i} {
    width: (1 / 24 * $i * 100) * 1%;
  }

  .el-col-offset-#{$i} {
    margin-left: (1 / 24 * $i * 100) * 1%;
  }

  .el-col-pull-#{$i} {
    position: relative;
    right: (1 / 24 * $i * 100) * 1%;
  }

  .el-col-push-#{$i} {
    position: relative;
    left: (1 / 24 * $i * 100) * 1%;
  }
}

紧接着的会通过名为res的混入生成各个size的响应式布局样式

@include res(xs) {
  .el-col-xs-0 {
    display: none;
  }
  @for $i from 0 through 24 {
    .el-col-xs-#{$i} {
      width: (1 / 24 * $i * 100) * 1%;
    }
    ...
  }
}

我们来看一下这个名为res的混入,这个混入接收两个参数,$key表示传入响应式的key值(xs、sm等)

/* Break-points
 -------------------------- */
@mixin res($key, $map: $--breakpoints) {
  // 循环断点Map,如果存在则返回
  @if map-has-key($map, $key) {
    @media only screen and #{inspect(map-get($map, $key))} {
      @content;
    }
  } @else {
    @warn "Undefeined points: `#{$map}`";
  }
}

如果没有传入第二个参数 m a p , 会 使 用 默 认 值 map,会使用默认值 map使–breakpoints

/* Break-point
--------------------------*/
$--sm: 768px !default;
$--md: 992px !default;
$--lg: 1200px !default;
$--xl: 1920px !default;

$--breakpoints: (
  'xs' : (max-width: $--sm - 1),
  'sm' : (min-width: $--sm),
  'md' : (min-width: $--md),
  'lg' : (min-width: $--lg),
  'xl' : (min-width: $--xl)
);

回到res混入中,通过@if和 scss内置的函数方法map-has-key判断key值是否在 m a p 中 , 如 果 k e y 值 在 map中,如果key值在 mapkeymap中存在,就会生成一个媒体查询。这里的inspect也是scss的内置函数,用来将变量值转换为字符串形式;map-get函数用来获取map中key所对应的value值。

举个栗子,当我们使用

<el-col :xs="8" :sm="6" :md="4" :lg="3" :xl="1"><div class="grid-content bg-purple-light"></div></el-col>

就会分别生成max-width: 765px,min-width: 766px、992px、1200px、1920px的媒体查询,根据画面的不同展示不同的样式,他们对应每一列的宽度也会不一样。

重新封装

官方文档给定el-row的最大宽度有24列,现在如果将其扩展为48列应该如何操作呢?

在前面分析样式文件中,每一列的宽度都是在一个从1至24的循环中设置,最小列宽为 (1 / 24 * 1 * 100) * 1% = (1 / 24)%,如果想要扩展列数,我们只需要将循环的上限扩大为48,以及每一列改为 (1 / 48 * i * 100)* 1%即可。

更改

这样做似乎不行,在页面上显示的时候仍按照24的宽度,期待大佬分享正确改法。

思路的确是这样的,还记得最开始分析目录结构的时候介绍的lib文件夹吗,通过element-ui->lib->theme-chalk->col.css可以看到打包完成的element col的样式。
在这里插入图片描述可以明显的看到列的最大值为24,具体每一列的宽度也计算好了放在文件的后面。而我之前一直是在scss文件中去修改它的循环条件,计算结果最终却没有重新打包,即引用的是未经过打包的、没有修改的css文件,所以最终导致无法显示。也许你会想,那直接修改打包后的文件可以吗?答案是不行,vue项目中的node-module->element-ui文件夹中没有build文件夹。所以无法直接修改项目中的element-ui。

解决方法

首先将ElementUI的源码clone下来并安装依赖

git clone https://github.com/ElemeFE/element.git
cd element
npm install

然后在packages文件夹中去修改目标文件的源代码结构以及theme-chalk下的样式,修改完毕后执行npm run dist进行打包

使用dist打包原因来自官方文档:https://github.com/ElemeFE/element/blob/master/.github/CONTRIBUTING.zh-CN.md

打包结束会生成一个lib文件夹,将他替换掉项目中node_modules->element-ui下的lib文件夹即可(我之前使用的是element 12+,打包后lib文件夹中只有一个index.js,将版本回退到2.4.5的时候打包结果和项目中文件结构相同)

打包的过程中如果你的 node版本 ≥ 12.0 并且 gulp版本 < 4.0,会遇到下面的报错
在这里插入图片描述
面对版本冲突错误,我选择升级gulp的版本,方法可以看gulp官方文档:https://gulpjs.com/docs/en/getting-started/quick-start/

在stackoverflow上面看到了另外一种解决办法:https://stackoverflow.com/questions/55921442/how-to-fix-referenceerror-primordials-is-not-defined-in-node-js,通过在package.json文件里修改配置实现兼容

解决版本冲突后重新打包,将打包后的lib文件夹,替换掉项目中lib文件夹后重启项目,就可以正常使用了。

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-12-03 12:57:17  更:2021-12-03 12:57:28 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 7:49:12-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码