inStyle(在 Sass 中修改当前选择器 `&`)

Avatar of Filip Naumovic
Filip Naumovic

DigitalOcean 为您的旅程的每个阶段提供云产品。立即开始使用 $200 的免费积分!

以下是来自 Salsita Software 的 Filip Naumovic 的客座文章。Filip 创建了 一个 Sass 工具 来帮助解决我知道我经常遇到的问题。您很高兴地在 Sass 中进行嵌套。您可能已经深入了两三层,并且需要根据**某些父选择器**来设置变体的样式。您需要要么跳出嵌套并开始一个新的嵌套上下文,要么使用 `@at-root` 进行彻底更改。我会让 Filip 来讲述他的新工具改变了这一切的故事。

Sass 在清理我们的 CSS 代码库方面帮助了我们中的许多人。当它出现在场景中时,一些非常有用的模式出现了,并迅速被采用。

例如,`&` 的用法 和嵌套允许清理原本相当糟糕的代码块。

这个

.my-app { display: block; }
.my-app .widget { border-radius: 5px; }
.my-app .widget.blue { color: blue; }
.isIE6 .my-app .widget { background-image: url('fake-borders.png'); }
@media (max-width: 768px) { .my-app .widget { float: left; } }

变成了这个

.my-app {
  display: block;
  .widget {
    border-radius: 5px;
    &.blue {
      color: blue;
    }
    .isIE6 & {
      background-image: url("fake-borders.png");
    }
    @media (max-width: 768px) {
      float: left;
    }
  }
}

这对我们来说是一个多么积极的变化!

所有 `widget` 元素的样式变体都清晰地嵌套在自身下面,使用缩进作为相关性的视觉提示和查询生成器。

在这种情况下,当前选择器(`&`)为最常见的模式提供了一种快捷方式。对元素的变体进行样式设置,该变体是通过元素本身的属性或预先添加的父状态调用的。

嵌套的媒体查询是一个比较新的补充,但它们暗示着向样式缩进语法演变几乎是自然而然的。它易于阅读和导航,因为它以某种方式反映了熟悉的 DOM 结构,并将一个元素的所有样式都放在一个地方,同时仍然生成我们宝贵但有时很复杂的 选择器。

如今,嵌套和当前选择器功能存在于 Less、Sass 和 Stylus 中。喝点酒,几乎可以称之为标准。

一个经典的“你不能那样做”案例。

以上面的代码块为例,让我们添加 `my-app.expanded .widget` 的样式。

尽管我们拥有强大的工具,但我们很快发现自己只有有限的选择

选项 1

使用现代的 `@at-root` 指令(或 Stylus 中的 `/`),我们完全离开当前范围并重复完整的根查询以将相关的新的样式嵌套在 `widget` 下面,因为当前选择器无法帮助我们表达这种关系。

.my-app {
  display: block;
  .widget {
    border-radius: 5px;
    &.blue {
      color: blue;
    }
    .isIE6 & {
      background-image: url("fake-borders.png");
    }
    // repeating the root selector here
    @at-root .my-app.expanded .widget {
      color: red'
    }
    @media (max-width: 768px) {
      float: left;
    }
  }
}

这会创建难以阅读的代码,并且存在很多重复,尤其是在实际使用远远超过我们的小示例片段时。但是,它保持了我们辉煌的嵌套范式。

选项 2

我们在 `my-app` 下面创建一个新的代码块,并使用它来更改与 `expanded` 状态相关的 所有子元素。这意味着我们的 `widget` 现在在不同的位置进行样式设置,并且这种分离随着嵌套中每个元素中添加的每个状态而增加。

.my-app {
  display: block;
  .widget {
    border-radius: 5px;
    &.blue {
      color: blue;
    }
    .isIE6 & {
      background-image: url("fake-borders.png");
    }
    @media (max-width: 768px) {
      float: left;
    }
  }
  &.expanded .widget
     color: red;
  }
}

虽然这直接违背了我们的“将所有相关样式嵌套”梦想,但这是我们学会忍受的缺陷。你们中的许多人甚至可能会为这种模式辩护,因为它已经成为做事的方式很长时间了。

但是,为了选择起见,如果有一个*选项 3*,那会不会很棒?一种允许我们表达对 `my-app.expanded` 的简单更改,这种更改会影响我们的 `widget`,而无需逃离上下文?

这个想法在我心中困扰了相当长一段时间,即使只是出于我对自己的样式表的一种强迫症。我已经把它作为我的副业,试图在样式仓库中找到这个缺失的工具。

寻找选项 3

在深入研究这个主题时,我发现蜘蛛网、永恒的讨论和各种各样的建议,其中许多建议在当前选择器字符 `&` 中添加一些特殊语法。这样做意味着要花几个月的时间学习复杂的核心库,并进行 漫长的战争,这立即感觉像一个不可接受的负担。

其次,我认为 `&` 工作得很好,因为它清楚地表示了整个上下文,因此为它添加更多功能可能会存在问题。它只做一件事,而且做得很好,因此在目前看来,为它创建一个好伙伴似乎是一个更好的主意。

为了方便集成,我决定在预处理器语言级别实现这个想法,这样您就可以直接 `@import` 并使用它。如今,预处理器是功能强大的框架,所以为什么不呢?

我的首选是 Stylus,因为它太棒了。不幸的是,由于 问题 1703,当前选择器占位符目前无法在 mixin 函数内部修改。就像一个虔诚的信徒,我会一直等到 Stylus 修复它,但我必须继续寻找我可以*现在*实现的东西。

正如我所知,您不能解析 Less 中的当前选择器,所以它也被排除了。

另一方面,SassScript 被证明是一个强大的工具。虽然它缺乏许多用于字符串和数组操作的有用抽象,但手动制作这些函数是完全可能的。许多函数 已经由 Sass Prince Kitty Giraudel 提供

经过几个月的受控字符串恐怖……

inStyle for Sass 3.4+ 诞生了!

我知道,这个名字很俗气。但它暗示了功能,因为您希望它在实际代码中可读。Mixin 语法对预处理器用户来说已经很熟悉,因此拥有一个暗示性的名称来描述元素父级中的更改对我来说听起来很合理,因为它可以增加抵御陌生感的缓冲。

无论如何,所有这些都必须保持可读性,同时处理复杂的情况,否则它会失去目的,转而使用 `@at-root` 选择器方法或将代码嵌套在其他地方。我决定使用两个基本机制,我相信它们可以满足最卑鄙的需求,同时保持逻辑上简单的解析算法

使用 1)修改

对当前选择器中存在的复合元素的添加被证明可以处理大约 80% 的实际代码,就像我们的第一个示例试图实现的那样。

.my-app {
  display: block;
  .widget {
    border-radius: 5px;
    &.blue {
      color: blue;
    }
    .isIE6 & {
      background-image: url("fake-borders.png");
    }
    @include in(".my-app.expanded") {
      color: red; // .my-app.expanded .widget { };
    }
    @media (max-width: 768px) {
      float: left;
    }
  }
}

尝试这样阅读

在 `my-app.expanded` 状态中设置 `widget` 的样式。

该函数从嵌套底部到顶部搜索 `my-app` 元素的第一个出现位置(跳过当前元素),并将类 `expanded` 附加到它,返回一个新的选择器。

更长的查询和组合修改怎么办?

table {
  table-layout: fixed;
  thead {
    font-weight: normal;
    tr {
      height: 30px;
      td {
        background-color: #fafafa;
        &.user {
          font-weight: bold'
        }
        @include in('table.one tr.two:hover') {
          background-image: url(rainbow.png) // table.one thead tr.two:hover td { };
        }
      }
    }
  }
}

找到 `tr` 父级并使用 `two:hover` 进行修改。向上移动,还找到 `table` 并使用 `one` 进行修改,其他元素被跳过。

从新选择器中删除无关的多选择器

ul, ol {
  list-style: none;
  li {
    display: inline-block;
    a {
      text-decoration: underline;
      @include in("ol.links") {
        color: orange; // ol.links li a { };
      }
    }
  }
}

不可能的情况和无效的 CSS 查询会在编译时产生阻塞的 Sass 错误

table {
  table-layout: fixed;
  td {
    height: 30px;
    @include in("table^s&()#") {
      what: the; // ERROR, invalid CSS
    }
    @include in ("tr.green:hover") {
      border-color: green; // ERROR, no tr or tr.green to modify in &
    }
  }
}

在生产中进行崩溃测试(哈哈!)时,我发现另一个非常实用的需求,我无法仅通过修改父树来满足。事实上,它解决了上面的示例,因为您必须能够使用 `tr.green:hover` 来做到这一点。您只需要能够说出在哪里。

使用 2)插入

让我们假设以下内容

table {
  table-layout: fixed;
  thead {
    font-weight: normal;
  }
  tr {
    height: 30px;
  }
  td {
    background-color: #fafafa;
  }
}

您理想情况下会将 `table thead tr` 选择器嵌套在哪里?根据教条,您似乎必须像这样添加它

table {
  table-layout: fixed;
  thead {
    font-weight: normal;
    tr {
      height: 50px;
    }
  tr {
    height: 30px;
  }
  td {
    background-color: #fafafa;
  }
}

然而,所讨论的样式化元素是tr,你已经把它作为通用样式了,所以理论上,把它嵌套在自身下面作为变体,可能更接近你对关系的思考方式,填补了当前选择器&无法描述的空白。

在这种情况下,这意味着必须有一种简单的方法,可以在当前元素的某个位置插入一些选择器,同时还允许与复合修改组合。我无法想象在不添加特殊字符的情况下实现这一点,所以我选择了视觉上暗示的^ 插入符。

table {
  table-layout: fixed;
  thead {
    font-weight: normal;
  }
  tr {
    height: 30px;
    @include in("^thead") {
      height: 50px; // table thead tr { };
    }
  }
  td {
    background-color: #fafafa;
    @include in("table.blue-skin ^tbody") {
      background-color: blue; // table.blue-skin tbody td { };
    }
  }
}

在这种情况下,插入符将thead插入到当前或上次修改元素的上一级。更多的插入符意味着在当前选择器中进行更高的跳跃。

main {
  display: flex;
  > div {
    flex-grow: 1;
    span {
      display: inline-block;
      &.highlight {
        outline-style: dashed;
        @include in("^^.section.active") {
          outline-style: solid; // main .section.active > div span.highlight { };
        }
        @include in("^^^.section") {
          some: thing; // ERROR, inserting too high, it would equal to ".section &"
        }
      }
    }
  }
}

注意:&.highlightspan是同一个元素,所以插入将其视为嵌套中的一个步骤。

我认为 inStyle 在最简单的用例中表现出色,而这些用例也是最常见的。但如果需要,事情会变得更复杂。

使用 3) 高级组合

匹配算法允许你更疯狂地一次插入或修改多个复合元素。

.my-app {
  display: flex;
  section {
    flex: 1;    
    header {
      width: 100%;
      @include in("^^.wrapper ^.dialog)") {
        height: 50px; // .my-app .wrapper section .dialog header { };
      }
      @include in("^.overlay ^.sidebar") {
        position: fixed; // .my-app section .overlay .sidebar header { };
      }
      @include in("^.modifier section.next ^.parent") {
        opacity: 0; // .my-app .modifier section.next .parent header { };
      }
    }
  }
}
  1. .dialog 插入到header 的上一级,.wrapper 插入到上两级。
  2. .sidebar 插入到header 的上一级,.overlay 直接插入到其上一级。
  3. .parent 推到header 的上一级,用.next 修改section,然后将.modifier 推到其上一级。

这提醒我,也许你有一些反馈! 我一直在考虑在你想将多个复合元素直接彼此相邻插入时启用一些更简单的语法,比如@include in("^(.overlay .sidebar)"),或者改进解析器并启用更自然的@include in("^.overlay .sidebar")。告诉我你的想法!

使用一段时间后,我发现我大多数不方便的代码模式都可以通过简单地更改这里和那里的一个元素,或者将新的选择器推到某个位置并保持原位来轻松解决。不过,我必须诚实地说,就这个想法的本质而言,它可能会对你的常规代码组织产生相当大的影响。

我理解使用 inStyle 可能会引发激烈争论。我的同事似乎要么思想开明,要么不在乎,这两种情况都很棒。

如果你使用它,我希望正确的处理方式与任何其他工具一样:当它适合工作时使用。大量使用复杂的嵌套 mixin 可能不如直接编写完整的查询更易读,但另一方面,它可以简化大多数现实世界中的问题,同时保持精简的占用空间。

在不久的将来,我想让 Stylus 移植版本正常工作,也许还会创建一个 Atom 编辑器插件,在代码中提示显示生成的查询。

尝试解决 CSS 的“第一世界问题”很有趣,我希望你能认为这个主题至少值得讨论。这个项目是开源的,所以随时加入代码或反馈!

无论你喜欢还是讨厌,它都在 GitHub 上,这里有一个小小的 微型网站,这里还有 实时调试器,以备不时之需。

查看 CodePen 上 Salsita Software (@salsita) 编写的 inStyle 崩溃测试假人

感谢阅读!