自建vue組件 air-ui (3) -- css 開發規範

前言

以前分析過了 element-ui 的項目(自建vue組件 air-ui (2) -- 先分析一下 element ui 項目),包括目錄結構,構建以及項目開發的總體思路,今天打算仍是以 element-ui這個項目的源碼爲主,咱們來聊一聊開發一個ui組件的時候,咱們應該怎麼設計 css 的開發規範。 這一點我以爲 element-ui 它們封裝的很是好,該抽象的有抽象,該封裝的有封裝。因此後續我開發 air-ui 直接把這一套挪過去用了。css

什麼是 BEM

關於 BEM 的更詳細的介紹,能夠看它的官網: BEM官網html

BEMBlock(塊) Element(元素) Modifier(修飾器)的簡稱, 它能夠幫助你建立出能夠複用的前端組件和前端代碼, 他有如下三個特色:前端

  • 重用性 不一樣方式組織獨立的塊,並智能地重用它們,能夠減小必須維護的CSS代碼量。經過一系列風格指南,您能夠構建一個塊庫,使您的CSS超級有效。
  • 單元性 塊的樣式歷來不依賴同頁面其它的元素,因此你永遠不會遇到級聯問題。您還能夠將完成的項目中的塊轉移到新項目中。
  • 結構化 BEM方法可使得你的CSS代碼結構性很好,從而更加容易理解。

使用BEM規範來命名CSS,組織HTML中選擇器的結構,利於CSS代碼的維護,使得代碼結構更清晰。 固然也有弊端,好比名字會稍長, 並且由於大部分都是隻有一層結構,還要注意樣式覆蓋問題vue

如何使用 BEM

  1. 一個獨立的(語義上或視覺上),能夠複用而不依賴其它組件的部分,可做爲一個塊(Block)
  2. 屬於塊的某部分,可做爲一個元素(Element)
  3. 用於修飾塊或元素,體現出外形行爲狀態等特徵的,可做爲一個修飾器(Modifier)

element ui 的使用

接下來我要講的就是 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-';
複製代碼
  1. 雙下劃線 __ 來做爲塊和元素的間隔, 好比 el-form-item__content
  2. 雙中劃線 -- 來做爲塊和修飾器 或 元素和修飾器 的間隔, 好比 el-form--inline
  3. 中劃線 - 來做爲 塊|元素|修飾器 名稱中多個單詞的間隔
  4. 狀態的前綴用 is, 好比是否選中,就是 is-checked

命名空間 $namespace 其實就是 BEM 的前綴,也就是說,後面我在作 air-ui 的時候,直接設置$namespace: 'air'; 就能夠了。app

BEM 在 sass 中的定義

packages/theme-chalk/src/mixins/mixins.scss 有對 BEM 的宏定義:函數

block 的定義

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

element 的定義

@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 將父級選擇器直接暴力的改爲根選擇器。

接下來咱們將看一下這個 ifelse 分支,爲何會出現 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 ,就是將咱們的選擇器轉換成一個字符串,而接下來的三個函數,分別判斷了:

  1. 是否存在 修飾符, 就是 m, 經過上級選擇器是否含有標記爲 m-- 子串
  2. 是否存在 flag,好比 .is-checked,經過上級選擇器是否含有 is- 子串
  3. 是否存在僞類,好比 :hover,經過上級選擇器是否含有 : 子串

最後綜合在一塊兒返回結果,避免嵌套。

m 嵌套 e 的狀況

好比在 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
}
複製代碼

能夠看到有嵌套進去了

包含狀態 b 嵌套 e

好比在 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;
    }
  }
}
複製代碼
包含僞類 b 嵌套 e

好比仍是在 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
  }
}
複製代碼

modifier 的定義

相對於 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 的分析也就這些了,接下來就是在具體寫組件的時候,熟練應用了。 接下來用一個例子來回顧一下。

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 代碼:

1

實現響應式佈局的 宏 res

其實 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)
);
複製代碼

咱們能夠在線實現一下: 具體實現:

1

參數定製 var.scss

element-ui 中全部可定製的參數,所有在 var.scss 文件裏面。這個文件也是後面作主題定製的關鍵,由於只要重寫這個文件的某些參數,從新打包成 css,就至關於作了一個新的主題了。 這個在後面的文章中會再講到


系列文章:

相關文章
相關標籤/搜索