以前分析過了 element-ui
的項目(自建vue組件 air-ui (2) -- 先分析一下 element ui 項目),包括目錄結構,構建以及項目開發的總體思路,今天打算仍是以 element-ui
這個項目的源碼爲主,咱們來聊一聊開發一個ui組件的時候,咱們應該怎麼設計 css 的開發規範。 這一點我以爲 element-ui
它們封裝的很是好,該抽象的有抽象,該封裝的有封裝。因此後續我開發 air-ui
直接把這一套挪過去用了。css
關於 BEM
的更詳細的介紹,能夠看它的官網: BEM官網html
BEM
是 Block
(塊) Element
(元素) Modifier
(修飾器)的簡稱, 它能夠幫助你建立出能夠複用的前端組件和前端代碼, 他有如下三個特色:前端
重用性
不一樣方式組織獨立的塊,並智能地重用它們,能夠減小必須維護的CSS代碼量。經過一系列風格指南,您能夠構建一個塊庫,使您的CSS超級有效。單元性
塊的樣式歷來不依賴同頁面其它的元素,因此你永遠不會遇到級聯問題。您還能夠將完成的項目中的塊轉移到新項目中。結構化
BEM方法可使得你的CSS代碼結構性很好,從而更加容易理解。使用BEM規範來命名CSS,組織HTML中選擇器的結構,利於CSS代碼的維護,使得代碼結構更清晰。 固然也有弊端,好比名字會稍長, 並且由於大部分都是隻有一層結構,還要注意樣式覆蓋問題vue
接下來我要講的就是 element-ui
如何利用sass,編寫具備可讀性和可維護性的BEM規則的css代碼。element-ui
在 package/theme-chalk/src/mixins/config.scss
這個文件中,有一個針對 BEM 的規範定義:sass
$namespace: 'el';
$element-separator: '__';
$modifier-separator: '--';
$state-prefix: 'is-';
複製代碼
__
來做爲塊和元素的間隔, 好比 el-form-item__content
--
來做爲塊和修飾器 或 元素和修飾器 的間隔, 好比 el-form--inline
-
來做爲 塊|元素|修飾器 名稱中多個單詞的間隔is
, 好比是否選中,就是 is-checked
命名空間 $namespace
其實就是 BEM 的前綴,也就是說,後面我在作 air-ui
的時候,直接設置$namespace: 'air';
就能夠了。app
在 packages/theme-chalk/src/mixins/mixins.scss
有對 BEM 的宏定義:函數
@mixin b($block) {
$B: $namespace+'-'+$block !global;
.#{$B} {
@content;
}
}
複製代碼
邏輯看起來不難理解,結合上面的 config.scss
的定義,這時就能很清楚的看到block
的生成就是基於BEM規範中,塊是設計或佈局的一部分,具備必定的意義,利用命名空間el
加上中劃線,以及傳入的block
的名字,構建出block
的樣式,例如alert
組件,在渲染完成後是el-alert
,體現出它的惟一性。而在塊的內部,再來編寫跟這個塊關聯的其餘樣式代碼。佈局
這邊注意兩個細節:post
!global
表示變量提高,將局部變量 B
提高爲全局變量,這樣子在其餘函數體內也能訪問到此變量 (這樣子 b 裏面的 e 才能訪問到 b 的選擇器 B
)#{}
表示插值,能夠經過 #{}
插值語法在選擇器和屬性名中使用 SassScript
變量, 好比原先是這樣子:$name: foo;
$attr: border;
p.#{$name} {
#{$attr}-color: blue;
}
複製代碼
通過編譯以後,就會變成:
p.foo {
border-color: blue;
}
複製代碼
@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;
}
}
}
}
複製代碼
上面的 each
很好理解,由於有可能傳進去的多個參數,好比在 table
中,就有這種寫法:
@include b(table) {
position: relative;
// something table content
@include e((header-wrapper, body-wrapper, footer-wrapper)) {
width: 100%;
}
}
複製代碼
編譯成 css 就是:
.el-table__body-wrapper, .el-table__footer-wrapper, .el-table__header-wrapper {
width: 100%
}
複製代碼
固然這邊也要注意一個細節,就是 @at-root
將父級選擇器直接暴力的改爲根選擇器。
接下來咱們將看一下這個 if
和 else
分支,爲何會出現 hitAllSpecialNestRule
函數判斷的分支,緣由是在修飾符或者其餘mixin中(好比 僞類或者狀態) 嵌套一個元素element,會出現修飾符在前,而元素在後的編譯結果,因此咱們用 hitAllSpecialNestRule
函數來判斷是否存在特殊的嵌套,若是存在的話,就將 element 元素嵌套在裏面,若是不存在,則原樣輸出(改爲根選擇器輸出)
接下來看一下 hitAllSpecialNestRule
的定義, 在 packages/theme-chalk/src/mixins/function.scss
中
@import "config";
/* BEM support Func -------------------------- */
@function selectorToString($selector) {
$selector: inspect($selector);
$selector: str-slice($selector, 2, -2);
@return $selector;
}
@function containsModifier($selector) {
$selector: selectorToString($selector);
@if str-index($selector, $modifier-separator) {
@return true;
} @else {
@return false;
}
}
@function containWhenFlag($selector) {
$selector: selectorToString($selector);
@if str-index($selector, '.' + $state-prefix) {
@return true
} @else {
@return false
}
}
@function containPseudoClass($selector) {
$selector: selectorToString($selector);
@if str-index($selector, ':') {
@return true
} @else {
@return false
}
}
@function hitAllSpecialNestRule($selector) {
@return containsModifier($selector) or containWhenFlag($selector) or containPseudoClass($selector);
}
複製代碼
第一個函數 selectorToString
,就是將咱們的選擇器轉換成一個字符串,而接下來的三個函數,分別判斷了:
m
, 經過上級選擇器是否含有標記爲 m
的 --
子串.is-checked
,經過上級選擇器是否含有 is-
子串:hover
,經過上級選擇器是否含有 :
子串最後綜合在一塊兒返回結果,避免嵌套。
好比在 message-box.scss
當爲居中佈局的時候,就會出現這種狀況:
@include b(message-box) {
//...
// centerAlign 佈局
@include m(center) {
//...
@include e(header) {
padding-top: 30px;
}
}
}
複製代碼
編譯成 css 就是:
.el-message-box--center .el-message-box__header {
padding-top: 30px
}
複製代碼
能夠看到有嵌套進去了
好比在 step.scss
當爲橫向展現的時候,就會出現這種狀況:
@include b(step) {
//...
@include when(horizontal) {
//...
@include e(line) {
height: 2px;
top: 11px;
left: 0;
right: 0;
}
}
}
複製代碼
編譯成 css 就是:
.el-step.is-horizontal .el-step__line {
height: 2px;
top: 11px;
left: 0;
right: 0
}
複製代碼
其中 when
也是一個自定義的宏,主要是用來添加狀態:
@mixin when($state) {
@at-root {
&.#{$state-prefix + $state} {
@content;
}
}
}
複製代碼
好比仍是在 step.scss
也會出現這種狀況:
@include b(step) {
//...
@include pseudo(last-of-type) {
@include e(line) {
display: none;
}
}
}
複製代碼
編譯成 css 就是:
.el-step:last-of-type .el-step__line {
display: none
}
複製代碼
其中 pseudo
也是一個自定義的宏,主要是用來添加僞類狀態:
@mixin pseudo($pseudo) {
@at-root #{&}#{':#{$pseudo}'} {
@content
}
}
複製代碼
相對於 e 的定義, m 的定義就會比較好懂了:
@mixin m($modifier) {
$selector: &;
$currentSelector: "";
@each $unit in $modifier {
$currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ","};
}
@at-root {
#{$currentSelector} {
@content;
}
}
}
複製代碼
這邊要注意一個細節,在拼接 currentSelector
字符串時,使用了$
父級選擇器,而沒有使用全局變量B
+ 全局變量E
來拼接,由於結構不必定是B-E-M
,有多是B-M
。
同時也有存在一次定義多個 m 的狀況,好比在 table.scss
中就有:
@include b(table) {
@include m((group, border)) {
border: $--table-border;
}
}
複製代碼
編譯成 css 就是:
.el-table--border, .el-table--group {
border: 1px solid #EBEEF5
}
複製代碼
基本上關於 BEM
的分析也就這些了,接下來就是在具體寫組件的時候,熟練應用了。 接下來用一個例子來回顧一下。
要寫例子,直接在 scss在線編譯 直接模擬,很是方便: 首先先把一些 參數和用到的宏定義,預填進去:
$namespace: 'el';
$element-separator: '__';
$modifier-separator: '--';
$state-prefix: 'is-';
@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;
}
}
}
//@debug inspect("Helvetica"); unquote('"Helvetica"')
@function selectorToString($selector) {
$selector: inspect($selector);
$selector: str-slice($selector, 2, -2);
@return $selector;
}
// 判斷選擇器(.el-button__body--active) 是否包含 '--'
@function containsModifier($selector) {
$selector: selectorToString($selector);
@if str-index($selector, $modifier-separator) {
@return true;
} @else {
@return false;
}
}
// 判斷選擇器(.el-button__body.is-active) 是否包含 'is'
@function containWhenFlag($selector) {
$selector: selectorToString($selector);
@if str-index($selector, '.' + $state-prefix) {
@return true;
} @else {
@return false;
}
}
// 判斷選擇器(.el-button__body:before) 是否包含僞元素(:hover)
@function containPseudoClass($selector) {
$selector: selectorToString($selector);
@if str-index($selector, ':') {
@return true;
} @else {
@return false;
}
}
// hit:命中 nest:嵌套
@function hitAllSpecialNestRule($selector) {
@return containsModifier($selector) or containWhenFlag($selector) or containPseudoClass($selector);
}
複製代碼
接下來就能夠在下面直接寫 sass 代碼了:
.container {
@include b('button') {
width: 210px;
height: 200px;
@include e('body') {
color: #ccc;
@include m('success'){
background-color: #fff;
}
}
}
}
.container--fix {
@include b('button') {
width: 200px;
height: 200px;
@include e('body') {
color: #fff;
@include m('success'){
background-color: #fff;
}
}
}
}
複製代碼
右邊就會生成對應的 css 代碼:
其實 element-ui
在 sass 定義的宏和函數很少,大部分都是爲了 BEM
服務的。 只有一個 宏 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}`";
}
}
複製代碼
用過 element-ui
應該知道在使用 col
組件的時候,是容許設置根據不一樣的屏幕尺寸來設置不一樣的響應尺寸的,好比如下這個:
<el-row :gutter="10">
<el-col :xs="8" :sm="6" :md="4" :lg="3" :xl="1"><div class="grid-content bg-purple"></div></el-col>
<el-col :xs="4" :sm="6" :md="8" :lg="9" :xl="11"><div class="grid-content bg-purple-light"></div></el-col>
<el-col :xs="4" :sm="6" :md="8" :lg="9" :xl="11"><div class="grid-content bg-purple"></div></el-col>
<el-col :xs="8" :sm="6" :md="4" :lg="3" :xl="1"><div class="grid-content bg-purple-light"></div></el-col>
</el-row>
複製代碼
其實這個就是經過 res 這個宏實現的:具體配置是這樣子的,這個是針對大屏幕的:
@include res(lg) {
.el-col-lg-0 {
display: none;
}
@for $i from 0 through 24 {
.el-col-lg-#{$i} {
width: (1 / 24 * $i * 100) * 1%;
}
.el-col-lg-offset-#{$i} {
margin-left: (1 / 24 * $i * 100) * 1%;
}
.el-col-lg-pull-#{$i} {
position: relative;
right: (1 / 24 * $i * 100) * 1%;
}
.el-col-lg-push-#{$i} {
position: relative;
left: (1 / 24 * $i * 100) * 1%;
}
}
}
複製代碼
而後再結合這些規定的參數,就能夠實現不一樣的媒體查詢的樣式:
$--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)
);
複製代碼
咱們能夠在線實現一下: 具體實現:
element-ui
中全部可定製的參數,所有在 var.scss
文件裏面。這個文件也是後面作主題定製的關鍵,由於只要重寫這個文件的某些參數,從新打包成 css,就至關於作了一個新的主題了。 這個在後面的文章中會再講到
系列文章: