我们如何在飞轮的 Rails 中构建 UI 组件
已发表: 2019-11-16在大型 Web 应用程序中保持视觉一致性是许多组织共同面临的问题。 在飞轮,我们也不例外。 我们的主要 Web 应用程序是使用 Ruby on Rails 构建的,我们有大约 15 名 Rails 开发人员和 3 名前端开发人员在任何一天向它提交代码。 我们也很重视设计(这是我们作为一家公司的核心价值观之一),并且在我们的 Scrum 团队中拥有三位与开发人员密切合作的设计师。
我们的一个主要目标是确保任何飞轮开发人员都可以构建一个没有任何障碍的响应式页面。 障碍通常包括不知道使用哪些现有组件来构建模型(这会导致代码库中包含非常相似的冗余组件)以及不知道何时与设计人员讨论可重用性。 这会导致不一致的客户体验、开发人员的挫败感以及开发人员和设计人员之间的不同设计语言。
我们经历了几次迭代风格指南和构建/维护 UI 模式和组件的方法,每次迭代都有助于解决我们当时面临的问题。 我们现在正在采用一种新的(对我们而言)方法,我相信这将在很长一段时间内为我们奠定基础。 如果您在 Rails 应用程序中遇到类似问题,并且想从服务器端处理组件,希望本文能给您一些想法。
在本文中,我将深入探讨:
- 我们要解决的问题
- 约束组件
- 在服务器端渲染组件
- 我们不能使用服务器端组件的地方
我们要解决的问题
我们希望完全限制我们的 UI 组件,并消除以多种方式创建相同 UI 的可能性。 虽然客户可能无法分辨(起初),但对组件没有约束会导致开发人员体验混乱,使事情变得非常难以维护,并且难以进行全局设计更改。
我们处理组件的传统方式是通过我们的样式指南,其中列出了构建给定组件所需的全部标记。 例如,我们的 slat 组件的样式指南页面如下所示:
这工作得很好并且适合我们几年,但是当我们添加变体、状态或使用组件的替代方式时,问题开始蔓延。 对于复杂的 UI,参考样式指南以了解要使用哪些类以及要避免哪些类以及输出所需变化所需的标记顺序变得很麻烦。 通常,设计师会对给定的组件进行少量添加或调整。 由于样式指南并不完全支持这一点,因此使该调整正确显示的替代黑客(例如不恰当地蚕食另一个组件的一部分)变得令人恼火。
无约束组件示例
为了说明不一致是如何随着时间的推移而出现的,我将使用 Flywheel 应用程序中我们的一个组件的一个简单(并且人为的)但非常常见的示例:卡片标题。
从设计模型开始,这就是卡片标题的样子。 它非常简单,有一个标题、一个按钮和一个底部边框。
.card__header
.card__header-left
%h2 Backups
.card__header-right
= link_to "#" do
= icon("plus_small")
编码后,想象一个设计师想要在标题左侧添加一个图标。 开箱即用,图标和标题之间没有任何边距。
...
.card__header-left
= icon("arrow_backup", color: "gray25")
%h2 Backups
...
理想情况下,我们会在卡片标题的 CSS 中解决这个问题,但是对于这个例子,假设另一个开发人员认为“哦,我知道! 我们有一些保证金助手。 我会在标题上加一个助手类。”
...
.card__header-left
= icon("arrow_backup", color: "gray25")
%h2.--ml-10 Backups
...
好吧,从技术上讲,这看起来就像模型一样,对吧?! 当然可以,但是假设一个月后,另一个开发人员需要一个卡片标题,但没有图标。 他们找到最后一个示例,复制/粘贴它,然后简单地删除图标。
再次看起来正确,对吧? 断章取义,对于那些对设计没有敏锐眼光的人来说,当然! 但看看它旁边的原件。 标题上的左边距仍然存在,因为他们没有意识到需要删除左边距助手!
让这个例子更进一步,假设另一个模型需要一个没有底部边框的卡片标题。 人们可能会在样式指南中找到一种称为“无边界”的状态并应用它。 完美的!
然后另一个开发人员可能会尝试重用该代码,但在这种情况下,他们实际上需要一个边框。 假设他们忽略了样式指南中记录的正确用法,并且没有意识到删除无边框类会给他们带来边框。 相反,他们添加了一条水平线。 最终在标题和边框之间有一些额外的填充,所以他们对 hr 应用了一个辅助类,瞧!
通过对原始卡片标题的所有这些修改,我们现在在代码中手头一团糟。
.card__header.--borderless
.card__header-left
%h2.--ml-10 Backups
.card__header-right
= link_to "#" do
= icon("plus_small")
%hr.--mt-0.--mb-0
请记住,上面的示例只是为了说明不受约束的组件如何随着时间的推移变得混乱。 如果我们团队中的任何人试图发布卡片标题的变体,则应该通过设计审查或代码审查来发现它。 但是这样的事情有时会从裂缝中溜走,因此我们需要防弹的东西!
约束组件
您可能认为上面列出的问题已经用组件清楚地解决了。 这是一个正确的假设! 像 React 和 Vue 这样的前端框架非常受欢迎。 它们是封装 UI 的绝佳工具。 但是,我们并不总是喜欢它们的一个小问题——它们要求您的 UI 由 JavaScript 呈现。
Flywheel 应用程序的后端非常繁重,主要使用服务器呈现的 HTML——但对我们来说幸运的是,组件可以有多种形式。 归根结底,UI 组件是样式和设计规则的封装,可将标记输出到浏览器。 通过这种实现,我们可以对组件采用相同的方法,但没有 JavaScript 框架的开销。
我们将在下面讨论如何构建受约束的组件,但以下是我们发现使用它们的一些好处:
- 将组件放在一起从来没有真正错误的方法。
- 该组件为您完成所有设计思考。 (您只需传递选项!)
- 创建组件的语法非常一致且易于推理。
- 如果需要对组件进行设计更改,我们可以在组件中更改一次,并确信它会随处更新。
在服务器端渲染组件
那么我们通过约束组件在谈论什么? 让我们深入挖掘!
如前所述,我们希望在 Flywheel 应用程序中工作的任何开发人员都能够查看页面的设计模型并能够立即构建该页面而不会遇到任何障碍。 这意味着创建 UI 的方法必须是 A) 很好地记录和 B) 非常具有声明性并且没有猜测。
部分救援(或者我们认为)
我们过去尝试过的第一个尝试是使用 Rails 部分。 Partials 是 Rails 为您提供模板可重用性的唯一工具。 自然,它们是每个人最先接触到的东西。 但是依赖它们有很大的缺点,因为如果您需要将逻辑与可重用模板结合起来,您有两种选择:在使用部分的每个控制器之间复制逻辑或将逻辑嵌入到部分本身中。
Partials 确实可以防止复制/粘贴重复错误,并且在您需要重用某些内容的前几次它们可以正常工作。 但是根据我们的经验,partials 很快就会因为对越来越多的功能和逻辑的支持而变得混乱。 但是逻辑不应该存在于模板中!
细胞简介
幸运的是,有一个更好的替代部分,它允许我们重用代码并将逻辑保持在视图之外。 它被称为 Cells,是 Trailblazer 开发的 Ruby gem。 Cells 早在 React 和 Vue 等前端框架流行起来之前就已经存在,它们允许您编写封装的视图模型来处理逻辑和模板。 它们提供了一个视图模型抽象,Rails 并没有真正开箱即用。 实际上,我们已经在 Flywheel 应用程序中使用 Cells 有一段时间了,只是还没有在全球范围内实现超级可重用的规模。
在最简单的层面上,Cells 允许我们像这样抽象一大块标记(我们使用 Haml 作为我们的模板语言):
%div
%h1 Hello, world!
变成一个可重用的视图模型(此时与局部视图非常相似),并将其变成这样:
= cell("hello_world")
这最终帮助我们将组件限制在不修改单元格本身的情况下无法添加辅助类或不正确的子组件的位置。
构建细胞
我们将所有 UI 单元放在 app/cells/ui 目录中。 每个单元格必须只包含一个 Ruby 文件,后缀为 _cell.rb。 从技术上讲,您可以使用 content_tag 帮助器直接在 Ruby 中编写模板,但我们的大多数 Cell 还包含相应的 Haml 模板,该模板位于组件命名的文件夹中。
一个没有逻辑的超级基本单元看起来像这样:
// cells/ui/slat_cell.rb
module UI
class SlatCell < ViewModel
def show
end
end
end
show 方法是实例化单元格时呈现的内容,它会自动在文件夹中查找与单元格同名的相应 show.haml 文件。 在这种情况下,它是 app/cells/ui/slat (我们将所有 UI 单元都限定为 UI 模块)。
在模板中,您可以访问传递给单元格的选项。 例如,如果单元格在像 = cell(“ui/slat”, title: “Title”, subtitle: “Subtitle”, label: “Label”) 这样的视图中实例化,我们可以通过 options 对象访问这些选项。
// cells/ui/slat/show.haml
.slat
.slat__inner
.slat__content
%h4= options[:title]
%p= options[:subtitle]
= icon(options[:icon], color: "blue")
很多时候,我们会将简单元素及其值移动到单元格中的方法中,以防止在不存在选项时呈现空元素。
// cells/ui/slat_cell.rb
def title
return unless options[:title]
content_tag :h4, options[:title]
end
def subtitle
return unless options[:subtitle]
content_tag :p, options[:subtitle]
end
// cells/ui/slat/show.haml
.slat
.slat__inner
.slat__content
= title
= subtitle
使用 UI 实用程序包装单元格
在证明这可以大规模工作的概念之后,我想解决调用单元格所需的无关标记。 它只是不太正确,很难记住。 所以我们为它做了一个小帮手! 现在我们可以调用 = ui “name_of_component” 并内联传入选项。
= ui "slat", title: "Title", subtitle: "Subtitle", label: "Label"
将选项作为块而不是内联传递
将 UI 实用程序再深入一点,很快就会发现,一个包含一堆选项的单元格都在一行上会非常难以理解,而且非常难看。 这是一个包含许多内联定义选项的单元格示例:
= ui “slat", title: “Title”, subtitle: “Subtitle”, label: “Label”, link: “#”, tertiary_title: “Tertiary”, disabled: true, checklist: [“Item 1”, “Item 2”, “Item 3”]
它非常繁琐,导致我们创建了一个名为 OptionProxy 的类,它拦截 Cells 的 setter 方法并将它们转换为哈希值,然后将其合并到选项中。 如果这听起来很复杂,别担心——这对我来说也很复杂。 这是我们的高级软件工程师之一 Adam 编写的 OptionProxy 类的要点。
这是在我们的单元格中使用 OptionProxy 类的示例:
module UI
class SlatCell < ViewModel
def show
OptionProxy.new(self).yield!(options, &block)
super()
end
end
end
现在有了这些,我们可以将繁琐的内联选项变成一个更令人愉快的块!
= ui "slat" do |slat|
- slat.title = "Title"
- slat.subtitle = "Subtitle"
- slat.label = "Label"
- slat.link = "#"
- slat.tertiary_title = "Tertiary"
- slat.disabled = true
- slat.checklist = ["Item 1", "Item 2", "Item 3"]
引入逻辑
到目前为止,这些示例还没有包含任何关于视图显示内容的逻辑。 这是 Cells 提供的最好的东西之一,所以让我们来谈谈吧!
坚持使用我们的 slat 组件,我们有时需要将整个事物呈现为链接,有时将其呈现为 div,具体取决于是否存在链接选项。 我相信这是我们唯一可以呈现为 div 或链接的组件,但它是 Cells 强大功能的一个非常简洁的示例。
下面的方法调用 link_to 或 content_tag 帮助器,具体取决于选项[:link]
的存在。
注意:这是由 Adam Lassek 启发和创建的,他在帮助我们使用 Cells 构建整个 UI 开发方法方面具有巨大的影响力。
def container(&block)
tag =
if options[:link]
[:link_to, options[:link]]
else
[:content_tag, :div]
end
send(*tag, class: “slat__inner”, &block)
end
这允许我们用容器块替换模板中的 .slat__inner 元素:
.slat
= container do
...
Cells 中我们经常使用的另一个逻辑示例是条件输出类。 假设我们向单元格添加了一个禁用的选项。 除了您现在可以传递一个 disabled: true 选项并观察整个事情变成禁用状态(以不可点击的链接变灰)之外,在调用单元格时没有其他任何变化。
= ui "slat" do |slat|
...
- slat.disabled = true
当 disabled 选项为 true 时,我们可以在模板中的元素上设置类,以获得所需的禁用外观。
.slat{ class: possible_classes("--disabled": options[:disabled]) }
.slat__inner
.slat__content
%h4{ class: possible_classes("--alt": options[:disabled]) }= options[:title]
%p{ class: possible_classes("--alt": options[:disabled]) }=
options[:subtitle]
= icon(options[:icon], color: "gray")
传统上,我们必须记住(或参考样式指南)哪些单个元素需要额外的类才能使整个事物在禁用状态下正常工作。 单元格允许我们声明一个选项,然后为我们完成繁重的工作。
注意: possible_classes 是我们创建的一种方法,它允许以一种很好的方式有条件地在 Haml 中应用类。
我们不能使用服务器端组件的地方
虽然单元方法对我们的特定应用程序和我们的工作方式非常有帮助,但我会说它解决了我们 100% 的问题。 我们仍然编写 JavaScript(很多),并在整个应用程序中构建了相当多的 Vue 体验。 75% 的时间,我们的 Vue 模板仍然存在于 Haml 中,我们将 Vue 实例绑定到包含元素,这使我们仍然可以利用单元格方法。
但是,在将组件完全约束为单文件 Vue 实例更有意义的地方,我们不能使用 Cells。 例如,我们的选择列表都是 Vue。 但我觉得没关系! 我们还没有真正遇到过需要在 Cells 和 Vue 组件中拥有重复版本的组件,所以有些组件是 100% 使用 Vue 构建的,有些是使用 Cells 构建的。 如果一个组件是用 Vue 构建的,这意味着需要 JavaScript 在 DOM 中构建它,我们利用 Vue 框架来做到这一点。 但是,对于我们的大多数其他组件,它们不需要 JavaScript,如果需要,它们需要已经构建了 DOM,我们只需挂接并添加事件侦听器。
随着我们不断推进 cell 方法,我们肯定会尝试将 cell 组件和 Vue 组件结合起来,这样我们就只有一种创建和使用组件的方法。 我还不知道那是什么样子,所以当我们到达那里时,我们会过那座桥!
我们的结论
到目前为止,我们已经将大约 30 个最常用的视觉组件转换为 Cells。 它给我们带来了巨大的生产力爆发,并让开发人员有一种验证感,即他们正在构建的体验是正确的,而不是被破解在一起的。
我们的设计团队比以往任何时候都更有信心,我们应用程序中的组件和体验与他们在 Adobe XD 中设计的内容是 1:1 的。 现在,组件的更改或添加仅通过与设计师和前端开发人员的交互来处理,这使团队的其他成员保持专注,无需担心知道如何调整组件以匹配设计模型。
我们一直在迭代约束 UI 组件的方法,但我希望本文中介绍的技术能让您了解哪些方法对我们有效!
来飞轮工作吧!
在飞轮,每个部门都对我们的客户和利润产生有意义的影响。 无论是客户支持、软件开发、营销还是介于两者之间的任何事情,我们都在共同努力实现我们的使命,即建立一个人们可以真正爱上的托管公司。
准备好加入我们的团队了吗? 我们正在招聘! 在这里申请。