我们如何在 Rails 中构建 UI 组件

已发表: 2024-06-28

在大型 Web 应用程序中保持视觉一致性是许多组织面临的共同问题。 我们的 Flywheel 产品背后的主要 Web 应用程序是使用 Ruby on Rails 构建的,并且我们有大约多名 Rails 开发人员和三名前端开发人员在任何一天向其提交代码。 我们也非常重视设计(这是我们作为一家公司的核心价值观之一),并且拥有三名设计师,他们与我们的 Scrum 团队的开发人员密切合作。

两个人合作设计网站

我们的一个主要目标是确保任何开发人员都可以毫无障碍地构建响应式页面。 障碍通常包括不知道使用哪些现有组件来构建模型(这会导致代码库中包含非常相似的冗余组件),并且不知道何时与设计人员讨论可重用性。 这会导致不一致的客户体验、开发人员的挫败感以及开发人员和设计人员之间不同的设计语言。

我们已经经历了风格指南以及构建/维护 UI 模式和组件的方法的多次迭代,每次迭代都帮助解决了我们当时面临的问题。 我们相信,我们的新方法将为我们在未来很长一段时间内做好准备。 如果您在 Rails 应用程序中遇到类似的问题,并且您想从服务器端处理组件,我希望本文能给您一些想法。

一名留着胡子的男子坐在显示代码行的电脑显示器前,对着镜头微笑

在本文中,我将深入探讨:

  • 我们正在解决什么问题
  • 约束组件
  • 服务器端渲染组件
  • 我们不能使用服务器端组件的地方

我们正在解决什么问题

我们希望完全限制我们的 UI 组件,并消除以多种方式创建相同 UI 的可能性。 虽然客户(一开始)可能无法判断,但对组件没有限制会导致开发人员体验混乱,使维护变得非常困难,并且难以进行全局设计更改。

我们处理组件的传统方式是通过样式指南,其中列出了构建给定组件所需的全部标记。 例如,我们的 slat 组件的样式指南页面如下所示:

板条组件的样式指南页面

这种方法多年来一直运行良好,但当我们添加变体、状态或使用该组件的替代方法时,问题开始出现。 对于复杂的 UI,参考样式指南来了解要使用哪些类、要避免哪些类,以及输出所需变体所需的标记顺序变得很麻烦。

通常,设计人员会对给定组件进行少量添加或调整。 由于样式指南不太支持这一点,因此使该调整正确显示的替代方法(例如不恰当地蚕食另一个组件的一部分)变得非常普遍。

无约束组件示例

为了说明不一致是如何随着时间的推移而出现的,我将使用 Flywheel 应用程序中我们的组件之一的一个简单(且人为的)但非常常见的示例:卡片标题。

从设计模型开始,这就是卡头的样子。 它非常简单,只有一个标题、一个按钮和一个底部边框。

 .card__header
  .card__header-左
    %h2 备份
  .card__header-right
    = link_to "#" 做
      = 图标("plus_small")

编码后,想象一下设计师想要在标题左侧添加一个图标。 开箱即用,图标和标题之间不会有任何边距。

 ...
  .card__header-左
    =图标(“arrow_backup”,颜色:“gray25”)
    %h2 备份
...

理想情况下,我们会在卡片标题的 CSS 中解决这个问题,但对于这个例子,假设另一个开发人员认为“哦,我知道! 我们有一些保证金助手。 我只是在标题上加上一个助手类。”

 ...
  .card__header-左
    =图标(“arrow_backup”,颜色:“gray25”)
    %h2.--ml-10 备份
...

好吧,从技术上讲,这看起来就像模型一样,对吧?! 当然可以,但假设一个月后,另一个开发人员需要一个卡头,但没有图标。 他们找到最后一个示例,复制/粘贴它,然后简单地删除图标。

看起来又是正确的,对吧? 断章取义,对于没有敏锐设计眼光的人来说,当然! 但看看它旁边的原件。 标题上的左边距仍然存在,因为他们没有意识到需要删除左边距助手!

更进一步,假设另一个模型要求没有底部边框的卡片标题。 人们可能会在风格指南中找到一种称为“无边界”的状态并应用它。 完美的!

然后,另一个开发人员可能会尝试重用该代码,但在这种情况下,他们实际上需要一个边框。 假设他们忽略了样式指南中记录的正确用法,并且没有意识到删除无边界类会给他们带来边界。 相反,他们添加了一条水平线。 最终标题和边框之间有一些额外的填充,因此他们对 hr 应用了一个辅助类,瞧!

对原始卡头进行了所有这些修改后,我们现在的代码变得一团糟。

 .card__header.--无边框
  .card__header-左
    %h2.--ml-10 备份
  .card__header-right
    = link_to "#" 做
      = 图标("plus_small")
  %hr.--mt-0.--mb-0

请记住,上面的示例只是为了说明不受约束的组件如何随着时间的推移而变得混乱。 如果我们团队中的任何人试图发布卡头的变体,则应该通过设计审查或代码审查来发现。 但像这样的事情有时确实会被忽视,因此我们需要防弹!


约束组件

您可能认为上面列出的问题已经通过组件得到了明确的解决。 这是一个正确的假设! 正是出于这个目的,React 和 Vue 等前端框架非常受欢迎; 它们是封装 UI 的神奇工具。 然而,它们有一个我们并不总是喜欢的问题——它们要求您的 UI 由 JavaScript 呈现。

我们的 Flywheel 应用程序的后端非常繁重,主要是服务器渲染的 HTML,但幸运的是,组件可以有多种形式。 归根结底,UI 组件是样式和设计规则的封装,可将标记输出到浏览器。 通过这种实现,我们可以对组件采用相同的方法,但无需 JavaScript 框架的开销。

我们将在下面介绍如何构建受限组件,但以下是我们通过使用它们发现的一些好处:

  • 将组件组合在一起的方式永远不会有错误。
  • 该组件为您完成所有设计思考。 (你只需传递选项即可!)
  • 创建组件的语法非常一致且易于推理。
  • 如果需要对组件进行设计更改,我们可以在组件中更改一次,并确信它会在所有地方进行更新。

服务器端渲染组件

那么我们通过约束组件来谈论什么呢? 让我们深入挖掘吧!

如前所述,我们希望任何在应用程序中工作的开发人员都能够查看页面的设计模型,并能够立即无障碍地构建该页面。 这意味着创建 UI 的方法必须 A)有很好的文档记录,B)非常声明性且无需猜测。

部分救援(至少我们是这么认为的)

我们过去尝试过的第一个尝试是使用 Rails 部分。 部分是 Rails 为您提供的模板中可重用性的唯一工具。 自然,它们是每个人首先接触到的东西。 但是依赖它们有很大的缺点,因为如果您需要将逻辑与可重用模板结合起来,您有两种选择:在使用部分的每个控制器之间复制逻辑或将逻辑嵌入部分本身。

部分确实可以防止复制/粘贴重复错误,并且在您需要重用某些内容的前几次,它们可以正常工作。 但根据我们的经验,随着对越来越多的功能和逻辑的支持,部分很快就会变得混乱。 但逻辑不应该存在于模板中!

细胞简介

幸运的是,有一个比部分更好的替代方案,它允许我们重用代码并将逻辑保留在视图之外。 它称为 Cells,是 Trailblazer 开发的 Ruby gem。 Cell 早在 React 和 Vue 等前端框架流行之前就已经存在了,它们允许您编写处理逻辑模板的封装视图模型。 它们提供了视图模型抽象,而 Rails 并没有真正做到开箱即用。 事实上,我们在 Flywheel 应用程序中使用 Cells 已经有一段时间了,只是不是在全球范围内、超级可重用的规模上使用。

在最简单的层面上,Cells 允许我们像这样抽象一大块标记(我们使用 Haml 作为我们的模板语言):

 %div
  %h1 你好,世界!

变成一个可重用的视图模型(此时与部分视图非常相似),并将其变成这样:

 = 细胞(“你好世界”)

这最终帮助我们将组件限制在不修改单元本身的情况下无法添加辅助类或不正确的子组件的位置。

构建细胞

我们将所有 UI Cell 放在 app/cells/ui 目录中。 每个单元只能包含一个 Ruby 文件,后缀为 _cell.rb。 从技术上讲,您可以使用 content_tag 帮助程序直接在 Ruby 中编写模板,但我们的大多数 Cell 还包含相应的 Haml 模板,该模板位于由组件命名的文件夹中。

一个没有逻辑的超级基本单元看起来像这样:

 // 单元格/ui/slat_cell.rb
模块用户界面
  类 SlatCell < ViewModel
    定义显示
    结尾
  结尾
结尾

show 方法是实例化单元格时呈现的内容,它会自动在与单元格同名的文件夹中查找相应的 show.haml 文件。 在本例中,它是 app/cells/ui/slat(我们将所有 UI Cell 范围限定为 UI 模块)。

在模板中,您可以访问传递到单元格的选项。 例如,如果单元格在 = cell(“ui/slat”, title: “Title”, subtitle: “Subtitle”, label: “Label”) 这样的视图中实例化,我们可以通过 options 对象访问这些选项。

 // cells/ui/slat/show.haml
.slat
  .slat__inner
    .slat__内容
      %h4= 选项[:标题]
      %p= 选项[:字幕]
      =图标(选项[:图标],颜色:“蓝色”)

很多时候,我们会将简单元素及其值移动到单元格中的方法中,以防止在选项不存在时呈现空元素。

 // 单元格/ui/slat_cell.rb
定义标题
  返回除非选项[:标题]
  内容标签:h4,选项[:标题]
结尾
默认字幕
  返回除非选项[:字幕]
  内容标签:p,选项[:字幕]
结尾
// cells/ui/slat/show.haml
.slat
  .slat__inner
    .slat__内容
      = 标题
      = 副标题

使用 UI 实用程序包装单元格

在证明了这可以大规模工作的概念之后,我想解决调用单元格所需的无关标记。 它只是不太流畅并且很难记住。 所以我们为它做了一个小帮手! 现在我们可以调用= ui“name_of_component”并内联传递选项。

 = ui“slat”,标题:“标题”,副标题:“副标题”,标签:“标签”

将选项作为块而不是内联传递

再进一步考虑一下 UI 实用程序,我们很快就会发现,一个单元格在一行上包含一堆选项将非常难以理解,而且非常丑陋。 下面是一个包含许多内联定义选项的单元格示例:

 = ui “slat”,标题:“标题”,副标题:“副标题”,标签:“标签”,链接:“#”,tertiary_title:“第三”,禁用:true,清单:[“项目 1”,“项目” 2”,“第 3 项”]

这非常麻烦,这导致我们创建一个名为 OptionProxy 的类,它拦截 Cells setter 方法并将其转换为哈希值,然后将其合并到选项中。 如果这听起来很复杂,别担心——这对我来说也很复杂。 以下是我们的一位高级软件工程师 Adam 编写的 OptionProxy 类的要点。

以下是在单元中使用 OptionProxy 类的示例:

 模块用户界面
  类 SlatCell < ViewModel
    定义显示
      OptionProxy.new(self).yield!(选项,&block)
      极好的()
    结尾
  结尾
结尾

现在,有了这个,我们就可以将繁琐的内联选项变成一个更令人愉快的块了!

 = ui“板条”做|板条|
  - slat.title = "标题"
  - slat.subtitle = "副标题"
  - slat.label =“标签”
  - slat.link =“#”
  - slat.tertiary_title = "第三"
  - slat.disabled = true
  - slat.checklist = [“项目 1”,“项目 2”,“项目 3”]

逻辑介绍

到目前为止,这些示例尚未包含有关视图显示内容的任何逻辑。 这是 Cells 提供的最好的东西之一,所以让我们来谈谈它!

坚持我们的 slat 组件,我们有时需要将整个事物渲染为链接,有时将其渲染为 div,具体取决于是否存在链接选项。 我相信这是我们唯一可以呈现为 div 或链接的组件,但它是 Cells 强大功能的一个很好的例子。

下面的方法根据选项[:link]的存在来调用 link_to 或 content_tag 帮助器。

 def 容器(&块)
  标签=
    如果选项[:链接]
      [:link_to,选项[:link]]
    别的
      [:内容标签,:div]
    结尾
  发送(*标签,类:“slat__inner”,&block)
结束

这允许我们用容器块替换模板中的 .slat__inner 元素:

 .slat
  = 容器做
  ...

我们经常使用的 Cells 中逻辑的另一个例子是条件输出类。 假设我们向单元格添加了一个禁用选项。 单元格的调用中没有其他任何变化,除了您现在可以传递一个禁用:true选项并观察整个事物变成禁用状态(灰色且带有不可点击的链接)。

 = ui“板条”做|板条|
  ...
  - slat.disabled = true

当禁用选项为 true 时,我们可以在模板中的元素上设置获得所需禁用外观所需的类。

 .slat{ 类: possible_classes("--disabled": 选项[:disabled]) }
  .slat__inner
    .slat__内容
      %h4{ 类: possible_classes("--alt": 选项[:禁用]) }= 选项[:标题]
      %p{ 类: possible_classes("--alt": 选项[:禁用]) }=
      选项[:字幕]
      =图标(选项[:图标],颜色:“灰色”)

传统上,我们必须记住(或参考样式指南)哪些单个元素需要额外的类才能使整个元素在禁用状态下正常工作。 单元格允许我们声明一个选项,然后为我们完成繁重的工作。

注意: possible_classes 是我们创建的一个方法,允许以良好的方式有条件地应用 Haml 中的类。


我们不能使用服务器端组件的地方

虽然单元方法对于我们的特定应用和我们的工作方式非常有帮助,但我不能说它解决了我们 100% 的问题。 我们仍然编写 JavaScript(大量)并在整个应用程序中使用 Vue 构建相当多的体验。 75% 的情况下,我们的 Vue 模板仍然存在于 Haml 中,并且我们将 Vue 实例绑定到包含元素,这使我们仍然可以利用单元方法。

然而,在将组件完全限制为单文件 Vue 实例更有意义的地方,我们不能使用 Cells。 例如,我们的选择列表都是 Vue. 但我觉得没关系! 我们还没有真正遇到需要在 Cells 和 Vue 组件中拥有重复版本的组件,因此有些组件 100% 使用 Vue 构建,有些组件使用 Cells 构建,这没关系。

如果一个组件是用 Vue 构建的,则意味着需要 JavaScript 在 DOM 中构建它,我们利用 Vue 框架来做到这一点。 不过,对于我们的大多数其他组件来说,它们不需要 JavaScript,如果需要,则需要已构建 DOM,我们只需挂接并添加事件侦听器即可。

随着我们在单元方法上不断取得进展,我们肯定会尝试单元组件和 Vue 组件的组合,以便我们拥有一种且唯一的一种创建和使用组件的方法。 我还不知道那是什么样子,所以我们到达那里后就会过那座桥!


我们的结论

到目前为止,我们已经将大约三十个最常用的视觉组件转换为单元格。 它给我们带来了生产力的巨大爆发,并让开发人员有一种验证感,即他们正在构建的体验是正确的,而不是被拼凑在一起的。

我们的设计团队比以往任何时候都更有信心,我们应用程序中的组件和体验与他们在 Adob​​e XD 中设计的组件和体验是 1:1。 现在,对组件的更改或添加仅通过与设计师和前端开发人员的交互来处理,这使团队的其他成员能够集中注意力,并且不用担心知道如何调整组件以匹配设计模型。

我们不断迭代约束 UI 组件的方法,但我希望本文中介绍的技术能让您了解什么对我们来说效果很好!


来和我们一起工作吧!

每个负责我们产品的部门都会对我们的客户和利润产生有意义的影响。 无论是客户支持、软件开发、营销还是介于两者之间的任何方面,我们都在共同努力,以实现我们的使命,即建立一家人们可以真正爱上的托管公司。

准备好加入我们的团队了吗? 我们正在招聘! 在这里申请。