Lzh on GitHub

@extend

在设计页面时,通常会遇到这样的情况:一个类应具有另一个类的所有样式,以及自己的特定样式。例如,BEM 方法论鼓励使用修饰符类,这些修饰符类与块或元素类位于同一元素上。但这会使 HTML 变得混乱,容易因忘记同时包含两个类而产生错误,并可能将非语义的样式问题带入你的标记中。

在设计页面时,通常会遇到这样的情况:一个类应具有另一个类的所有样式,以及自己的特定样式。例如,BEM 方法论鼓励使用修饰符类,这些修饰符类与块或元素类位于同一元素上。但这会使 HTML 变得混乱,容易因忘记同时包含两个类而产生错误,并可能将非语义的样式问题带入你的标记中。

<div class="error error--serious">
  Oh no! You've been hacked!</div>
.error {
  border: 1px #f00;
  background-color: #fdd;
}

.error--serious {
  border-width: 3px;
}

Sass 的 @extend 规则解决了这个问题。其写法为 @extend <selector>,它告诉 Sass 一个选择器应该继承另一个选择器的样式。

.error {
  border: 1px #f00;
  background-color: #fdd;

  &--serious {
    @extend .error;
    border-width: 3px;
  }
}

当一个类扩展另一个类时,Sass 对所有匹配扩展器的元素进行样式设置,就像它们也匹配被扩展的类一样。当一个类选择器扩展另一个类选择器时,它的工作方式就像你在 HTML 中已经拥有扩展类的每个元素上都添加了被扩展的类一样。你只需编写 class="error--serious",Sass 就会确保它的样式就像它也拥有 class="error" 一样。

当然,选择器不只是单独用于样式规则中。Sass 知道要扩展选择器被使用的任何地方。这确保了你的元素样式与它们匹配被扩展的选择器时完全一样。

.error:hover {
  background-color: #fee;
}

.error--serious {
  @extend .error;
  border-width: 3px;
}
扩展在样式表的其余部分编译之后解析。特别是,它在父选择器解析之后发生。这意味着如果你 @extend .error,它不会影响 .error { &__icon { ... } } 中的内部选择器。这也意味着 SassScript 中的父选择器看不到扩展的结果。

工作原理

与将样式复制到当前样式规则中的混合宏不同,@extend 会更新包含被扩展选择器的样式规则,使其也包含扩展选择器。在扩展选择器时,Sass 进行智能统一

  • 它从不生成像 #main#footer 这样不可能匹配任何元素的选择器
  • 它确保复杂的选择器交织在一起,无论 HTML 元素的嵌套顺序如何,它们都能正常工作。
  • 它尽可能地修剪冗余选择器,同时仍确保特异性大于或等于扩展器的特异性。
  • 它知道一个选择器何时匹配另一个选择器匹配的所有内容,并且可以将它们组合在一起。
  • 它智能地处理组合器通用选择器和包含选择器伪类
.content nav.sidebar {
  @extend .info;
}

// This won't be extended, because `p` is incompatible with `nav`.
p.info {
  background-color: #dee9fc;
}

// There's no way to know whether `<div class="guide">` will be inside or
// outside `<div class="content">`, so Sass generates both to be safe.
.guide .info {
  border: 1px solid rgba(#000, 0.8);
  border-radius: 2px;
}

// Sass knows that every element matching "main.content" also matches ".content"
// and avoids generating unnecessary interleaved selectors.
main.content .info {
  font-size: 0.8em;
}
你可以使用选择器函数直接访问 Sass 的智能统一!selector.unify() 函数返回一个匹配两个选择器交集的选择器,而 selector.extend() 函数的工作方式与 @extend 类似,但作用于单个选择器
因为 @extend 会更新包含被扩展选择器的样式规则,所以它们的样式在层叠中的优先级是基于被扩展选择器的样式规则出现的位置,而不是基于 @extend 出现的位置。这可能会让人感到困惑,但请记住:这与你将扩展类添加到 HTML 时这些规则的优先级是相同的!

占位符选择器

有时你只想编写一个只打算被扩展的样式规则。在这种情况下,你可以使用占位符选择器,它们看起来像以 % 而不是 . 开头的类选择器。任何包含占位符的选择器都不会包含在 CSS 输出中,但扩展它们的选择器

.alert:hover, %strong-alert {
  font-weight: bold;
}

%strong-alert:hover {
  color: red;
}

私有占位符

模块成员一样,占位符选择器可以通过在名称开头添加 -_ 来标记为私有。私有占位符选择器只能在其定义的样式表内被扩展。对于任何其他样式表,它看起来就像该选择器不存在一样。

扩展范围

当一个样式表扩展一个选择器时,该扩展将只影响在上游模块中编写的样式规则——也就是说,那些通过 @use 规则或 @forward 规则被该样式表加载的模块,以及那些模块加载的模块,等等。这有助于使你的 @extend 规则更具可预测性,确保它们只影响你在编写它们时已知的样式。

如果你使用 @import 规则,扩展根本没有作用域。它们不仅会影响你导入的每个样式表,还会影响导入你的样式表的每个样式表,以及那些样式表导入的所有其他内容,等等。没有 @use,扩展是全局的。

强制和可选扩展

通常,如果一个 @extend 在样式表中没有匹配任何选择器,Sass 将会产生一个错误。这有助于防止拼写错误或在重命名选择器时忘记重命名继承它的选择器。需要被扩展选择器存在的扩展是强制的

然而,这可能不总是你想要的。如果你希望 @extend 在被扩展选择器不存在时什么也不做,只需在末尾添加 !optional

扩展还是混合宏?

扩展和混合宏都是在 Sass 中封装和重用样式的方式,这自然引发了何时使用哪一个的问题。当你需要使用参数来配置样式时,混合宏显然是必要的,但如果它们只是一块样式呢?

根据经验,当你表达语义类(或其他语义选择器)之间的关系时,扩展是最好的选择。因为一个带有 .error--serious 类的元素是一个 error,所以它扩展 .error 是有意义的。但对于非语义的样式集合,编写混合宏可以避免层叠问题,并使其更容易在后期进行配置。

大多数 Web 服务器使用一种非常擅长处理重复的相同文本块的算法来压缩它们提供的 CSS。这意味着,尽管混合宏可能会比扩展产生更多的 CSS,但它们可能不会显著增加你的用户需要下载的量。所以,选择最适合你用例的功能,而不是生成最少 CSS 的功能!

局限性

不允许使用的选择器

只有简单选择器——像 .infoa 这样的单个选择器——可以被扩展。如果 .message.info 可以被扩展,@extend 的定义会说匹配扩展器的元素将像它们匹配 .message.info 一样被样式化。这与同时匹配 .message.info 完全相同,因此编写它而不是 @extend .message, .info 没有任何好处。

同样,如果 .main .info 可以被扩展,它会做(几乎)与单独扩展 .info 相同的事情。细微的差别不值得让人困惑,看起来它在做一些实质上不同的事情,所以这也不被允许。

.alert {
  @extend .message.info;
  //      ^^^^^^^^^^^^^
  // Error: Write @extend .message, .info instead.

  @extend .main .info;
  //      ^^^^^^^^^^^
  // Error: write @extend .info instead.
}

HTML 启发式

@extend 交织复杂的选择器时,它不会生成所有可能的祖先选择器组合。它可能生成的许多选择器不太可能真正匹配真实的 HTML,而生成所有这些选择器会使样式表变得太大,而实际价值却很小。相反,它使用启发式:它假设每个选择器的祖先是自包含的,不会与其他选择器的祖先交织在一起。

header .warning li {
  font-weight: bold;
}

aside .notice dd {
  // Sass doesn't generate CSS to match the <dd> in
  //
  // <header>
  //   <aside>
  //     <div class="warning">
  //       <div class="notice">
  //         <dd>...</dd>
  //       </div>
  //     </div>
  //   </aside>
  // </header>
  //
  // because matching all elements like that would require us to generate nine
  // new selectors instead of just two.
  @extend li;
}

@media 中的扩展

虽然 @extend 允许在 @media 和其他 CSS at-rule 中使用,但它不允许扩展出现在其 at-rule 之外的选择器。这是因为扩展选择器仅在给定的媒体上下文中适用,并且没有办法在生成的选择器中保留该限制而不复制整个样式规则。

@media screen and (max-width: 600px) {
  .error--serious {
    @extend .error;
    //      ^^^^^^
    // Error: ".error" was extended in @media, but used outside it.
  }
}

.error {
  border: 1px #f00;
  background-color: #fdd;
}