在使用 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-element
,this
将报告自身为事件目标,该目标碰巧是我们单击的元素。如果您认为它会引用 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 控制作用域如何帮助到您。
我确实喜欢 BEM 语法(每天都在使用它),
但不得不承认,使用取地址符指令和您建议的
$self
来“组合”类名,对我来说感觉并不是正确的做法。我个人有两个想要避免的点
如果没有源代码映射(好吧,在我的项目中通常可以使用),在大型代码库中查找
.component__child-element
的来源可能会更困难,因为它会中断所有 .scss 文件的 Shift+Ctrl+F 搜索。“深度嵌套”可能会让人困惑,并且更难阅读,尤其是对于团队中非 Sass 专家而言。
我认为在描述
.component
的块后,在border: 1px dashed rebeccapurple;
后关闭它,然后开始一个.component__child-element
的块(使用完全手写的类名)并没有什么坏处。我明白 DRY 原则不再那么好了,但为了代码的清晰度,这样做会稍微好一些。
正如我之前所说,这是我多年来形成的个人最佳实践,但我乐于听取每个人的意见!
我同意你在这里的评论。我经常遇到这种情况,而且几乎每次都让人头疼,很难找到问题的所在。
太棒了,很高兴您能看到这一点,并认识到它不适合您个人。希望它能帮助到其他人 :)
如果您难以找到某些内容的位置,那么您真的需要查看 Harry Roberts 的 ITCSS。将其与 BEM 和 CSS 命名空间结合使用(搜索他的网站以了解他对这些内容的看法),它将改变您工作和计划项目的方式。将您的“块”命名为类似 .c-my-block { blah blah } 的东西。将其放在 components 层文件夹下的 _my-block.scss 部分文件(ITCSS 建议在文件名前缀添加其所属的层,但我将其放在了文件夹中),您将永远能够找到您要查找的内容。
只有那个“组件”存在于那个文件中,连同它所有的元素和修饰符。去看看吧,非常棒。它不是一个完美的系统,没有一个是,但它比我遇到过的任何其他系统都要好得多,因此我真的很希望CSS-Tricks能写一篇关于它的文章,以推动它的采用。
我发现这种方法的可搜索性存在问题。如果你想在你的项目中搜索
.component__child-element
怎么办?嵌套.component__child-element
真的有必要吗?很多用BEM编写代码的人嵌套
&
,因为它会生成单层选择器,所以在这种情况下,它非常有用 :)是的,我以前也用过
&--modifier
、&__element
嵌套在块内,但后来发现可搜索性的麻烦超过了它的用处。如果有一个Atom/VS Code扩展可以以某种方式找到以这种方式编写的项目,我可能会考虑重新使用它 :)刚刚阅读了一些关于架构的其他评论,我肯定也会进一步研究这方面的内容。
我对此有一个想法。我认为你在错误的地方寻找解决可搜索性是嵌套的SASS父选择器带来的问题。问题不在那里。问题在于你的SASS文件的大小和复杂性。
如果你的SASS文件如此庞大且复杂,以至于你经常依靠CMD+F查找/搜索来查找你的选择器(并且因为嵌套的父选择器而遇到麻烦),那么你的SASS文件就太大了。你需要将它们分解成更多更小的部分文件。这使得查找你想要的内容变得容易得多。一个好的经验法则是,如果需要在SASS文件中垂直滚动超过1或2“页”,那么它就太大了,需要更好地进行分隔。
嗨,很棒的解释文章!
我用这种方法解决了这个问题
这种方法有什么问题吗?
一点问题也没有!如果你想这样使用并且它对你有用,那就没问题! :)
问题是,如果
component
的名称发生更改,则需要手动更新它,从而消除使用&
的某些实用程序。我明白了!谢谢你们 :)
可读性似乎有点困难,但我认为这是因为有两件新东西对我来说是新的,作用域变量和用变量命名选择器。
文章中提出的示例可以让我们一目了然地看到
.component__child-element
在默认情况下以及在--reversed
父元素内的样式。虽然你提出的这个示例存在潜在的可维护性问题:在
&--reversed
声明块内为.component__child-element
设置样式,样式被分成两个块。在以后的某个时刻,这可能不太明显,因为这些声明在物理上距离更远。不知道你是否明白我的意思;)
我发现
$b
和$e
是块和元素的有用命名方案。在元素内部,$self
可能会令人困惑。这是一个很酷的想法!我喜欢BEM的原因之一是它的命名具有描述性。考虑到这一点,我可能会将这些变量命名为
$block
和$element
。:+1:
我非常喜欢这个想法。它已经让我重新思考了为我当前客户编写的一些新组件的方式。
自从Sass 3.3推出以来,我一直很喜欢这项技术——http://alwaystwisted.com/articles/2014-02-27-even-easier-bem-ing-with-sass-33——个人而言,并且根据过去4年的经验,我认为,如果你难以轻松找到相关的代码,因为你使用了&符号,这突显了需要更好地构建文件架构。
很高兴看到你在这方面取得的进展。我已经开始慢慢地用这种方法重构一些组件,到目前为止,它产生了显著的积极影响。
我也同意你的观点,难以找到代码突显了架构问题。
我喜欢这个想法,因为我喜欢嵌套以进行视觉组织,但这样也不会创建更长的选择器:) 就搜索而言,我认为只要你将组件原子化得足够多,并将每个组件分离到不同的scss导入文件中,就应该足够容易了。
谢谢,Andy!在使用BEM时,我总是遇到这个DRY问题。这是一个非常好的解决方法。
搜索代码可能是一个问题。但我没有遇到,原因如下。我通常将BEM与Brad Frost的原子设计方法结合使用。因此,我构建模式,每个模式都有自己的文件,这些文件直接映射到块(来自BEM语法)。结果是文件小巧易读。当我想查找
.block__element--modifier
的定义时,我只需打开_block.scss
文件,并且在几乎所有情况下,我都能在IDE的第一个视图中看到所有SCSS代码。这种方法还可以防止我进行过深的嵌套。也许这对某些人有用。
我也这样做。如果搜索对你来说是一个问题,只需将事物原子化更多即可。
如果
_block.scss
是一个很长的文件怎么办?或者你会通过创建子文件夹来进一步分离吗?例如,一个卡片组件:.card__heading
是否在它自己的文件夹中?如果你想在修改后的卡片中获取card__heading
怎么办?例如.card--subtle
。我很想知道你如何分解你的文件。目前,我只是在components
文件夹中使用一个注释良好的_card.scss
部分文件。我不会为块使用任何子文件夹。我也有几个长文件(超过100行),尤其是在原子部分。但对我来说这不是问题。因为当你花足够的时间为元素和修饰符找到好的命名时,文件可以是自解释的。当元素的规则很长时,它往往可以提取到一个单独的块中。当然,你必须定义(对于项目团队或你自己)什么是长——这是一个非常主观的问题。
元素或块的修改通常只影响某些规则。当你覆盖,比如说超过75%的规则(当有很多规则时),是否值得使用修饰符?在我看来,在这种情况下是否使用另一个块也取决于视觉外观。
很酷的想法,谢谢分享。你仍然最终会得到嵌套的CSS选择器,而不是BEM力求的完全扁平的CSS。
我已经完全扁平化了,将修饰符添加到块内的每个元素中并在那里添加修改。解决了
&
问题,如果修饰符名称是一个变量,它相当DRY,你最终会得到完全扁平的CSS,并且gzip会最大程度地减少类膨胀的影响。仅适用于修饰符,或者如果你在元素选择器上使用
$self
而不是&
。为了清楚起见,如果有人偶然发现了这一点将会生成
如果你运行
它将生成
可以肯定地说,对于网络上的大多数事物来说——平衡就是一切。
喜欢这个Andy!
谢谢Lucy!希望它能帮到你 :)
不幸的是,如果你正在使用Symfony Assetic和Sass,这将无法工作,因为Symfony Sass编译器将会中断,仅供参考。
这是我读到的关于这种方法的第一篇文章!写得很好。我已经使用它一段时间了,甚至在一些地方更进一步。正如我所看到的,我也将我的 Sass 文件保持得很小,就像乐高积木一样,这有助于扩展性。
我喜欢将某个元素的样式放在一个地方,所以我发现 & 技术很有帮助。
这是我上一个项目中的面包屑模块
然后,一旦你对这样写 CSS 感到舒服,你的下一步就可以使用 mixin 来帮助整理你的修饰符。
这是我 hero 组件中的一个例子
然后,一旦你开始在组件中使用 mixin,你就会发现它们也适用于 body 类,我喜欢称之为“状态类”。
假设我打开了网站菜单,这会在 body 中添加了一个类
然后你可以添加状态 mixin(以 s 为前缀)
然后在你的模块中的任何地方使用这些 mixin
你会发现,即使在将动画属性放在那里之后,你也可以用这种方式保持模块非常干净 :)
这里有谁写类似的 Sass 吗?