结合Element分析基于scss实现BEM的方法

时间:2021-1-18 作者:admin

在一些小型项目中,搭建样式体系并不复杂,但当涉及到更大、更复杂,需要团队协作的项目时,组织代码至少从以下几个方面考虑

  • 编写代码所花时间
  • 所需代码量
  • 高性能,浏览器要做多少加载
  • 可读性、灵活性、是否方便协同开发

因此,BEM的css命名方法论就诞生了!

概念

Bem 是块(block)、元素(element)、修饰符(modifier)的简写,由 Yandex 团队提出的一种前端 CSS, 命名方法论。class名可以获得更多的描述和更加清晰的结构;使代码易于阅读理解,方便协同开发

  • 块(block) 代表更高级别的抽象或组件,可理解为组件最外层元素
  • 元素(element)组件的后代元素
  • 修饰符(modifier)表示块或者后代元素的 的不同状态或版本

命名规范

  • -中弧线连接单词,可以是块元素也可以是子元素
  • __ 双下划线 连接块与块的子元素 (其实单下划线也是可以的,但由于命名习惯中单下划线有时候也作为单词连接符,造成混淆,所以使用双下划线更为稳妥)
  • — 双中划线 描述一个块或者块的子元素的一种状态
  • is is-关键字结合使用时,指示模块特定的状态类;一般用于js控制样式时,css命名用is-开头 例如 is-open、is-disabled

BEM的优势

我们以实际例子为证;在该例子当中,css的命名很松散,没有对应的关联关系,我们看不到card 与header和body的关联,也看不出该组件由什么组成

<div class="card shadow primary">
  <div class="header">
    <span>卡片名称</span>
    <button type="button" class="button">
      <span>操作按钮</span>
    </button>
  </div>
  <div class="body">
    <div class="text item">列表内容 1</div>
  </div>
</div>

我们再来看看使用BEM 风格的命名,我们看到mk-card–primary是当前card组件的主题(修饰状态),mk-card__header和mk-card__body分别是mk-card的子元素;is-shadow则是代表该组件的一些控制样式

<div class="mk-card mk-card--primary is-shadow">
  <div class="mk-card__header">
    <span>卡片名称</span>
    <button type="button" class="button">
      <span>操作按钮</span>
    </button>
  </div>
  <div class="mk-card__body">
    <div class="text item">列表内容 1</div>
  </div>
</div>

你看BEM的优势是不是出来了

  • 可读性强 (类名语义化、结构化)
  • 扩展性强 (CSS选择器的粒度足够地细,可灵活改动,不用考虑选择器之间的权重问题)
  • 适应性强 (模块化复用的理念,让BEM很容易配合其他框架一起使用)

BEM 缺点

BEM经常受人诟病的一个点在于命名长而难看,但是我们不能因此忽略BEM给我们带来的好处,通过less/scss编写会更加便捷;我们要尽量避免过长的嵌套,这就要明确组件划分的细腻度,从哪里划分,何时划分。

实例分析

<div class="collapse">
  <div class="wrap">
    <div
      class="header"
    >
      <div class="header-title">
        <span>标题1</span>
      </div>
      <i class="right-icon"></i>
    </div>
    <div class="divider"></div>
    <div class="content">
      <div class="">Hello World</div>
    </div>
  </div>
</div>

看上面的命名方式,很难通过css的命名看出组件之间的关联,但似乎这种结构使用BEN命名风格,会出现多层级的嵌套;那么我们如何使用BEM构建层级关系但又避免多级嵌套呢?这就需要我们将该组件粒度细化,划分成多个小组件,以下面的结构为例

<div class="mk-collapse">
  <div class="mk-collapse-item mk-collapse-item--expanded">
    <!-- 
      对于折叠面板的header 层,我们可以划分一个单元格组件 mk-cell 组件 减少嵌套,当前层级可加mk-collapse-item__title class建立关联,后续的子元素归于mk-cell的层级,后续mk-cell也可独立在其他地方复用
     -->
    <div
      class="mk-cell mk-collapse-item__title "
    >
      <div class="mk-cell__title">
        <span>标题1</span>
      </div>
      <i class="mk-icon mk-cell__right-icon"></i>
    </div>
    <!-- 
      对于分割线这类常规公告样式,我们可以直接用class ,独立出来,不必可以建立关联
     -->
    <div class="divider"></div>
    <div class="mk-collapse-item__wrapper">
      <div class="mk-collapse-item__content">Hello World</div>
    </div>
  </div>
</div>

你看,这样划分,是不是就避免了多级嵌套,命名又臭又长的情况产生呢;

结合Element分析基于scss实现BEM的方法

回到我们本篇文章的重点板块了,结合element,我们来分析下sass如何实现BEM 结构命名;在源码解析的开始,如果对sass的高阶用法还没有一个系统的认识,建议先看看 SASS的知识体系构建这篇文章

element scss文件源码

在element 源码中,主题文件放在element/tree/dev/packages/theme-chalk目录

在 theme-chalk/src/mixins/config.scss 文件中,有对elment ui风格的基础配置

$namespace: 'el';   //前缀名
$element-separator: '__';  //子元素连接符
$modifier-separator: '--'; //块样式状态连接符
$state-prefix: 'is-';   //特定状态列前缀

在theme-chalk/src/mixins/mixins.scss 中,则定义了BEM的混合方法;

/* BEM
 -------------------------- */
@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;
    }
  }
}

element scss源码解析

@mixin b($block) 的解析

@mixin b($block) {
  $B: $namespace+'-'+$block !global;  //变量拼接形成对应格式的class;再使用!global将其提升为全局变量
  .#{$B} {
    @content;  // 使用混合;大括号后定义的样式将都会解析到这里
  }
}

b方法相对来说比较简单;主要运用了以下知识点

  1. 使用变量的拼接运算;namespace在config.scss文件中有定义,为“el”,假设namespace 在config.scss文件中有定义,为“el”,假设namespaceconfig.scssel,block为card,则拼接结果为el-card
  2. !global 变量提升为全局变量
  3. 使用插值语法将$B这个选择器作为选择器使用
  4. @content 将引用混合后大括号外额外的样式

我们可以通过案例更直观的感受;scss在线编译工具;也可以在我写的文章 SASS的知识体系构建 中的安装编译模块查看相关的编译方法

//编译前
$namespace: "mk";

@mixin b($block) {
  $B: $namespace + "-" + $block !global;
  .#{$B} {
    @content;
  }
}
@include b((card)) {
  border-radius: 4px;
  border: 1px solid #ebeef5;
  background-color: #fff;
  overflow: hidden;
  color: #303133;
  transition: 0.3s;

  @include e((hover-zone, title)) {
    padding: 20px;
  }
}

//编译后
.mk-card {
  border-radius: 4px;
  border: 1px solid #ebeef5;
  background-color: #fff;
  overflow: hidden;
  color: #303133;
  -webkit-transition: 0.3s;
  transition: 0.3s;
}

@mixin e($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;
      }
    }
  }
}

先来分析这部分的代码

//定义混合
@mixin e($element) {
  $E: $element !global;
  $selector: &;
  $currentSelector: "";

  @each $unit in $element {
    $currentSelector: #{$currentSelector +
      "." +
      $B +
      $element-separator +
      $unit +
      ","};
  }
  @debug $currentSelector;   //我们使用sass debug语句来查看改变量的值

  #{$currentSelector} {
    content: "11";
    @content;
  }
}

//执行混合;b方法的定义在这里就不重复写了
@include b((card)) {
  border-radius: 4px;
  border: 1px solid #ebeef5;
  background-color: #fff;
  overflow: hidden;
  color: #303133;
  transition: 0.3s;

  @include e((title, body)) {
    padding: 20px;
  }
}

我们看看打印出的变量

实际上@each内 其实就是变量拼接,通过__符连接父级选择器和传入的子元素,而传入的值可以是一个,也可以是数组;通过实例来理解

//执行混合;b方法的定义在这里就不重复写了
@include b((card)) {
  border-radius: 4px;
  border: 1px solid #ebeef5;
  background-color: #fff;
  overflow: hidden;
  color: #303133;
  transition: 0.3s;

  @include e(footer) {   //传入单个
    padding: 20px;
  }

  @include e((title, body)) {  //传入数组
    padding: 20px;
  }
}

//编译后
.mk-card .mk-card__footer {  //传入单个编译结果
  content: "11";
  padding: 20px;
}
.mk-card .mk-card__title,  //传入数组编译结果
.mk-card .mk-card__body {
  content: "11";
  padding: 20px;
}

再来分析@if 这块的逻辑

@if hitAllSpecialNestRule($selector) {
    @at-root {
      #{$selector} {
        #{$currentSelector} {
          @content;
        }
      }
    }
  } @else {
    @at-root {
      #{$currentSelector} {
        @content;
      }
    }
  }

hitAllSpecialNestRule 是在 ==theme-chalk/src/mixins/function.scss== 中 定义的方法,其主要作用是判断父级选择器是否包含’–‘,’is-‘,’:’

如果包含则e方法中定义的样式要嵌套于改父级样式之下;若不包含,则通过@at-root跳出选择器嵌套,

/* 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);
}


  • containsModifier 方法是判断父级选择器是否包含’–‘
  • containWhenFlag 方法是判断 父级选择器是否包含’.is-‘
  • containPseudoClass 方法是判断 父级是否包含 ‘:’

理解这几个函数只需要弄清楚自定义函数selectorToString以及inspect和str-slice、str-index这三个sass内建函数 (就是sass自带函数)即可

  • inspect(value)使用inspect(value) 使用inspect(value)使inspect value)函数来生成一个对调试Map有用的输出字符串,因为Map无法转换为纯CSS。使用一个作为CSS函数的变量或参数的值将导致错误。

  • str-slice(string,string, string,start-at, end−at:−1)从end-at:-1) 从 endat:1)string 中截取子字符串,通过 start−at和start-at 和 startatend-at 设置始末位置,未指定结束索引值则默认截取到字符串末尾。

  • str-index(string,string, string,substring) 返回一个下标,标示 substring在substring 在 substringstring 中的起始位置。没有找到的话,则返回 null 值。这里$string必须为字符串

这里的下标都是从 1 开始

我们结合debug语句来看看selectorToString 的执行

//这里有人可能觉得这里是多此一举,但其实这里是通过inspect和str-slice把传入的选择器变量转换为字符串;因为在str-index 函数中传入的str-index($string, $substring)中,$string 必须要是一个字符串

@function selectorToString($selector) {
  @debug "初始化的选择器: #{$selector}";   //打印结果为.mk-card
  $selector: inspect($selector);
  @debug "inspect格式化后的选择器: #{$selector}";  //打印结果为 (.mk-card)
  $selector: str-slice($selector, 2, -2);
  @debug "str-slice格式化后的选择器: #{$selector}";  //打印结果为.mk-card;所以我觉得这里不是多余的么
  $selector: inspect($selector);
  @return $selector;
}



@include b(card) {
  border-radius: 4px;
  border: 1px solid #ebeef5;
  background-color: #fff;
  overflow: hidden;
  color: #303133;
  transition: 0.3s;

  @include e(footer) {
    //传入单个
    padding: 20px;
  }

}

再来分别看看containsModifier、containWhenFlag、containPseudoClass这几个函数

@function containsModifier、($selector) {
  $selector: selectorToString($selector);

    //看看当前选择器是否存在$modifier-separator这个变量也就是"--";
  @if str-index($selector, $modifier-separator) { 
    @return true;
  } @else {
    @return false;
  }
}

@function containWhenFlag($selector) {
  $selector: selectorToString($selector);
    //看看当前选择器是否存在$state-prefix这个变量也就是"is-";
  @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;
  }
}

我们可以看看含有这几个元素与不含这些元素的编译区别

//****************不含"--"、"is-"、 ":"
//编译前  不包含,则通过@at-root跳出选择器嵌套
.mk-card{
  $selector: &;
  @include e(footer) {
    //传入单个
    padding: 20px;
  }
}

//编译后
.mk-card__footer {
  padding: 20px;
}

//****************含"--"、"is-"、 ":"
//编译前 ;包含则嵌套于父级选择器下
.mk-card:hover {
  $selector: &;
  @include e(footer) {
    //传入单个
    padding: 20px;
  }
}
//编译后
.mk-card:hover .mk-card__footer {
  padding: 20px;
}

到这里,@mixin e($element) 的解析就完了

@mixin m($modifier)

@mixin m($modifier) {
  $selector: &;
  $currentSelector: "";
  @each $unit in $modifier {
    $currentSelector: #{$currentSelector +
      & +
      $modifier-separator +
      $unit +
      ","};
  }

  @at-root {
    #{$currentSelector} {
      @content;
    }
  }
}

其实与@mixin e(element)很相似,这里不做过多描述;与element)很相似,这里不做过多描述;与element)modifier-separator(–)拼接;我们看看编译结果就理解了

//编译前
@include b(card) {
  border-radius: 4px;
  border: 1px solid #ebeef5;
  background-color: #fff;
  overflow: hidden;
  color: #303133;
  transition: 0.3s;

  @include e(footer) {
    //传入单个
    padding: 20px;
  }

  @include m(primary) {
    background: #409eff;
  }
}

//编译后
.mk-card {
  border-radius: 4px;
  border: 1px solid #ebeef5;
  background-color: #fff;
  overflow: hidden;
  color: #303133;
  -webkit-transition: 0.3s;
  transition: 0.3s;
}

.mk-card__footer {
  padding: 20px;
}

.mk-card--primary {
  background: #409eff;
}

至此,清楚的了解B、E、M这三个混合的实现原理,剩下的其他混合其实就都可以理解了

@mixin configurable-m(modifier,modifier, modifier,E-flag: false)

不做过多分析,直接看编译结果就懂了

@mixin configurable-m($modifier, $E-flag: false) {
  $selector: &;
  $interpolation: "";

  @if $E-flag {
    $interpolation: $element-separator + $E-flag;
  }

  @at-root {
    #{$selector} {
      .#{$B + $interpolation + $modifier-separator + $modifier} {
        @content;
      }
    }
  }
}

@include b(card) {
  border-radius: 4px;
  border: 1px solid #ebeef5;
  background-color: #fff;
  overflow: hidden;
  color: #303133;
  transition: 0.3s;

  @include configurable-m(primary, task) {
    display: flex;
  }
  @include configurable-m(primary, false) {
    display: flex;
  }

}

//编译后
.mk-card .mk-card__task--primary {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
}

.mk-card .mk-card--primary {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
}

@mixin spec-selector(specSelector:””,specSelector: “”,specSelector:,element: E,E,E,modifier: false,block:block: block:B)

不做过多分析,直接看编译结果就懂了

@mixin spec-selector(
  $specSelector: "",
  $element: $E,
  $modifier: false,
  $block: $B
) {
  $modifierCombo: "";

  @if $modifier {
    $modifierCombo: $modifier-separator + $modifier;
  }

  @at-root {
    #{&}#{$specSelector}.#{$block
      + $element-separator
      + $element
      + $modifierCombo} {
      @content;
    }
  }
}

@include b(card) {
  border-radius: 4px;
  border: 1px solid #ebeef5;
  background-color: #fff;
  overflow: hidden;
  color: #303133;
  transition: 0.3s;

  @include e(footer) {
    //传入单个
    padding: 20px;

    @include spec-selector(
      $specSelector: "",
      $element: $E,
      $modifier: primary,
      $block: $B
    ) {
      display: flex;
    }
  }
}

//编译后
.mk-card {
  border-radius: 4px;
  border: 1px solid #ebeef5;
  background-color: #fff;
  overflow: hidden;
  color: #303133;
  -webkit-transition: 0.3s;
  transition: 0.3s;
}

.mk-card__footer {
  padding: 20px;
}

.mk-card__footer.mk-card__footer--primary {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
}

@mixin meb(modifier:false,modifier: false, modifier:false,element: E,E, E,block: $B)

不做过多分析,直接看编译结果就懂了

@mixin meb($modifier: false, $element: $E, $block: $B) {
  $selector: &;
  $modifierCombo: "";

  @if $modifier {
    $modifierCombo: $modifier-separator + $modifier;
  }

  @at-root {
    #{$selector} {
      .#{$block + $element-separator + $element + $modifierCombo} {
        @content;
      }
    }
  }
}

@include b(card) {
  border-radius: 4px;
  border: 1px solid #ebeef5;
  background-color: #fff;
  overflow: hidden;
  color: #303133;
  transition: 0.3s;

  @include e(footer) {
    //传入单个
    padding: 20px;

   @include meb($modifier: primary) {
      display: flex;
    }
  }
}

//编译后
.mk-card {
  border-radius: 4px;
  border: 1px solid #ebeef5;
  background-color: #fff;
  overflow: hidden;
  color: #303133;
  -webkit-transition: 0.3s;
  transition: 0.3s;
}

.mk-card__footer {
  padding: 20px;
}
.mk-card__footer .mk-card__footer--primary {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
}

@mixin when($state)

定义一些状态样式;不做过多分析,直接看编译结果就懂了

@mixin when($state) {
  @at-root {
    &.#{$state-prefix + $state} {
      @content;
    }
  }
}

@include when(active) {
  color: red;
}

//编译后
.is-active {
  color: red;
}

到这里 ,结合Element分析基于scss实现BEM的方法分析就结束了,此篇文章也是自己多sass的学习的一个总结,文章可能会有一些不足之处,欢迎大家批评指正,当然如果觉得这篇文章有干货,也可以点个赞赞鼓励一下哦;后期也会以sass在项目中的实际运用,模块化的架构做一次总结分析,敬请期待!

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。