我們如何在 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 元件的方法,但我希望本文介紹的技術能讓您了解什麼對我們來說效果很好!


來和我們一起工作吧!

每個負責我們產品的部門都會對我們的客戶和利潤產生有意義的影響。 無論是客戶支援、軟體開發、行銷或介於兩者之間的任何方面,我們都在共同努力,以實現我們的使命,即建立一家人們可以真正愛上的託管公司。

準備好加入我們的團隊了嗎? 我們正在招募! 在這裡申請。