Element源碼分析系列1一Layout(佈局)

動機

感受如今的業務開發,若是不是很特殊的需求,基本都能在對應的組件庫內找到組件使用,這樣編寫代碼就成了調用組件,可是卻隱藏了組件內的思想,所以弱化了編程能力,因此我想寫這麼個分析系列來鞭策本身深刻分析組件的原理,提升代碼閱讀理解能力,我以爲必定要記下點什麼來,若是隻是看不動筆感受很快就忘了,所以準備持續寫這麼個分析
css

Element源碼結構

官網傳送門點此, 主要目錄以下圖html

其中組件的源碼放在 package目錄下, src中是一些工具函數(某些組件都會使用這些函數)和國際化相關的代碼,進入 package目錄裏,則是全部組件的源碼

注意這些文件夾裏只包含js或者vue,而全部組件的樣式文件在最下面的 theme-chalk文件夾裏,整個項目結構仍是很清晰

Layout(佈局)源碼分析

  • <el-row>源碼分析

首先進入打開官網查看Layout相關部分的說明,發現主要的組件就2個: el-row,el-col,這2個分別表明行的容器和裏面列的容器,相似於bootstrapcolrow,首先咱們查看el-row的實現,進入package裏面的row文件夾,裏面是一個src文件夾和index.js文件vue

打開 index.js,這裏最後一句導出 Row供咱們 import,而中間的 install方法則是把這個組件當成一個Vue的插件來使用,經過 Vue.use()來使用該組件,install方法傳遞一個Vue的構造器,Element的全部組件都是一個對象{...},裏面有個 render函數來建立組件的html結構, render方法的好處很大,使得建立html模板的代碼更加簡潔高效,而不是冗長的各類div標籤堆疊,更相似於一種配置形式來建立html. 最後經過 export default導出,而不是經常使用的單文件組件形式,所以必須提供install方法

import Row from './src/row';
/* istanbul ignore next */
Row.install = function(Vue) {
  //全局註冊該組件(經常使用的組件最好全局註冊)
  Vue.component(Row.name, Row);
};
export default Row;
複製代碼

這裏其實有2種方法使用組件,一是當作插件,而是直接import後註冊組件,官網示例代碼以下,也能夠不註冊成全局組件git

import Vue from 'vue';
import { Button, Select } from 'element-ui';
import App from './App.vue';
Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
/* 或寫爲 * Vue.use(Button) * Vue.use(Select) */
new Vue({
  el: '#app',
  render: h => h(App)
});
複製代碼

下面進入src/row.js中一探究竟,首先代碼的總體結構以下,直接導出一個對象,裏面是組件的各類配置項github

export default {
    ...
}
複製代碼

整個組件的代碼量很少,下面是給出了詳細註釋express

export default {
  //組件名稱,注意是駝峯命名法,這使得實際使用組件時短橫線鏈接法<el-row>和駝峯法<ElRow>均可以使用
  name: 'ElRow',
  //自定義屬性(該屬性不是component必需屬性),重要,用於後面<el-col>不斷向父級查找該組件
  componentName: 'ElRow',
  //組件的props
  props: {
    //組件渲染成html的實際標籤,默認是div
    tag: {
      type: String,
      default: 'div'
    },
    //該組件的裏面的<el-col>組件的間隔
    gutter: Number,
    /* 組件是不是flex佈局,將 type 屬性賦值爲 'flex',能夠啓用 flex 佈局,
    *  並可經過 justify 屬性來指定 start, center, end, space-between, space-around
    *  其中的值來定義子元素的排版方式。
    */ 
    type: String,
    //flex佈局的justify屬性
    justify: {
      type: String,
      default: 'start'
    },
    //flex佈局的align屬性
    align: {
      type: String,
      default: 'top'
    }
  },

  computed: {
    //row的左右margin,用於抵消col的padding,後面詳細解釋,注意是計算屬性,這裏經過gutter計算出實際margin
    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 !== 'top' ? `is-align-${this.align}` : '',
        { 'el-row--flex': this.type === 'flex' }
      ],
      style: this.style
    }, this.$slots.default);
  }
};
複製代碼

下面說一下計算屬性裏面的sytle(),這裏面經過gutter屬性計算出了本組件的左右margin,且爲負數,這裏有點費解,下面上圖解釋,首先gutter的做用是讓row裏面的col產生出間隔來,可是注意容器的最左和最右側是沒有間隔的編程

上圖就是最終示意圖,黑框就是 <el-row>的寬度範圍,裏面是 <el-col>組件,下一節介紹, 這個組件的寬度其實按 <el-row>百分比來計算,並且 box-sizingborder-box,注意 gutter屬性是定義在父級的 <el-row>上,子級的col經過 $parent能夠拿到該屬性,而後給 <el-col>分配 padding-leftpadding-right,所以每一個col都有左右padding,上圖中每一個col佔寬25%,gutter的寬度就是col的padding的2倍,可是注意最左側和最右側是沒有padding的,那麼問題來了,怎麼消去最左和最右的padding? 這裏就是 <el-row>負的margin起的做用,若是不設置上面的計算屬性的style,那麼左右2側就會有col的padding,所以這裏負的margin抵消了col的padding,且該值爲 -gutter/2+'px'

注意若是初看上面的圖,通常的想法是col之間用margin來間隔,實際上是不行的,而用padding來間隔就很簡單,width按百分比來分配就行(box-sizing要設置爲border-box)

element-ui

下面解釋下最後返回的渲染函數render,這個函數有3個參數,第一個參數是html的tag名稱(最終在網頁中顯示的標籤名),第二個參數是一個包含模板相關屬性的數據對象,裏面有至關多模板相關的屬性,以下bootstrap

{
  // 和`v-bind:class`同樣的 API
  // 接收一個字符串、對象或字符串和對象組成的數組
  'class': {
    foo: true,
    bar: false
  },
  // 和`v-bind:style`同樣的 API
  // 接收一個字符串、對象或對象組成的數組
  style: {
    color: 'red',
    fontSize: '14px'
  },
  // 正常的 HTML 特性
  attrs: {
    id: 'foo'
  },
  // 組件 props
  props: {
    myProp: 'bar'
  },
  // DOM 屬性
  domProps: {
    innerHTML: 'baz'
  },
  // 事件監聽器基於 `on`
  // 因此再也不支持如 `v-on:keyup.enter` 修飾器
  // 須要手動匹配 keyCode。
  on: {
    click: this.clickHandler
  },
  // 僅對於組件,用於監聽原生事件,而不是組件內部使用
  // `vm.$emit` 觸發的事件。
  nativeOn: {
    click: this.nativeClickHandler
  },
  // 自定義指令。注意,你沒法對 `binding` 中的 `oldValue`
  // 賦值,由於 Vue 已經自動爲你進行了同步。
  directives: [
    {
      name: 'my-custom-directive',
      value: '2',
      expression: '1 + 1',
      arg: 'foo',
      modifiers: {
        bar: true
      }
    }
  ],
  // 做用域插槽格式
  // { name: props => VNode | Array<VNode> }
  scopedSlots: {
    default: props => createElement('span', props.text)
  },
  // 若是組件是其餘組件的子組件,需爲插槽指定名稱
  slot: 'name-of-slot',
  // 其餘特殊頂層屬性
  key: 'myKey',
  ref: 'myRef'
}
複製代碼

尤爲注意第三個參數,它表明子節點,是一個String或者Array,當是String時表明文本節點的內容,此時這就是個文本節點,若是是Array,裏面就是子節點,數組中每一個值都是一個render的參數函數數組

[
    //文本節點
    '先寫一些文字',
    createElement('h1', '一則頭條'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
]
複製代碼

再看上面render函數的第三個參數是this.$slots.default,這裏的意思就是獲取該組件下面不是具名插槽的內容,default 屬性包括了全部沒有被包含在具名插槽中的節點,對於以下代碼,該render函數就會把<el-row>以及<h1>test<h1>做爲其子節點一塊兒渲染出來

<el-row>
    <h1>test<h1>
    <slot name='t'>t1</slot>
</el-row>
複製代碼

最後解釋下樣式相關代碼,row.scss的路徑是packages/theme-chalk/src/row.scss,代碼是scss類型,render裏的class以下

class:[
        'el-row',
        this.justify !== 'start' ? `is-justify-${this.justify}` : '',
        this.align !== 'top' ? `is-align-${this.align}` : '',
        { 'el-row--flex': this.type === 'flex' }
      ],
複製代碼

這裏的el-row類其實沒有定義,能夠本身在寫代碼時補充,官網就是這麼用的,後面幾個都是控制flex佈局的,因而可知<el-row>默認佔滿父容器寬度且高度auto自適應

  • <el-col>源碼分析
    col的使用也很簡單,以下,有span,offset,pull,push等屬性
<el-col :span="6" :offset="6"><div class="grid-content bg-purple"></div></el-col>
複製代碼

進入package/col查看,col的代碼稍長,主要多出來的邏輯是控制自適應(@media screen)

export default {
  //組件名稱
  name: 'ElCol',
  props: {
    //組件佔父容器的列數,總共24列,若是設置爲0則渲染出來display爲none
    span: {
      type: Number,
      default: 24
    },
    //最終渲染出的標籤名,默認div
    tag: {
      type: String,
      default: 'div'
    },
    //經過制定 col 組件的 offset 屬性能夠指定分欄向右偏移的欄數
    offset: Number,
    //柵格向右移動格數
    pull: Number,
    //柵格向左移動格數
    push: Number,
    //響應式相關
    xs: [Number, Object],
    sm: [Number, Object],
    md: [Number, Object],
    lg: [Number, Object],
    xl: [Number, Object]
  },

  computed: {
    //獲取el-row的gutter值
    gutter() {
      let parent = this.$parent;
      //不斷經過獲取父元素直到找到el-row元素位置,注意這裏的技巧,componentName實際
      //是el-row組件設置的一個自定義屬性,用來判斷是不是el-row組件
      while (parent && parent.$options.componentName !== 'ElRow') {
        parent = parent.$parent;
      }
      return parent ? parent.gutter : 0;
    }
  },
  render(h) {
    let classList = [];
    let style = {};
    //經過gutter計算本身的左右2個padding,達到分隔col的目的
    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(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);
  }
};
複製代碼

下面解釋下['span', 'offset', 'pull', 'push']這幾個的做用,span很好理解,佔父容器的列數,對應scss代碼以下

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

.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%;
  }
}
複製代碼

注意上面的[attribute*=value] 選擇器,它選擇了全部類名以el-col-開頭的類,加上float和border-box,水平佈局float確定不可少,再看for循環,這裏scss的威力就發揮了,若是隻用css,那代碼量要乘以24,el-col-數字類型的類的寬度就是百分比,下面的offset其實是margin-left,這可能會致使一行排列不下全部的col,會致使換行出現,而el-col-pull則不一樣,僅僅只是相對原來的位置移動,不會形成擠下去換行的狀況,而會形成不一樣col互相覆蓋

注意上面的js部分大量使用模板字符串而不是字符串拼接,達到簡化代碼的目的,這個值得學習

相關文章
相關標籤/搜索