工程

实现 Kibana UI 组件化,第 1 部分:可扩展的 CSS

本文面向大规模 CSS

面对数万行 CSS 代码、八层选择器嵌套,只能一级又一级地层层覆盖,直到……(惊吓脸)……!important 出现!有些人可能一提到这个怪物就犯怵,但是在 Kibana 团队,我们却能凭着灌了满肚子的咖啡和准备就绪的 Web 检查器,直面问题。 我是不是太夸张了?也许吧。但是编写可扩展 CSS 的艰难确是真真切切。在 Kibana 团队,我们采用格式化的方法来编写 CSS,从而解决这个问题:

  • 我们依托组件构思我们的 UI。
  • 我们用类简化我们的 CSS。
  • 我们用 BEM 命名规范让我们的标记和 CSS 更加可预测和可读。

我们用组件构建我们的 UI

组件是用户界面的构成要素,即工程师构建功能时粘合起来的按钮、表单 fields、模态窗口和其他视觉元素。

通过用组件编写 CSS,我们减轻了工程师的工作,因为他们可以直接获取现有的 CSS 和标记,不用自己编写。

围绕组件编写的 CSS 也更容易维护,因为组件自身会限制自己的样式范围,以免我们更改时出现任何意想不到的后果。

以我们的一个组件为例。我们称之为 Panel。

Our Panel component

组件既有视觉表现形式,也有代码表现形式,二者都使用同样的名称进行识别。这样我们就可以在讨论 UI 时使用一种通用语言。现在,设计师、工程师或产品经理无论什么时候提到“Panel”,参与对话的每一个人都能够准确地理解他们所指的概念。

接下来,我们来探索如何让我们的 CSS 保持简洁,然后再来看看我们 Panel 组件背后的 CSS。

简洁性是可扩展 CSS 的基础

简洁的代码活动部件较少,目标表述清晰。代码中的逻辑单元相互分离,相互之间既有清晰无歧义的交互,也有定义明确的边界。

简洁的 CSS 同样具有这些特质,但是我们简化 CSS 的方法不同于简化那些使用 JavaScript 或 Python 等命令式语言编写的代码所使用的方法。在 Kibana 团队,我们发现我们简洁性工具盒中最强大的工具就是 CSS

以下是我们使用类简化 CSS 时遵循的基本规则:

用类明确地将标记映射到呈现的 UI。 如果我们给类命名时将组件考虑在内,类名称就会变得富有意义。这些类名称将成为我们标记中的地标和路标。它们会帮助我们确定标记的位置、理解元素之间的关系、无需经常查看浏览器即可在心中形成 UI 的图像。

将选择器限制到一个单独的类。 选择器涉及多个类时,就会对上下文产生依赖性。也就是说它的样式需要在特定的元素关系下才能生效,而这个关系在读取标记时会很难发现。通过将各个选择器限制到一个单独的类,我们可以简化元素之间的关系,让我们的标记更容易读取,也可以减少我们更改上下文时导致意外破坏的可能性。

仅在“叶子”节点使用继承的属性。 CSS 将“font-size”(字体大小)和“color”(颜色)等属性应用到元素时,也会通过继承将这些属性应用到元素的子元素。这有时会带来意想不到甚至是不合需要的后果。对此,我们的解决方案是避免让继承操纵我们的 UI 外观,只将继承的属性用于不包含子元素的 UI 部件,例如表单标签和按钮。

我们来看看这些原则在我们 Panel 组件代码中的应用。以下是用于创建 Panel 的标记:

<div class="panel">
  <div class="panelHeader">
    <div class="panelHeader__title">
      Panel title
    </div>
  </div>

  <div class="panelBody">
    <!-- Content goes here -->
  </div>
</div>

注意我们如何通过在标记中使用类名称来清晰地确定 UI。可以很明显地看出,这是一个包含标题和正文的 Panel 组件。在 Kibana 团队,我们发现通过改善标记的可读性,还可以更快更可靠地构建 UI。

这是用于定义此标记外观的 CSS:

.panel {
  border-left: 2px solid #e4e4e4;
  border-right: 2px solid #e4e4e4;
  border-bottom: 2px solid #e4e4e4;
}

.panelHeader {
  display: flex;
  align-items: center;
  padding: 10px;
  height: 50px;
  background-color: #e4e4e4;
}

  /*
   * FYI, we indent child classes like this to emphasize its role
   * in the markup as a tightly-coupled child of the .panel class.
   */
  .panelHeader__title {
    font-size: 18px;
    line-height: 1.5;
  }

.panelBody {
  padding: 10px;
}

看看我们如何为各个选择器声明一个单独的类。这样我们能够非常容易地了解 CSS 如何作用于标记,并生成视觉 UI 组件。

最后,注意 .panelHeader__title class 类是唯一具有继承属性的选择器。这是因为我们知道除 Panel 标题中的文本外,应用此选择器的元素绝不包含任何子元素。这个例子说明了我们如何避免 CSS 的继承模式可能带来的问题。

BEM 命名规范

Yandex 公司于 2007 年 开发出 BEM 命名规范 以来,此命名规范的知名度大大提高。有关 BEM 细微差异的解释不在本篇博客范围之列,如有兴趣了解,建议阅读这篇 Harry Roberts 所著文章或这篇 CSS-Tricks 文章,了解更多信息。

我们之所以选择 BEM 作为我们的 CSS 命名规范,是因为我们在读取标记时,需要通过一种方法来识别构成组件的不同类之间的紧密联系。与此同时,我们需要通过一种方法来区分各个类在我们标记中发挥的作用 - 有些类发挥结构性作用,有些类用于修改其他类,有些类旨在通过 JavaScript 动态应用。BEM 为我们提供了一些样式,供我们区分这些不同的类。

如需更加详细地了解 BEM 工作原理,请参阅我上面提到的链接。

后续预告:组件化过程

本文是系列内容的第 1 部分。在接下来的文章中,我将分享我们有关组件化流程过程的收获,包括如何设计具备可重用性和可组合性的组件。感谢阅读!

如果您有兴趣加入我们的团队, 我们一直在寻觅杰出人才