likes
comments
collection
share

Sass在Element Plus组件库中的BEM架构详解

作者站长头像
站长
· 阅读数 73

平时我们写JS代码时会有一些架构的设计,css也不例外,它也有自己的自己架构设计,尤其是在组件库中。今天带来的是BEM架构在Element Plus组件库中的设计详解,包括代码的实现,以及最重要的——这样设计的目的是什么。

前置知识

BEM

BEM 是由 Yandex 团队提出的一种 CSS 命名方法论,是OOCSS模式的一种实现,BEM实际上是blockelementmodifier的缩写,分别为块层、元素层、修饰符层,命名格式为:

block-name__<element-name>--<modifier-name>-<modifier_value>
  • block与element使用__相接
  • modifier与前面修饰的结构用--相接(可以与block或element相接)
  • 修饰符的值用-与修饰符层相连

例如button组件

<button class="button">
  <div class="button__inner button__inner--primary">
    button
  </div>
</button>

在组件库中,每一个组件被分为一个个的block,组件中的每一块被分成了一个个的element,对blockelement的修饰被分成一个个的modifier,这样不但有效解决了css类名的命名冲突,也方便了我们维护。

调试

我们写的是scss文件,调试scss文件可使用sassnpm包来完成,先全局安装

npm install -g sass

先在项目中使用@debug打印在控制台或使用@error抛出错误。 然后对scss文件所在目录运行如下命令,可以使用-w开启监听模式,若scss文件或所依赖的文件发生变化,将自动重新编译

sass .\icon.scss icon.css -w

在你的scss文件没有错误的情况下,以上命令将icon.scss编译为icon.css文件。

组件库中的BEM模式有哪些写法

BEMscss一共有以下几种形态

  • 常规写法
  • b--m嵌套b__e
  • 伪类或表示状态的.is-选择器中嵌套b__e
  • 特殊选择符+、~ 等的衔接
  • 共享和继承
  • 多个element或多个modifier共享

组件库中常见的写法有这些,一个组件这样写还好,如果是所有的组件的样式都要加上__--等符号,写起来将会特别麻烦,接下来我们就看看怎么来解决这个问题。

常规写法

BEM架构最平常的写法就是这样,我们拿b、e、m分别为icon、item、color举例:

.icon {
  &__item {
    &--color {}
  }
}

block后面接elementelement后面接modifier,现在我们要简化的就是拼接__e--m的过程,所以我们使用定义混合指令``@mixin来表示b、e、m,在这之前我们先定义一些常量

$common-separator: '-' !default; // 公共的连接符
$element-separator: '__' !default; // 元素以__分割
$modifier-separator: '--' !default; // 修饰符以--分割
$state-prefix: 'is-' !default; // 状态以is-开头

然后定义三个@mixin

@mixin b($block) {
  .#{$block} {
      @content;
   }
}

@mixin e($element) {
  $selector: &; // & 里面保存着上下文,在这个 mixin 中其实指的就是 block

  @at-root {
    // @at-root 指规则跳出嵌套,写在最外层
    #{$selector + $element-separator + $element} {
      @content;
    }
 }
}

@mixin m($modifier) {
  $selector: &;

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

写一个例子试一下

@include b(icon) {
  --color: inherit;
  height: 1em;

  svg {
    height: 1em;
    width: 1em;
  }

  @include e(item) {
    width: 0.5rem;
    @include m(color) {
      color: aqua;
    }
  }
}

编译后的css如下

.icon {
  --color: inherit;
  height: 1em;
}
.icon svg {
  height: 1em;
  width: 1em;
}
.icon__item {
  width: 0.5rem;
}
.icon__item--color {
  color: aqua;
}

可以看到编译后的css并没有生成后代选择器,也就是类似.icon .icon__item,可以知道生成的scss文件中类名选择器并没有嵌套,而是拼接后被@at-root提到最外面,可以推知拼接类名后的scss的形式为:

.icon {
  --color: inherit;

  svg {
    height: 1em;
    width: 1em;
  }

  @at-root {
    .icon--item {
      color: aqua;

      @at-root {
        .icon__item--color {
          width: 0.5rem;
        }
      }
      
    }
  }
}

有同学疑惑到底是为啥需要@at-root,我们让只有blockelement的例子试一下如果没有@at-root会是怎样:

// 转换前
@include b(icon) {
  --color: inherit;

  svg {
    height: 1em;
    width: 1em;
  }

  @include e(item) {
    color: aqua;
  }
}

// 转换后的scss
.icon {
  --color: inherit;

  svg {
    height: 1em;
    width: 1em;
  }

  .icon--item {
    color: aqua;
  }
}

// 编译后的css
.icon {
  --color: inherit;
}
.icon svg {
  height: 1em;
  width: 1em;
}
.icon .icon--item {
  color: aqua;
}

加上modifier试一下

// 转换后的css
.icon {
  --color: inherit;
}
.icon svg {
  height: 1em;
  width: 1em;
}
.icon .icon__item {
  color: aqua;
}
.icon .icon__item .icon .icon__item--color {
  width: 0.5rem;
}

// 可以推知转换前的scss的形态
.icon {
  --color: inherit;

  svg {
    height: 1em;
    width: 1em;
  }

  .icon--item {
    color: aqua;

  	.icon__item--color {
      width: 0.5rem;
    }
  }
}

可以看到如果没有@at-root,将会无限嵌套。 所以得出@at-root的作用:在编译时不进行嵌套形成(类似.icon .icon__item这种选择器)

b--m嵌套b__e

b、e、m的顺序生成已经可以,但是我们可能要生成.icon--color内嵌.icon__item,也就是b--m内嵌b__e,我们先试一下如果直接写:

@include b(icon) {
  --color: inherit;

  @include m(color) {
    color: aqua;
    @include e(item) {
      width: 0.5rem;
    }
  }
}

// 编译后的css
.icon {
  --color: inherit;
}

.icon--color {
  color: aqua;
}

.icon--color__item {
  width: 0.5rem;
}

// 实际上期望的
.icon--color {
  color: aqua;
}
.icon--color .icon__item {
  width: 0.5rem;
}

所以当@mixin e()的父选择器的类名是modifier的时候不进行类名拼接,而是嵌套。 创建一个function.scss文件来放置函数,因为选择器是一个数组(scss的数组形式是(,,,)),而判断类名用到的字符串方法的操作对象是字符串,所以需要先将数组转为字符串,我们抽成一个函数:

@function selectorToString($selector) {
  $selector: inspect($selector); // Debug: (.icon--color,)
  $selector: str-slice($selector, 2, -2); // 把前后的括号删掉
  @return $selector;
}

关于Sass字符串的方法可以参考Sass String FunctionsSass inspect() Function 再判断modifier

// 判断是否有Modifier
@function containsModifier($selector) {
  $selector: selectorToString($selector);
	// str-index()返回 $modifier-separator在$selector的位置
  @if str-index($selector, $modifier-separator) {  
    @return true;
  } @else {
    @return false;
  }
}

我们还需要或许被嵌套的b__eblock的名称 block的名称可以通过传入的字符串$selector切割获得,也可以利用全局变量,这里贴上字符串切割代码

@import "function";
$modifier-string: selectorToString($selector);
$ms-index: str-index($modifier-string, $modifier-separator);
$end-index: $ms-index + 1;
$block: str-slice($modifier-string, 2, $ms-index - 1);

不过我们还是通过全局变量来实现更为简洁,在@mixin b()@mixin e()都加上全局变量,element的名称后面也可能用上 根据function.scss的函数改写@mixin e()@mixin e()里面判断上层是否是b--m,如果是,手动进行嵌套(因为@at-root的存在)且改变类名

@mixin b($block) {
  $B: $block !global;
  .#{$B} {
    @content;
  }
}

@mixin e($element) {
  $selector: &;
  $E: $element !global;

  @if containsModifier($selector) {
    // 如果这是一个b--m,也就是先走了@mixin m(),里面就变成b--m嵌套b__e
    @at-root {
      #{$selector} {  // b--m
        .#{$B + $element-separator + $E} {  // b__e
          @content;
        }
      }
    }
  } @else {
    @at-root {
      // @at-root 指规则跳出嵌套,写在最外层
      #{$B + $element-separator + $E} {
        @content;
      }
    }
  }
}

伪类或状态state中嵌套b__e

我们约定使用.is-xxx类名选择器来表示dom节点的状态,而且将伪元素和state也用@mixin表示

@mixin when($state) {
  $selector: &;
  @at-root {
    // 这里不是拼接,而是使用兄弟
    #{$selector + "." + $state-prefix + $state} {
      @content;
    }
  }
}
// 伪类
@mixin pseudo($pseudo) {
  $selector: &;
  @at-root {
    #{$selector + ":" + $pseudo} {
      @content;
    }
  }
}

当在@mixin e中没有处理外层的伪类和state时,依旧是直接将__e直接拼上去

@include b(icon) {
  @include pseudo(hover) {
    @include e(side) {
      color: blueviolet;
    }
  }
  @include when(click) {
    @include e(side) {
      color: darkcyan;
    }
  }
}

// 编译后的css
.icon:hover__side {
  color: blueviolet;
}

.icon.is-click__side {
  color: darkcyan;
}

所以这里的处理方法和前面b--e嵌套b__e一样,即判断外层是否有表示状态的.is-或伪类,我们将这几个判断合并在一起写在function.scss

// 判断是否有表示状态的 .is-
@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;
  }
}

// 判断是否含有 Modifier、表示状态的 .is- 和 伪类
@function hitAllSpecialNestRule($selector) {
  @return containsModifier($selector) or containWhenFlag($selector) or containPseudoClass($selector);
}

然后将@mixin econtainsModifier换成hitAllSpecialNestRule即可 再编译一下上面的scss

// 编译后的css
.icon:hover .icon__side {
  color: blueviolet;
}

.icon.is-click .icon__side {
  color: darkcyan;
}

特殊选择符拼接

在使用>+等符号时,右边的选择器就得自己写了,比如:

@include b(icon) {
  &:focus + .icon__item {
    position: inherit;
  }
}

为了方便,写一个@mixin spec-selector来进行特殊选择符的拼接

@mixin spec-selector($specSelector: "", $element: $E, $modifier: false, $block: $B) {
  $modifierCombo: "";
  // 判断输出的是 b__e 还是 b__e--m
  @if $modifier {
    // 如果是b__e--m,就将m接上
    $modifierCombo: $modifier-separator + $modifier;
  }

  @at-root {
    // 默认是父级用特殊符号接上目前的b__e(--m),bem都是可选的,
    #{&}#{$specSelector}.#{$block + $element-separator + $element + $modifierCombo} {
      @content;
    }
  }
}

@mixin spec-selector支持自定义右边选择器,默认是当前的b__e,这是在大部分应用场景下的情况,modifier默认为false,也就是不拼接上modifier

共享

首先,当有多个元素@extends一个占位符或者选择器时,会被编译为下面这样

Sass 额外提供了一种特殊类型的选择器:占位符选择器 (placeholder selector)。与常用的 id 与 class 选择器写法相似,只是 # 或 . 替换成了 %。必须通过 @extend 指令调用,更多介绍请查阅 @extend-Only Selectors

%share {
  position: absolute;
  left: 50%;
  top: 50%;
}

.test1 {
  @extend %share;
  color: antiquewhite;
}
.test2 {
  @extend %share;
}
.test3 {
  @extend %share;
}

// 编译后的css
.test3, .test2, .test1 {
  position: absolute;
  left: 50%;
  top: 50%;
}

.test1 {
  color: antiquewhite;
}

%share在其他作用域时,会被编译成下面这样

.box {
  %share-test {
    position: absolute;
    left: 50%;
    top: 50%;
  }
}

.test1 {
  @extend %share-test;
  color: antiquewhite;
}
.test2 {
  @extend %share-test;
}
.test3 {
  @extend %share-test;
}

// 编译后的css
.box .test3, .box .test2, .box .test1 {
  position: absolute;
  left: 50%;
  top: 50%;
}

.test1 {
  color: antiquewhite;
}

所以,如果当%share@include b()里面时,他的作用域时在.icon里面,编译出来的css选择器前面就会多出一个.icon,这个是不符合预期的,因为我们的element写在b里面只是为了拼接,而不是嵌套

@include b(item) {
  %share {
    position: absolute;
    left: 50%;
    top: 50%;
  }

  @include e(item) {
    @extend %share;
  }
}

// 会被编译成下面的css
.icon .icon__item {
  position: absolute;
  left: 50%;
  top: 50%;
}

// 也就是说scss是这样的
.icon {
  .icon__item {
    position: absolute;
    left: 50%;
    top: 50%;
  }
}

解决方法,就是利用@at-root%share提到@include b(item)外面去,这样%share就不在.icon里面了

@mixin share-rule($name) {
  $rule-name: "%shared-" + $name;
  @at-root #{$rule-name} {
    @content;
  }
}

@mixin extend-rule($name) {
  @extend #{"%shared-" + $name};
}

重新编译

@include b(item) {
    @include share-rule(position) {
    position: absolute;
    left: 50%;
    top: 50%;
  }
  @include e(item) {
    @include extend-rule(position);
  }
}

// 编译后
.icon__item {
  position: absolute;
  left: 50%;
  top: 50%;
}

多个element或多个modifier共享

如果有多个elementmodifier同理)使用同一套样式,在没有创建这些element之前(或者说这些element需要其他样式),我们需要这样写:

@include b(item) {
  @include share-rule(color-shared) {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
  }
  @include e(white) {
    @include extend-rule(color-shared);
  }
  @include e(black) {
    @include extend-rule(color-shared);
  }
  @include e(origin) {
    @include extend-rule(color-shared);
  }
  @include e(blue) {
    @include extend-rule(color-shared);
  }
  // ...
}

要创建很多次element,每次都要去extend,特别麻烦;我们试试在创建element的时候就生成一套公用的样式:

@include b(icon) {
  // 传入不确定个参数
  @include e(white, black, origin, blue) {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
  }
}

让我们改写@mixin e(),里用参数数组传入多个参数

这里的$element...类似JS的拓展运算符,$element为数组

@mixin e($element...) {
  $selector: &;
  $E: $element !global;
  $currentSelector: "";
  // 用@each in遍历数组,在每一个选择器后面加逗号
  @each $unit in $element {
    $currentSelector: #{$currentSelector + "." + $B + $element-separator + $unit + ","};
  }
  @if hitAllSpecialNestRule($selector) {
    @at-root {
        #{$currentSelector} {
          @content;
        }
      }
    }
  } @else {
    @at-root {
      #{$currentSelector} {
        @content;
      }
    }
  }
}

编译后的css如下

.icon__white, .icon__black, .icon__origin, .icon__blue {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}

@mixin modifier()也是同样道理

@mixin m($modifier...) {
  $selector: &;
  $currentSelector: "";
  @each $unit in $modifier {
    // 这里$selector带了. ,不用加 .
    $currentSelector: #{$currentSelector + $selector + $modifier-separator + $unit + ","};
  }
  @debug $currentSelector;
  @at-root {
    #{$currentSelector} {
      @content;
    }
  }
}

测试一下

@include b(icon) {
  // 传入不确定个参数
  @include e(item) {
    @include m(primary, err) {
      color: red;
    }
  }
}

// 编译后
.icon__item--primary, .icon__item--err {
  color: red;
}

总结

可以大概总结一下这个过程:遇到bem、伪类、状态is-、特殊选择符就拼接,在element中遇到父选择器是modifier、状态is-、伪类就嵌套,通过混合指令,在组件库中我们能更好地使用BEM架构去构建样式。

相关代码链接:github.com/plutoLam/pl…

希望这篇文章对屏幕前的你有帮助,原创不易,欢迎点赞、收藏、转发、关注~~!

转载自:https://juejin.cn/post/7177185484177670205
评论
请登录