當咱們拿到一個 PC 端頁面的設計稿的時候,每每會發現頁面的佈局並非隨意的,而是遵循的必定的規律:行與行之間會以某種方式對齊。對於這樣的設計稿,咱們可使用柵格佈局來實現。css
早在 Bootstrap 一統江湖的時代,柵格佈局的概念就已深刻人心,整個佈局就是一個二維結構,包括列和行, Bootstrap 會把屏幕分紅 12 列,還提供了一些很是方便的 CSS 名讓咱們來指定每列佔的寬度百分比,而且還經過媒體查詢作了不一樣屏幕尺寸的適應。html
element-ui
也實現了相似 Bootstrap 的柵格佈局系統,那麼基於 Vue 技術棧,它是如何實現的呢?前端
和 Bootstrap 12 分欄不一樣的是,element-ui
目標是提供的是更細粒度的 24 分欄,迅速簡便地建立佈局,寫法大體以下:vue
<el-row>
<el-col>aaa</el-col>
<el-col>bbb</el-col>
</el-row>
<el-row>
...
</el-row>
複製代碼
這就是二維佈局的雛形,咱們會把每一個列的內容寫在 <el-col></el-col>
之間,除此以外,咱們還須要支持控制每一個 <el-col>
所佔的寬度自由組合佈局;支持分欄之間存在間隔;支持偏移指定的欄數;支持分欄不一樣的對齊方式等。element-ui
瞭解了 element-ui
Layout 佈局組件的需求後,咱們來分析它的設計和實現。緩存
回顧前面的例子,從寫法上看,咱們須要設計 2 個組件,el-row
和 el-col
組件,分別表明行和列;從 Vue 的語法上看,這倆組件都要支持插槽(由於在自定義組件標籤內部的內容都分發到組件的 slot 中了);從 HTML 的渲染結果上看,咱們但願模板會渲染成:sass
<div class="el-row">
<div class="el-col">aaa</div>
<div class="el-col">bbb</div>
</div>
<div class="el-row">
...
</div>
複製代碼
想達到上述需求,組件的模板能夠很是簡單。ide
el-row
組件模板代碼以下:函數
<div class="el-row">
<slot></slot>
</div>
複製代碼
el-col
組件代碼以下:佈局
<div class="el-col">
<slot></slot>
</div>
複製代碼
這個時候,新需求來了,我但願 el-row
和 el-col
組件不只能渲染成 div,還能夠渲染成任意我想指定的標籤。 那麼除了咱們要支持一個 tag
的 prop
以外,僅用模板是難以實現了。
咱們知道 Vue 的模板最終會編譯成 render
函數,Vue 的組件也支持直接手寫 render
函數,那這個需求用 render
函數實現就很是簡單了。
el-row
組件:
render(h) {
return h(this.tag, {
class: [
'el-row',
]
}, this.$slots.default);
}
複製代碼
el-col
組件:
render(h) {
return h(this.tag, {
class: [
'el-col',
]
}, this.$slots.default);
}
複製代碼
其中,tag
是定義在 props
中的,h
是 Vue 內部實現的 $createElement
函數,若是對 render
函數語法還不太懂的同窗,建議去看 Vue 的官網文檔 render 函數部分。
瞭解了組件是如何渲染以後,咱們來給 Layout 組件擴展一些 feature 。
Layout 佈局的主要目標是支持 24 分欄,即一行能被切成 24 份,那麼對於每個 el-col
,咱們想要知道它的佔比,只須要指定它在 24 份中分配的份數便可。
因而咱們給剛纔的示例加上一些配置:
<el-row>
<el-col :span="8">aaa</el-col>
<el-col :span="16">bbb</el-col>
</el-row>
<el-row>
...
</el-row>
複製代碼
來看第一行,第一列 aaa
佔 8 份,第二列 bbb
佔 16 份。總共寬度是 24 份,通過簡單的數學公式計算,aaa
佔總寬度的 1/3,而 bbb
佔總寬度的 2/3,進而推導出每一列指定 span
份就是佔總寬度的 span/24
。
默認狀況下 div 的寬度是 100% 獨佔一行的,爲了讓多個 el-col
在一行顯示,咱們只須要讓每一個 el-col
的寬佔必定的百分比,即實現了分欄效果。設置不一樣的寬度百分比只須要設置不一樣的 CSS 便可實現,好比當某列佔 12 份的時候,那麼它對應的 CSS 以下:
.el-col-12 {
width: 50%
}
複製代碼
爲了知足 24 種狀況,element-ui
使用了 sass
的控制指令,配合基本的計算公式:
.el-col-0 {
display: none;
}
@for $i from 0 through 24 {
.el-col-#{$i} {
width: (1 / 24 * $i * 100) * 1%;
}
}
複製代碼
因此當咱們給 el-col
組件傳入了 span
屬性的時候,只須要給對應的節點渲染生成對應的 CSS 便可,因而咱們能夠擴展 render
函數:
render(h) {
let classList = [];
classList.push(`el-col-${this.span}`);
return h(this.tag, {
class: [
'el-col',
classList
]
}, this.$slots.default);
}
複製代碼
這樣只要指定 span
屬性的列就會添加 el-col-${span}
的樣式,實現了分欄佈局的需求。
對於柵格佈局來講,列與列之間有必定間隔空隙是常見的需求,這個需求的做用域是行,因此咱們應該給 el-row
組件添加一個 gutter
的配置,以下:
<el-row :gutter="20">
<el-col :span="8">aaa</el-col>
<el-col :span="16">bbb</el-col>
</el-row>
<el-row>
...
</el-row>
複製代碼
有了配置,接下來如何實現間隔呢?實際上很是簡單,想象一下,2 個列之間有 20 像素的間隔,若是咱們每列各往一邊收縮 10 像素,是否是看上去就有 20 像素了呢。
先看一下 el-col
組件的實現:
computed: {
gutter() {
let parent = this.$parent;
while (parent && parent.$options.componentName !== 'ElRow') {
parent = parent.$parent;
}
return parent ? parent.gutter : 0;
}
},
render(h) {
let classList = [];
classList.push(`el-col-${this.span}`);
let style = {};
if (this.gutter) {
style.paddingLeft = this.gutter / 2 + 'px';
style.paddingRight = style.paddingLeft;
}
return h(this.tag, {
class: [
'el-col',
classList
]
}, this.$slots.default);
}
複製代碼
這裏使用了計算屬性去計算 gutter
,實際上是比較有趣的,它經過 $parent
往外層查找 el-row
,獲取到組件的實例,而後獲取它的 gutter
屬性,這樣就創建了依賴關係,一旦 el-row
組件的 gutter
發生變化,這個計算屬性再次被訪問的時候就會從新計算,獲取到新的 gutter
。
其實,想在子組件去獲取祖先節點的組件實例,我更推薦使用 provide/inject
的方式去把祖先節點的實例注入到子組件中,這樣子組件能夠很是方便地拿到祖先節點的實例,好比咱們在 el-row
組件編寫 provide
:
provide() {
return {
row: this
};
}
複製代碼
而後在 el-col
組件注入依賴:
inject: ['row']
複製代碼
這樣在 el-col
組件中咱們就能夠經過 this.row
訪問到 el-row
組件實例了。
使用 provide/inject
的好處在於不論組件層次有多深,子孫組件能夠方便地訪問祖先組件注入的依賴。當你在編寫組件庫的時候,遇到嵌套組件而且子組件須要訪問父組件實例的時候,避免直接使用 this.$parent
,儘可能使用 provide/inject
,由於一旦你的組件嵌套關係發生變化,this.$parent
可能就不符合預期了,而 provide/inject
卻不受影響(只要祖先和子孫的關係不變)。
在 render
函數中,咱們會根據 gutter
計算,給當前列添加了 paddingLeft
和 paddingRight
的樣式,值是 gutter
的一半,這樣就實現了間隔 gutter
的效果。
那麼這裏可否用 margin
呢,答案是不能,由於設置 margin
會佔用外部的空間,致使每列的佔用空間變大,會出現折行的狀況。
render
過程也是有優化的空間,由於 style
是根據 gutter
計算的,那麼咱們能夠把 style
定義成計算屬性,這樣只要 gutter
不變,那麼 style
就能夠直接拿計算屬性的緩存,而不用從新計算,對於 classList
部分,咱們一樣可使用計算屬性。組件 render
過程的一個原則就是能用計算屬性就用計算屬性。
再來看一下 el-row
組件的實現:
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',
],
style: this.style
}, this.$slots.default);
}
複製代碼
因爲咱們是經過給每列添加左右 padding
的方式來實現列之間的間隔,那麼對於第一列和最後一列,左邊和右邊也會多出來 gutter/2
大小的間隔,顯然是不符合預期的,因此咱們能夠經過設置左右負 margin
的方式填補左右的空白,這樣就完美實現了分欄間隔的效果。
如圖所示,咱們也能夠指定某列的偏移,因爲做用域是列,咱們應該給 el-col
組件添加一個 offset
的配置,以下:
<el-row :gutter="20">
<el-col :offset="8" :span="8">aaa</el-col>
<el-col :span="8">bbb</el-col>
</el-row>
<el-row>
...
</el-row>
複製代碼
直觀上咱們應該用 margin
來實現偏移,而且 margin
也是支持百分比的,所以實現這個需求就變得簡單了。
咱們繼續擴展 el-col
組件:
render(h) {
let classList = [];
classList.push(`el-col-${this.span}`);
classList.push(`el-col-offset-${this.offset}`);
let style = {};
if (this.gutter) {
style.paddingLeft = this.gutter / 2 + 'px';
style.paddingRight = style.paddingLeft;
}
return h(this.tag, {
class: [
'el-col',
classList
]
}, this.$slots.default);
}
複製代碼
其中 offset
是定義在 props
中的,咱們根據傳入的 offset
生成對應的 CSS 添加到 DOM 中。element-ui
一樣使用了 sass
的控制指令,配合基本的計算公式來實現這些 CSS 的定義:
@for $i from 0 through 24 {
.el-col-offset-#{$i} {
margin-left: (1 / 24 * $i * 100) * 1%;
}
}
複製代碼
對於不一樣偏移的分欄數,會有對應的 margin
百分比,就很好地實現分欄偏移需求。
當一行分欄的總佔比和沒有達到 24 的時候,咱們是能夠利用 flex
佈局來對分欄作靈活的對齊。
對於不一樣的對齊方式 flex
佈局提供了 justify-content
屬性,因此對於這個需求,咱們能夠對 flex
佈局作一層封裝便可實現。
因爲對齊方式的做用域是行,因此咱們應該給 el-row
組件添加 type
和 justify
的配置,以下:
<el-row type="flex" justify="center">
<el-col :span="8">aaa</el-col>
<el-col :span="8">bbb</el-col>
</el-row>
<el-row>
...
</el-row>
複製代碼
因爲咱們是對 flex
佈局的封裝,咱們只須要根據傳入的這些 props
去生成對應的 CSS,在 CSS 中定義 flex
的佈局屬性便可。
咱們繼續擴展 el-row
組件:
render(h) {
return h(this.tag, {
class: [
'el-row',
this.justify !== 'start' ? `is-justify-${this.justify}` : '',
{ 'el-row--flex': this.type === 'flex' }
],
style: this.style
}, this.$slots.default);
}
複製代碼
其中 type
和 justify
是定義在 props
中的,咱們根據它們傳入的值生成對應的 CSS 添加到 DOM 中,接着咱們須要定義對應的 CSS 樣式:
@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;
}
}
}
複製代碼
element-ui
在編寫 sass 的時候主要遵循的是 BEM 的命名規則,而且編寫了不少自定義 @mixin
來配合樣式名的定義。
這裏咱們來花點時間來學習一下它們,element-ui
的自定義 @mixin
定義在 pacakages/theme-chalk/src/mixins/
目錄中,我並不會詳細解釋這裏面的關鍵字,若是你對 sass 還不熟悉,我建議在學習這部份內容的時候配合 sass 的官網文檔看。
mixins/config.scss
中定義了一些全局變量:
$namespace: 'el';
$element-separator: '__';
$modifier-separator: '--';
$state-prefix: 'is-';
複製代碼
mixins/mixins.scss
中定義了 BEM 的自定義 @mixin
,先來看一下定義組件樣式的 @mixin b
:
@mixin b($block) {
$B: $namespace+'-'+$block !global;
.#{$B} {
@content;
}
}
複製代碼
這個 @mixin
很好理解,$B
是內部定義的變量,它的值經過 $namespace+'-'+$block
計算獲得,注意這裏有一個 !global
關鍵字,它表示把這個局部變量變成全局的,意味着你也能夠在其它 @mixin
中引用它。
經過 @include
咱們就能夠去引用這個 @mixin
,結合咱們的 case 來看:
@include b(row) {
// xxx content
}
複製代碼
會編譯成:
.el-row {
// xxx content
}
複製代碼
再來看錶示修飾符的 @mixin m
:
@mixin m($modifier) {
$selector: &;
$currentSelector: "";
@each $unit in $modifier {
$currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ","};
}
@at-root {
#{$currentSelector} {
@content;
}
}
}
複製代碼
這裏是容許傳入的 $modifier
有多個,因此內部用了 @each
,&
表示父選擇器,$selector
和 $currentSelector
是內部定義的 2 個局部變量,結合咱們的 case 來看:
@mixin b(row) {
@include m(flex) {
// xxx content
}
}
複製代碼
會編譯成:
.el-row--flex {
// xxx content
}
複製代碼
有同窗可能會疑問,難道不是:
.el-row {
.el-row--flex {
// xxx content
}
}
複製代碼
其實並非,由於咱們在該 @mixin
的內部使用了 @at-root
指令,它會把樣式規則定義在根目錄下,而不是嵌套在其父選擇器下。
最後來看一下表示同級樣式的 @mixin when
:
@mixin when($state) {
@at-root {
&.#{$state-prefix + $state} {
@content;
}
}
}
複製代碼
這個 @mixin
也很好理解,結合咱們的 case 來看:
@mixin b(row) {
@include m(flex) {
@include when(justify-center) {
justify-content: center;
}
}
}
複製代碼
會編譯成:
.el-row--flex.is-justify-center {
justify-content: center;
}
複製代碼
關於 BEM 的 @mixin
,經常使用的還有 @mixin e
,用於定義組件內部一些子元素樣式的,感興趣的同窗能夠自行去看。
再回到咱們的 el-row
組件的樣式,咱們定義了幾種flex
佈局的對齊方式,而後經過傳入不一樣的 justify
來生成對應的樣式,這樣咱們就很好地實現了靈活對齊分欄的需求。
element-ui
參照了 Bootstrap 的響應式設計,預設了五個響應尺寸:xs
、sm
、md
、lg
和 xl
。
容許咱們在不一樣的屏幕尺寸下,設置不一樣的分欄配置,因爲做用域是列,因此咱們應該給 el-col
組件添加 xs
xs
、sm
、md
、lg
和 xl
的配置,以下:
<el-row type="flex" justify="center">
<el-col :xs="8" :sm="6" :md="4" :lg="3" :xl="1">aaa</el-col>
<el-col :xs="4" :sm="6" :md="8" :lg="9" :xl="11">bbb</el-col>
<el-col :xs="4" :sm="6" :md="8" :lg="9" :xl="11">ccc</el-col>
<el-col :xs="8" :sm="6" :md="4" :lg="3" :xl="1">ddd</el-col>
</el-row>
<el-row>
...
</el-row>
複製代碼
同理,咱們仍然是經過這些傳入的 props
去生成對應的 CSS,在 CSS 中利用媒體查詢去實現響應式。
咱們繼續擴展 el-col
組件:
render(h) {
let classList = [];
classList.push(`el-col-${this.span}`);
classList.push(`el-col-offset-${this.offset}`);
['xs', 'sm', 'md', 'lg', 'xl'].forEach(size => {
classList.push(`el-col-${size}-${this[size]}`);
});
let style = {};
if (this.gutter) {
style.paddingLeft = this.gutter / 2 + 'px';
style.paddingRight = style.paddingLeft;
}
return h(this.tag, {
class: [
'el-col',
classList
]
}, this.$slots.default);
}
複製代碼
其中,xs
、sm
、md
、lg
和 xl
是定義在 props
中的,實際上 element-ui
源碼還容許傳入一個對象,能夠配置 span
和 offset
,但這部分代碼我就不介紹了,無非就是對對象的解析,添加對應的樣式。
咱們來看一下對應的 CSS 樣式,以 xs
爲例:
@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%;
}
.el-col-xs-offset-#{$i} {
margin-left: (1 / 24 * $i * 100) * 1%;
}
}
}
複製代碼
這裏又定義了表示響應式的 @mixin res
,咱們來看一下它的實現:
@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}`";
}
}
複製代碼
這個 @mixns
主要是查看 $map
中是否有 $key
,若是有的話則定義一條媒體查詢規則,若是沒有則拋出警告。
$map
參數的默認值是 $--breakpoints
,定義在 pacakges/theme-chalk/src/common/var.scss
中:
$--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)
);
複製代碼
結合咱們的 case 來看:
@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%;
}
.el-col-xs-offset-#{$i} {
margin-left: (1 / 24 * $i * 100) * 1%;
}
}
}
複製代碼
會編譯成:
@media only screen and (max-width: 767px) {
.el-col-xs-0 {
display: none;
}
.el-col-xs-1 {
width: 4.16667%
}
.el-col-xs-offset-1 {
margin-left: 4.16667%
}
// 後面循環的結果太長,就不貼了
}
複製代碼
其它尺寸內部的樣式定義規則也是相似,這樣咱們就經過媒體查詢定義了各個屏幕尺寸下的樣式規則了。經過傳入 xs
、sm
這些屬性的值不一樣,從而生成不一樣樣式,這樣在不一樣的屏幕尺寸下,能夠作到分欄的佔寬不一樣,很好地知足了響應式需求。
Element 額外提供了一系列類名,用於在某些條件下隱藏元素,這些類名能夠添加在任何 DOM 元素或自定義組件上。
咱們能夠經過引入單獨的 display.css
:
import 'element-ui/lib/theme-chalk/display.css';
複製代碼
它包含的類名及其含義以下:
hidden-xs-only
- 當視口在 xs 尺寸時隱藏hidden-sm-only
- 當視口在 sm 尺寸時隱藏hidden-sm-and-down
- 當視口在 sm 及如下尺寸時隱藏hidden-sm-and-up
- 當視口在 sm 及以上尺寸時隱藏hidden-md-only
- 當視口在 md 尺寸時隱藏hidden-md-and-down
- 當視口在 md 及如下尺寸時隱藏hidden-md-and-up
- 當視口在 md 及以上尺寸時隱藏hidden-lg-only
- 當視口在 lg 尺寸時隱藏hidden-lg-and-down
- 當視口在 lg 及如下尺寸時隱藏hidden-lg-and-up
- 當視口在 lg 及以上尺寸時隱藏hidden-xl-only
- 當視口在 xl 尺寸時隱藏咱們來看一下它的實現,看一下 display.scss
:
.hidden {
@each $break-point-name, $value in $--breakpoints-spec {
&-#{$break-point-name} {
@include res($break-point-name, $--breakpoints-spec) {
display: none !important;
}
}
}
}
複製代碼
實現很簡單,對 $--breakpoints-spec
遍歷,生成對應的 CSS 規則,$--breakpoints-spec
定義在 pacakges/theme-chalk/src/common/var.scss
中:
$--breakpoints-spec: (
'xs-only' : (max-width: $--sm - 1),
'sm-and-up' : (min-width: $--sm),
'sm-only': "(min-width: #{$--sm}) and (max-width: #{$--md - 1})",
'sm-and-down': (max-width: $--md - 1),
'md-and-up' : (min-width: $--md),
'md-only': "(min-width: #{$--md}) and (max-width: #{$--lg - 1})",
'md-and-down': (max-width: $--lg - 1),
'lg-and-up' : (min-width: $--lg),
'lg-only': "(min-width: #{$--lg}) and (max-width: #{$--xl - 1})",
'lg-and-down': (max-width: $--xl - 1),
'xl-only' : (min-width: $--xl),
);
複製代碼
咱們以 xs-only
爲例,編譯後生成的 CSS 規則以下:
.hidden-xs-only {
@media only screen and (max-width:767px) {
display: none !important;
}
}
複製代碼
本質上仍是利用媒體查詢定義了這些 CSS 規則,實現了在某些屏幕尺寸下隱藏的功能。
其實 Layout 佈局還支持了其它一些特性,我不一一列舉了,感興趣的同窗能夠自行去看。Layout 佈局組件充分利用了數據驅動的思想,經過數據去生成對應的 CSS,本質上仍是經過 CSS 知足各類靈活的佈局。
學習完這篇文章,你應該完全弄懂 element-ui
Layout 佈局組件的實現原理,而且對 sass
的 @mixin
以及相關使用到的特性有所瞭解,對組件實現過程當中能夠優化的部分,應該有本身的思考。
把不會的東西學會了,那麼你就進步了,若是你以爲這類文章有幫助,也歡迎把它推薦給你身邊的小夥伴。
下一篇預告 :Element-UI 技術揭祕(4)— Container 佈局容器組件的設計與實現。
另外,我最近剛開了公衆號「老黃的前端私房菜」,《Element-UI 技術揭祕》系列文章會第一時間在公衆號更新和發佈,除此以外,我還會常常分享一些前端進階知識,乾貨,也會偶爾分享一些軟素質技能,歡迎你們關注喔~