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

超級索尼發表於2018-08-15

動機

感覺現在的業務開發,如果不是很特殊的需求,基本都能在對應的元件庫內找到元件使用,這樣編寫程式碼就成了呼叫元件,但是卻隱藏了元件內的思想,因此弱化了程式設計能力,所以我想寫這麼個分析系列來鞭策自己深入分析元件的原理,提高程式碼閱讀理解能力,我覺得一定要記下點什麼來,如果只是看不動筆感覺很快就忘了,因此準備持續寫這麼個分析

Element原始碼結構

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

Element原始碼分析系列1一Layout(佈局)
其中元件的原始碼放在package目錄下,src中是一些工具函式(某些元件都會使用這些函式)和國際化相關的程式碼,進入package目錄裡,則是所有元件的原始碼

Element原始碼分析系列1一Layout(佈局)
注意這些資料夾裡只包含js或者vue,而所有元件的樣式檔案在最下面的theme-chalk資料夾裡,整個專案結構還是很清晰

Layout(佈局)原始碼分析

  • <el-row>原始碼分析

首先進入開啟官網檢視Layout相關部分的說明,發現主要的元件就2個: el-row,el-col,這2個分別代表行的容器和裡面列的容器,類似於bootstrapcolrow,首先我們檢視el-row的實現,進入package裡面的row資料夾,裡面是一個src資料夾和index.js檔案

Element原始碼分析系列1一Layout(佈局)
開啟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後註冊元件,官網示例程式碼如下,也可以不註冊成全域性元件

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中一探究竟,首先程式碼的整體結構如下,直接匯出一個物件,裡面是元件的各種配置項

export default {
    ...
}
複製程式碼

整個元件的程式碼量不多,下面是給出了詳細註釋

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產生出間隔來,但是注意容器的最左和最右側是沒有間隔的

Element原始碼分析系列1一Layout(佈局)
上圖就是最終示意圖,黑框就是<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)

下面解釋下最後返回的渲染函式render,這個函式有3個引數,第一個引數是html的tag名稱(最終在網頁中顯示的標籤名),第二個引數是一個包含模板相關屬性的資料物件,裡面有相當多模板相關的屬性,如下

{
  // 和`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部分大量使用模板字串而不是字串拼接,達到簡化程式碼的目的,這個值得學習

相關文章