基于scss的bem方法的实现

712 阅读2分钟

导读

首先来看一个bem命名示例

.my-message-box{}
.my-message-box__header{}
.my-message-box__header--active{}

如果使用已经封装好的bem方法的话,那么可以写成

@include b('message-box') {
  border: 1px solid black;
  @include e('header') {
    color: gray;
    @include m('active') {
      color: red;
    }
  }
}

接下来我们来看一下bem方法是如何实现的

sass语法回顾

  1. !global 变量提升,将局部变量提升为全局变量,在其他函数体内也能访问到此变量
  2. @at-root将父级选择器直接暴力的改成根选择器
  3. #{}插值,可以通过#{}插值语法在选择器和属性名中使用SassScript变量

bem方法解析

首先根目录下建目录theme/BEM,BEM下创建三个文件config.scss,function.scss,mixin.scss

config.scss

$namespace: 'my';
$element-separator: '__';
$modifier-separator: '--';
$state-prefix: 'is-';

里面定义了几个变量

function.scss

@import './config'; 
/* BEM support functions
----------------------- */
@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);
}

里面定义了几个方法

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

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

  1. 使用inspect(value)使用inspect( value)函数来生成一个对调试Map有用的输出字符串,因为Map无法转换为纯CSS。使用一个作为CSS函数的变量或参数的值将导致错误
  2. str-slice(string,start-at, end−at:−1)从string 中截取子字符串,通过start−at和end-at 设置始末位置,未指定结束索引值则默认截取到字符串末尾
  3. str-index(string,substring) 返回一个下标,标示 substring在string 中的起始位置。没有找到的话,则返回 null 值。这里$string必须为字符串

mixin.scss

@import './function';
 
/* BEM
--------------------- */
 
@mixin b($block-name) {
    $B: $namespace + '-' + $block-name !global;
    .#{$B} {
        @content;
    }
}
 
@mixin e($element-name) {
    $E: $element-name !global;
    $selector: &;
    $currentSelector: '';
    @each $unit in $element-name {
        $currentSelector: #{$currentSelector+'.'+$B+$element-separator+$unit + ','};
    }
 
    @if hitAllSpecialNestRule($selector) {
        @at-root {
            #{$selector} {
                #{$currentSelector} {
                    @content;
                }
            }
        }
    }
    @else {
        @at-root {
            #{$currentSelector} {
                @content;
            }
        }
    }
}
 
@mixin m($modifier-name) {
    $selector: &;
    $currentSelector: '';
    @each $unit in $modifier-name {
        $currentSelector: #{$currentSelector+&+$modifier-separator+$unit+','};
    }
    @at-root {
      #{$currentSelector} {
          @content;
      }
    }
}
 
@mixin when($state) {
    @at-root {
        &.#{$state-prefix + $state} {
            @content;
        }
    }
}
 
@mixin pseudo($pseudo) {
    @at-root #{&}#{':#{$pseudo}'} {
      @content;
    }
}
b方法
  1. 使用变量的拼接运算;namespace在config.scss文件中有定义,为“el”,假设namespace 在config.scss文件中有定义,为“el”,假设namespace在config.scss文件中有定义,为“el”,假设block为card,则拼接结果为el-card
  2. !global 变量提升为全局变量
  3. 使用插值语法将$B这个选择器作为选择器使用
  4. @content 将引用混合后大括号外额外的样式
e方法

e方法调用了hitAllSpecialNestRule方法(判断父级选择器是否包含'--','is',':')

m方法

m方法流程和b一致,区别在拼接currentSelector字符串时,使用了父级选择器

全局引入

在vue.config.js中配置css如下即可

css: {
   loaderOptions: {    
     sass: {
       prependData: "@import '@/theme/BEM/minxin.scss';",
     },
   },
 },

代码里面就可以调用了

<template>
  <div class="home">
    <div class="my-message-box">
      <span class="my-message-box__header">gray</span>
      <span class="my-message-box__header--active">red</span>
    </div>
  </div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import HelloWorld from '@/components/HelloWorld.vue'; // @ is an alias to /src
 
@Component({
  components: {
    HelloWorld,
  },
})
export default class Home extends Vue {}
</script>
<style lang="scss">
@include b('message-box') {
  border: 1px solid black;
  @include e('header') {
    color: gray;
    @include m('active') {
      color: red;
    }
  }
}
</style>