为了账号安全,请及时绑定邮箱和手机立即绑定

用CSS的`:has`选择器替换React代码

自打 CSS 出现以来……好吧,至少从 CSS 初期开始,我们就被告知 CSS 是级联的。这个名字本身就说明了这一点,它们是级联样式表。通过 CSS,一个元素可以对其内部的元素应用样式,然后是其内部的其他元素等等。但绝不可能反过来。一个元素不能通过任何其他方式(除了使用 JavaScript)将样式应用到其父元素。

到目前为止。

CSS 的 :has 选择器现在已被所有主要浏览器支持,我们现在实际上可以针对父元素进行选择。不仅如此,世界真的被彻底改变了。如果你像我一样,职业生涯起步于那个用透明 GIF 实现圆角的时代,那么今天的技术可能性会让你大吃一惊。

那么,除了作为一款酷炫的新玩具,它在React世界中实际上还有什么实际用途呢?让我们来看看三个非常有趣的用例。

什么是 :has 选择器?

如果你还记得,在标准的 CSS 中,我们可以这么干:

    .content .card {  
      background: #f0f0f0;  
    }  

    .content img {  
      margin: 1rem 0;  
    }

这段代码设置了.content .card类的背景颜色为#f0f0f0,并设置了.content img类的垂直边距为1rem。

这会将 .card 元素内的背景色改为浅灰色,并为 .content 元素内的图片周围添加边距,从而使图片在视觉上与文本分离。

我们还可以用 +~ 选择器来选下一个兄弟元素。如果这张图片紧跟着 .card 元素,我们可能想给它加点额外的边距,让它看起来更加独立。

    // 紧接在卡片元素后面的图片将
    // 边距会更大
    .content .card + img {  
      margin: 2rem 0;  
    }

你可以在这里查看代码示例。

然而,直到最近才,我们无法在“相反”的方向选择元素。如果我想改变紧跟在图片后面的 .card 元素的背景,例如,这在没有 JavaScript 的情况下是不可行的。或者,如果想要根据 .card 内部包含图片来对其进行不同的样式设置——这同样也不行。

新的 CSS 选择器 :has 解决了这个问题。

我想让.card 元素中的图片有粉红色的边框,其他元素都用灰色的边框。这很简单!

    /* 所有卡片的顶部边框都会是灰色 */
    .card {  
      border-top: 10px solid #f6f7f6;  
    }  

    /* 带有图片的卡片顶部边框会是粉色 */
    .card:has(img) {  
      border-top: 10px solid #fee6ec;  
    }

:has选择器也可以与其他选择器配合使用。我也想给后面跟着图片的卡片添加蓝色边框。当然没问题!我们可以用 + 组合器来实现这个功能。

    // 如果一张卡片后面跟着一张图片,则给它一个蓝色的边框
    .card:has(+ img) {  
      border-top: 10px solid #c4f4ff;  
    }

我们甚至可以更疯狂一点,编写这样的代码:“将绿色背景应用到一个没有h3标签的、内部包含img标签的、后面紧接着另一个.card元素的、且之后的任何位置包含一个img标签的.card元素上,但前提是后面跟着的.card元素包含多于一张图片。”

    // 看看这个样式选择器 ;)  
    .card:not(:has(h3)):has(img):has(+ .card):has(~ img):has(~ .card):has(> img:nth-child(1)) {  
      background-color: #c3dcd0;  
    }

看看下面的例子。

但是,当我们谈到我们自己的React应用程序时,这真的是我们想要做的事情吗?这样的样式会穿透组件边界,就像子选择器一样,非常严重。我们不是花了大约十年的时间来发明各种方法来防止这种情况吗?BEM、SASS、CSS-in-JS、CSS模块等等……我们尽一切可能将样式限制在其被应用的元素上。

我们为什么要突然改变一切,去做与最佳实践相反的事情?当然,除了每隔几年左右我们总是在React中这么做之外,也没有其他原因😂

答案是这样的:这样我们就可以移除一堆复杂的React代码!有时候,最好的React代码可能就是没有React代码。而且,尽管那个疯狂的选择器(真的,不要对你的同事这么玩),用CSS替换React可以简化逻辑,甚至有时候还能稍微提高性能。

我们来看看几个这样的例子。

元素的 :has 选择器 和 focus 状态

想象我们正在搭建一个任务板。这个任务板上会有许多卡片,每张卡片都有两个按钮:“打开”和“删除”。点击“打开”按钮会在模态窗口中显示卡片的完整内容。点击“删除”按钮就可以删掉这张卡片。

这个卡片的编码很简单:

    <div className="card">  
      这里有些文本  
      <div className="buttons">  
        <button>  
          <Open />  
        </button>  
        <button>  
          <Delete />  
        </button>  
      </div>  
    </div>

我们希望它能够完全通过键盘访问:使用制表键应该可以正常选择这些按钮。除此之外,我还希望使卡片在用户使用制表键选择按钮时高亮显示,以提高键盘用户的可访问性。当用户使用制表键选择任何这些按钮时,我希望该卡片稍微“突出”,并且同一列中的其他卡片变暗,以便突出当前的交互。此外,我还希望通过改变当前活动卡片的边框颜色来突出显示当前的交互。删除按钮的边框颜色为红色,打开按钮的边框颜色为绿色。

如果我们用 React 来实现这个功能,我们需要添加一个焦点事件监听器来检测当前哪个按钮处于激活状态,维护状态以便更改卡片本身的 className 属性,还需要以某种方式将该状态与父组件共享,以便其他卡片也可以进行更改。我们可能还需要引入 Context 或其他状态管理方案。不知不觉地,我们可能需要实现一个完整地焦点管理器,使得在每次标签切换时都重新渲染每张卡片。难怪我们在现实世界中很少看到这种复杂的交互效果,最好的情况是按钮有一致的轮廓。

不过,使用 :has 选择器,实现我刚刚提到的效果就变得非常简单。

第一步。给按钮添加一些 data- 属性,这样我们就可以选中它们,而无需依赖 className

    // 给按钮加上 data-action 属性:
    <button data-action="open">
      <Open />
    </button>
    <button data-action="delete">
      <Delete />
    </button>

第二步。找到带有突出的“删除”按钮的卡片组件并修改其CSS样式。

    /* 当删除按钮被选中时,卡片会放大并改变边框颜色 */
    /* 当卡片内的“删除”按钮获得焦点时 */
    .card:has([data-action='delete']:focus-visible) {  
      border-top: 10px solid #f7bccb;  
      box-shadow: 0 0 0 2px #f7bccb;  
      transform: scale(1.02);  
    }

第三步。找到带有突出“打开”按钮的卡片并修改其CSS样式

    /* 让卡片“放大”,并改变顶部边框颜色  

* 当卡片内的“打开”按钮获得焦点时  
     */
    .card:has([data-action='open']:focus-visible) {  
      /* 放大效果 */  
      transform: scale(1.02);  
      /* 顶部边框颜色 */  
      border-top: 10px solid #c3dccf;  
      /* 阴影效果 */  
      box-shadow: 0 0 0 2px #c3dccf;  
    }

第四步。这是最困难的一步:我们需要找到所有在带有打开或删除按钮的卡片前后的卡片,然后将它们变灰。这就对了。

    // 所有在聚焦“打开”按钮的卡片之后的卡片  
    .card:has([data-action='open']:focus-visible) ~ .card,  

    // 所有在聚焦“删除”按钮的卡片之后的卡片  
    .card:has([data-action='delete']:focus-visible) ~ .card,  

    // 所有在聚焦“打开”按钮的卡片之前的卡片  
    .card:has(~ .card [data-action='open']:focus-visible),  

    // 所有在聚焦“删除”按钮的卡片之前的卡片  
    .card:has(~ .card [data-action='delete']:focus-visible) {  
      filter: 灰度();  
      background-color: #f6f7f6;  
    }

最终结果:最漂亮的键盘导航体验,完全不使用JavaScript和React重新渲染!下面有可以尝试的实时示例。

:has 选择器,相关的内容

我觉得非常有趣且简单的一个“:has选择器”的用法是根据某些数据给东西加上颜色标签。

例如,让我们创建一个展示我们在商店里卖的产品的表格。这些产品属于特定的类别:比如说我们在线上卖办公用品、衣服,还有马匹。表格将包含几列,看起来类似这样:

并像这样编码

    ...  
    <tr>  
      <td>长筒袜</td>  
      <td>由... 创建</td>  
      <td>库存已满载</td>  
      <td>  
        <span className="category">  
          衣服  
        </span>  
      </td>  
    </tr>  
    ...

现在,我想通过在左边用类别颜色画一条边来微妙地强调哪一行属于哪个类别。当某个库存为空时,我想用红底突出显示该行,这样人们就会注意到。这就是我希望看到的效果。

在 React 中,我们通过 props 向 row 标签传递关于类别和库存的信息,甚至可能是第一个单元格中的内容。可能还需要为每种变化创建类名或甚至内部组件。这种做法在这情况下完全多余。

那我们就这样做吧。

第一步 在已经包含相关信息的单元格中,添加带有 data- 属性的信息。

    <tr>  
      <td>袜</td>  
      <td>创建自...</td>  
      <!-- 添加 data-category 属性 -->  
      <td data-inventory="full">库存已满</td>  
      <td>  
        <!-- 添加 data-category 属性 -->  
        <span className="category" data-category="衣物">  
          衣物  
        </span>  
      </td>  
    </tr>

步骤2. 使用 :has 属性为所有需要的部分用颜色标注。

如果某行中某个元素具有 data-category 属性,则给该行的第一个单元格添加不同颜色的边框。

    .table tr:has([data-category='服饰']) td:first-child {  
      border-left: 6px solid #f7bccb;  
    }  

    .table tr:has([data-category='办公用品']) td:first-child {  
      border-left: 6px solid #f4d592;  
    }  

    .table tr:has([data-category='宠物']) td:first-child {  
      border-left: 6px solid #c4f4ff;  
    }

如果某行具有 data-inventory 属性且值为 empty,则给该行加上红色背景。

    .table tr:has([data-inventory='empty']) {  
      background: #f6d0ce;  
    }

这段 CSS 代码用于将库存为空的行背景颜色设置为 #f6d0ce。

就这样——表格被分色得非常漂亮了。最酷的是,如果这些属性是从动态状态获取并且经常更新,整个行不需要重新渲染来更新这些颜色,只需要更新带有 data-attribute 的那个单元格就行。这不仅带来了一点性能提升,还让代码更简洁,是不是?

看看下面这个互动例子。

关于 :has 选择符和表单控件

最后,我特别喜欢的一个:has选择器的强大用例是根据表单元素的状态来调整元素样式。

例如,在一个可以禁用输入的表单中,我们也可以使输入的标签和描述看起来被禁用。

这个表单的代码大概如下:

    <form className="form">  
      <fieldset>  
        <label htmlFor="form-name">名字</label>  
        <input type="text" name="name" id="form-name" disabled value="Nadia" />  
        <div className="description">只需输入您的名字就好</div>  
      </fieldset>  

      <fieldset>  
        <label htmlFor="form-email">邮箱地址</label>  
        <input type="email" name="email" id="form-email" required />  
        <div className="description">我们不接受gmail邮箱</div>  
      </fieldset>  
    </form>

然后在 CSS 中,我们会在包含处于 :disabled 状态的 inputfieldset 中进行选择,并对 label.description 元素的样式进行设置。

    fieldset:has(input:disabled) label,  
    fieldset:has(input:disabled) .description {  
      color: #d6d6d6;  
    }

当然,也可以用焦点。我们可以在输入框获得焦点时,在左边添加一条线。

就这样做吧:

    fieldset:has(input:focus-visible) {  
      border-left: 10px solid #c4f4ff;  
    }

当我们创建一个带有复选框的列表时,我们可以很容易地高亮选中的行,而不必存储复选框的状态,或像通常那样为此创建一个.active类。

我们只需要一个CSS选择器就够了:

    .list-with-checkboxes li:has(input:checked) {  
      background: rgba(196, 244, 255, 0.3);  
    }

看看这里的实时预览:

这多酷啊,对吧?你最喜欢哪个 :has 相关的小技巧?在评论区里分享一下吧!

当然,还有很多用巧妙的选择器来简化我们 React 代码的好机会。这些只是我特别喜欢的几个例子。如果你想了解更多关于 :has 选择器,这里有一些我特别喜欢的文章,里面有很多类似的例子。

看看 CSS 在最近几年里如何发展,大约五年后,我们可能根本就不需要 React 了。😲 那将是多么有趣的一天!

本文最初发布于https://www.developerway.com。该网站有更多类似的文章。😉

可以看看《React 高级教程》来让你的 React 技能更上一层楼。

订阅 newsletter ,连接LinkedIn 关注我Twitter _以获取最新文章通知。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消