使用 Sass 和 BEM 命名控制作用域

Avatar of Andy Bell
Andy Bell

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费信用额度!

在使用 CSS 和 Sass 时,您可能不会考虑控制作用域。我们已经可以使用 取地址符 (&) 一段时间了,它为我们提供了一定程度的作用域,但是当嵌套很深时,它很容易失去其用处。当我们完全忘记它的含义时,& 会将我们带到一条充满灾难的蜿蜒道路上,并且可能导致一些非常严重的代码膨胀。

同时,在 JavaScript 中,我们可以通过在 this 有意义的地方将其转换为变量来控制 this 的作用域。这通常在方法或对象的开头。

function foo() {
  let self = this;

  return function() {
            
    // Self = foo(), even int his closure
    return self;
  }
}
    
// We need to instantiate foo() or its 'this' will be the window
let bar = new foo();

现在,我们在上述上下文中有了 self,无论我们在函数中的哪个位置,始终都能引用 foo()。当我们在 setTimeout 场景、闭包或事件处理程序中时,这非常有用。

希望一个关于事件的快速示例可以帮助说明我的观点。

使用此标记

<div class="component">
  <div class="component__child-element"></div>
</div>
<div class="component">
  <div class="component__child-element"></div>
</div>

我们可以添加此 JavaScript 代码

function foo() {

  // Load up all component child elements
  let childElements = [...document.querySelectorAll('.component__child-element')];
   
  // Loop each element and add an event listener
  childElements.map(element => {

      element.addEventListener('click', function(evt) {
          
        // Log what `this` currently is
        console.log(this);
      });
  });
}

// Create a new instance of foo
let bar = new foo();

在该代码示例中,如果您在控制台打开的情况下单击 component__child-elementthis 将报告自身为事件目标,该目标碰巧是我们单击的元素。如果您认为它会引用 foo(),则这不是理想的结果。

现在,如果我们在事件处理程序中使用 self 代替 this 运行相同的示例,则控制台将报告 foo() 的实例。

function foo() {
    
  // Control scope of this by storing it
  let self = this;
  
  // Load up all component child elements
  let childElements = [...document.querySelectorAll('.component__child-element')];
 
  // Loop each element and add an event listener
  childElements.map(element => {
    element.addEventListener('click', function(evt) {
      
      // Self will report itself as `foo()`
      console.log(self);
    });
  });
}

// Create a new instance of foo
let bar = new foo();

那么,这与 Sass 有什么关系呢?

很高兴您能坚持看到这里,因为 JavaScript 示例是我们将在 Sass 中查看内容的入门。

首先,让我们从相同的标记和一些核心样式开始。

<div class="component">
  <div class="component__child-element"></div>
</div>
<div class="component">
  <div class="component__child-element"></div>
</div>
.component {
  display: block;
  max-width: 30rem;
  min-height: 30rem;
  margin: 5rem auto;
  background: rebeccapurple;
  position: relative;
  border: 1px dashed rebeccapurple;
    
  &__child-element {
    display: block;
    width: 15rem;
    height: 15rem;
    border-radius: 50%;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate3d(-50%, -50%, 0);
    background: white;
  }
}

这为我们提供了一个漂亮的紫色正方形,里面有一个白色圆圈。没什么突破性的,但对这个例子很有用。

查看 CodePen 上 Geoff Graham (@geoffgraham) 的 Pen 带圆圈的正方形

您可能会注意到我们正在使用 BEM 语法,所以让我们继续创建一个修饰符。

我们想要一个 .component 的反转版本,它具有白色背景和紫色圆圈。因此,让我们直接按照我们可能想到的方式进行操作,将其包含在相同的嵌套规则集中

.component {
  // Other styles redacted for brevity 
  
  // The reversed modifier flips the colors around
  &--reversed {
    background: white;
    border-color: lightgray;
    
    &__child-element {
      background: rebeccapurple;
    }
  }
}

查看 CodePen 上 Geoff Graham (@geoffgraham) 的 Pen 带圆圈的正方形

等等,为什么这不起作用?问题在于 & 的作用域是 .component--reversed,因此 &__child-element 编译为 .component--reversed__child-element,这在标记中不存在。

$self 来救援!

就像我们在之前看到的 JavaScript 一样,我们将把初始作用域转换为名为 $self 的变量。您可以这样做

.component {
  $self: &;
}

这意味着无论我们在组件中使用 $self 的哪个位置,它都将编译为 .component

所以让我们重构那个反转的修饰符,以便它真正起作用

.component {
  $self: &; // Hey look, it's our new best friend!
  display: block;
  max-width: 30rem;
  min-height: 30rem;
  // Other styles redacted
  
  &--reversed {
    background: white;
    border-color: lightgray;
    
    // Here, we use $self to get the correct scope
    #{ $self }__child-element {
      background: rebeccapurple;
    }
  }
}

该元素的编译后的 CSS 现在是 .component--reversed .component__child-element,它当然存在并且成功地将内部圆圈变为紫色

查看 CodePen 上 Andy Bell (@hankchizljaw) 的 Pen “使用 Sass 和 BEM 命名控制作用域”示例

进一步探索

我喜欢在我的组件中做的一件事是在元素本身中引用父级的修饰符,而不是像我们之前那样在修饰符中引用元素。这样做是为了使我的样式按元素分组。出于同样的原因,我也会将媒体查询放在元素中。

我最终得到如下代码

// We're still within .component where $self has been declared.
&__child-element {
  display: block;
  width: 15rem;
  height: 15rem;
  border-radius: 50%;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate3d(-50%, -50%, 0);
  background: white;
  
  // Flip the color when the parent is reversed
  #{ $self }--reversed & {
    background: rebeccapurple;
  }
}

$self 作为根上下文可以让我们轻松地以这种方式处理修饰符。因为 $self.component 并且 &.component__element,所以添加 --reversed 部分会生成 .component--reversed .component__element,这正是我们想要的。

查看 CodePen 上 Andy Bell (@hankchizljaw) 的 Pen “使用 Sass 和 BEM 命名控制作用域”示例

更高级的示例

现在让我们真正放开手脚,编写一些有趣的东西。我们将布局三个单独的网格。使用 $self,我将使用网格项本身内的兄弟选择器更改网格项的颜色。

查看 CodePen 上 Andy Bell (@hankchizljaw) 的 Pen “使用 Sass 和 BEM 命名控制作用域”示例

让我们看看影响颜色的 Sass 代码

&__item {
  // Item styles redacted for brevity

  // For each direct `.grid sibling`, make this item rebeccapurple
  #{ $self } + #{ $self } & {
    background: rebeccapurple;
  }
}

该选择器编译后的结果是 .grid + .grid .grid__item$self& 的组合使我们能够在元素(即我们的 &)的上下文中生成它。这对于在复杂的代码库中保持某种秩序非常强大。

总结

我们从 JavaScript 中汲取灵感,通过在根目录使用以下代码片段来控制 Sass 中 BEM 组件的作用域:$self: &。通过作用域控制,当我们深入到修饰符或子元素时,可以灵活地编写干净、组织良好、BEM 驱动的 CSS。

我希望这个小技巧能帮助您改进 Sass。如果确实如此,请在 Twitter 上与我联系或在下方发表评论。我很想看看使用 Sass 控制作用域如何帮助到您。