好的,身為您的專業科技部落格作家,我將為您撰寫一篇關於 Rails view_component gem 基礎應用的技術文章。這篇文章將專注於實用性,並提供工程師可立即應用的程式碼範例。
告別 Rails Partial Hell:使用 ViewComponent 打造可測試、可重用的 UI 組件
親愛的工程師與資訊應用夥伴們:
Rails 以其「約定優於配置」的哲學和快速開發聞名,特別是在處理 View 層時,_partials.erb 的設 計讓許多重複的 UI 片段得以重用。然而,隨著應用程式的成長與複雜化,_partials.erb 的檔案數量可能會爆炸性增長,邏輯與標籤交織不清,導致:
- 難以維護: 找到特定 UI 片段的程式碼變得困難。
- 難以測試: View 層的邏輯難以進行單元測試。
- 重複造輪: 類似的 UI 邏輯在不同 partials 中重複出現。
- 耦合性高: partials 往往與其使用的 View 或 Controller 緊密耦合。
這是不是聽起來很熟悉呢?今天,我們要介紹一個能徹底改變您 Rails View 層開發體驗的強大工具:view_component gem。它將「組件化」的思維帶入 Rails,讓您像在前端框架(如 React, Vue)中一樣,將 UI 元素封裝成獨立、可測試、可重用的組件。
為何需要 ViewComponent?
想像一下,您正在用樂高積木建造一座城市。傳統的 Rails partials 就像您每次都得從零開始雕刻一塊塊石頭來蓋房子。而 ViewComponent 則提供了已經組裝好的「門」、「窗戶」、「屋頂」等標準積木,您可以直接拿來堆疊,甚至可以為這些積木設計測試,確保它們在各種情境下都能正常運作。
具體來說,ViewComponent 帶來以下核心優勢:
- 強化的封裝性 (Encapsulation): 將 UI 邏輯、資料處理與 HTML 模板完全封裝在一個獨立的 Ruby 類別中,擺脫 View helper 的散 佈問題。
- 優異的可測試性 (Testability): 由於 ViewComponent 是一個普通的 Ruby 類別,您可以輕鬆地為其撰寫單元測試,確保組件在各種輸入下的行為正確,這是傳統 partials 難以做到的。
- 高度的可重用性 (Reusability): 設計好的組件可以在應用程式的任何地方重複使用,大幅減少重複程式碼,提高開發效率。
- 清晰的關注點分離 (Separation of Concerns): ViewComponent 強迫您將展示邏輯從 View 本身或 Controller 中分離出來,讓程式碼結構更清晰。
- 性能提升 (Potential Performance Boost): 相較於某些複雜的 partials,ViewComponent 在渲染時通常效率更高,因為它們更接近底層的 Ruby 物件。
ViewComponent 基礎應用
接下來,我們將一步步帶您了解如何開始使用 ViewComponent。
1. 安裝 view_component
首先,將 view_component gem 加入您的 Gemfile:
# Gemfile\ngem 'view_component', '~> 3.0' # 使用最新穩定版本\n
然後執行 bundle install:
bundle install\n
2. 建立一個 ViewComponent
Rails 提供了生成器來快速建立 ViewComponent:
rails generate view_component FlashMessage\n# 或使用縮寫\n# rails g vc FlashMessage\n
執行後,您會看到類似以下的檔案結構:
app/components/flash_message_component.rb\napp/components/flash_message_component.html.erb\ntest/components/flash_message_component_test.rb\n# 如果是 Rails 6.1+, 還會有:\napp/components/flash_message_component_preview.rb\n
-
flash_message_component.rb: 這是 ViewComponent 的核心 Ruby 類別,您將在此定義組件的邏輯、屬性、以及任何輔助方法。它會繼承ViewComponent::Base。 -
flash_message_component.html.erb: 這是組件的模板,包含所有 HTML 標籤。 -
flash_message_component_test.rb: 用於為組件撰寫測試。 -
flash_message_component_preview.rb: 允許您在開發模式下,獨立預覽組件的不同狀態,非常方便!
3. ViewComponent 的結構
以我們剛剛建立的 FlashMessageComponent 為例:
app/components/flash_message_component.rb
# frozen_string_literal: true\n\nclass FlashMessageComponent < ViewComponent::Base\n # 定義組件需要的屬性 (props)\n attr_reader :type, :message\n\n # initialize 方法用於接收外部傳入的參數\n # 這些參數會成為組件的「狀態」或「資料」\n def initialize(type:, message:)\n @type = type # 例如: :success, :error, :notice\n @message = message\n end\n\n # (可選) content 方法用於處理傳入 ViewComponent 區塊的內容\n # 如果沒有提供區塊內容,這個方法可以省略\n # def call\n # content # 返回區塊內容\n # end\n\n # 可以在這裡定義輔助方法,這些方法只對這個組件有效\n def css_class\n case @type\n when :success then "bg-green-100 border-green-400 text-green-700"\n when :error then "bg-red-100 border-red-400 text-red-700"\n when :notice then "bg-blue-100 border-blue-400 text-blue-700"\n else "bg-gray-100 border-gray-400 text-gray-700"\n end + " border px-4 py-3 rounded relative my-2" # 假設這裡使用 Tailwind CSS\n end\n\n # 可以定義一個 method 來判斷組件是否應該被渲染\n # 這樣可以在 View 中減
少條件判斷\n def render?\n @message.present?\n end\nend\n
app/components/flash_message_component.html.erb
<%# 在模板中可以直接存取 ViewComponent 類別中定義的實例變數和方法 %>\n<div class="<%= css_class %>" role="alert">\n <strong class="font-bold"><%= @type.capitalize %>!</strong>\n <span class="block sm:inline ml-2"><%= @message %></span>\n <span class="absolute top-0 bottom-0 right-0 px-4 py-3">\n <svg class="fill-current h-6 w-6 text-<%= @type == :error ? 'red' : 'green' %>-500" role="button" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><title>Close</title><path d="M14.348 14.849a1.2 1.2 0 0 1-1.697 0L10 11.819l-2.651 3.029a1.2 1.2 0 1 1-1.697-1.697l2.758-3.15-2.759-3.152a1.2 1.2 0 1 1 1.697-1.697L10 8.183l2.651-3.031a1.2 1.2 0 1 1 1.697 1.697l-2.758 3.152 2.758 3.15a1.2 1.2 0 0 1 0 1.698z"/></svg>\n </span>\n</div>\n
4. 在 View 中使用 ViewComponent
在您的 Rails View (例如 application.html.erb 或任何 .erb 檔案) 中,您可以使用 render 方法來渲染 ViewComponent:
最簡單的用法:
<%= render FlashMessageComponent.new(type: :success, message: "您的操作已成功!") %>\n
帶有內容區塊的用法 (Content Block):
有時候,您希望 ViewComponent 能夠包裹一些動態的內容,就像 <button>Click Me</button> 中的 "Click Me" 一樣。這時,您可以在 render 時傳入一個區塊 (block)。
首先,修改 flash_message_component.html.erb,使用 content 變數來渲染區塊內容:
<%# ... (省略部分標籤) ... %>\n <strong class="font-bold"><%= @type.capitalize %>!</strong>\n <span class="block sm:inline ml-2"><%= content %></span> <%# 這裡顯示從區塊傳入的內容 %>\n<%# ... (省略部分標籤) ... %>\n
然後在 View 中這樣使用:
<%= render FlashMessageComponent.new(type: :notice) do %>\n <strong>提醒:</strong> 這是一個透過區塊傳入的內容。\n<% end %>\n
注意: 當您在 FlashMessageComponent 的 .html.erb 中使用了 content,並且在 initialize 中傳入了 message 參數,則 message 和 content 會同時存在。您需要根據設計決定是要顯示 message 還是 content, 或兩者結合。常見的做法是:如果提供了 content 區塊,則組件渲染 content;如果沒有,則渲染 message。這可以在 flash_message_component.html.erb 中用條件判斷實現:
<% if content? %>\n <span class="block sm:inline ml-2"><%= content %></span>\n<% elsif @message.present? %>\n <span class="block sm:inline ml-2"><%= @message %></span>\n<% end %>\n
並在 flash_message_component.rb 中確保 render? 判斷涵蓋兩種情況:ruby
def render?
@message.present? || content? # 如果有 message 或有 content 區塊,則渲染
end
實戰範例:整 合 Flash 訊息到 application.html.erb
Rails 的 flash 訊息是開發者常用的功能,但其顯示邏輯往往散落在 application.html.erb 中,導致程式碼冗長且難以維護。使用 FlashMessageComponent,我們可以將其完美封裝。
app/components/flash_message_component.rb (如前所示)
# frozen_string_literal: true\n\nclass FlashMessageComponent < ViewComponent::Base\n attr_reader :type, :message\n\n VALID_TYPES = [:success, :error, :notice, :alert, :warning].freeze\n\n def initialize(type:, message:)\n # 確保 type 是有效的,並轉換成 symbol\n @type = type.to_sym if type.present?\n @message = message\n end\n\n def css_class\n base_classes = "border px-4 py-3 rounded relative my-2"\n case @type\n when :success then "bg-green-100 border-green-400 text-green-700"\n when :error, :alert then "bg-red-100 border-red-400 text-red-700"\n when :notice then "bg-blue-100 border-blue-400 text-blue-700"\n when :warning then "bg-yellow-100 border-yellow-400 text-yellow-700"\n else "bg-gray-100 border-gray-400 text-gray-700"\n end + " " + base_classes\n end\n\n def render?\n @message.present? && VALID_TYPES.include?(@type)\n end\nend\n
app/components/flash_message_component.html.erb (如前所示,但假設我們只顯示 @message 不處理 content 區塊)
<div class="<%= css_class %>" role="alert">\n <strong class="font-bold"><%= @type.capitalize %>!</strong>\n <span class="block sm:inline ml-2"><%= @message %></span>\n <span class="absolute top-0 bottom-0 right-0 px-4 py-3">\n <svg class="fill-current h-6 w-6 text-gray-500" role="button" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><title>Close</title><path d="M14.348 14.849a1.2 1.2 0 0 1-1.697 0L10 11.819l-2.651 3.029a1.2 1.2 0 1 1-1.697-1.697l2.758-3.15-2.759-3.152a1.2 1.2 0 1 1 1.697-1.697L10 8.183l2.651-3.031a1.2 1.2 0 1 1 1.697 1.697l-2.758 3.152 2.758 3.15a1.2 1.2 0 0 1 0 1.698z"/></svg>\n </span>\n</div>\n
app/views/layouts/application.html.erb
<!DOCTYPE html>\n<html>\n <head>\n <title>MyApp</title>\n <%= csrf_meta_tags %>\n <%= csp_meta_tag %>\n <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>\n <%= javascript_importmap_tags %>\n </head>\n <body>\n <% flash.each do |type, message| %>\n <%# 使用 ViewComponent 渲染 flash 訊息 %>\n <%= render FlashMessageComponent.new(type: type, message: message) %>\n <% end %>\n\n <%= yield %>\n </body>\n</html>\n
現在,無論您的 Controller 中如何設定 flash 訊息:
# app/controllers/posts_controller.rb\nclass PostsController < ApplicationController\n def create\n @post = Post.new(post_params)\n if @post.save\n flash[:success] = "文章發佈成功!"\n redirect_to @post\n else\n flash[:error] = "文章發佈失敗,請檢查資料。"\n render :new\n end\n end\nend\n
它都會透過 FlashMessageComponent 以統一的風格呈現,並且邏輯與樣式都在組件內部得到良好的管理。
預覽功能 (Preview)
view_component 提供了一個強大的預覽功能,讓您在開發時無需啟動整個應用程式,就能獨 立地查看和測試組件的各種狀態。
打開 app/components/flash_message_component_preview.rb:
# frozen_string_literal: true\n\nclass FlashMessageComponentPreview < ViewComponent::Preview\n def success_message\n render FlashMessageComponent.new(type: :success, message: "資料已成功儲存!")\n end\n\n def error_message\n render FlashMessageComponent.new(type: :error, message: "操作失敗,請重新嘗試。")\n end\n\n def notice_message_with_long_text\n render FlashMessageComponent.new(type: :notice, message: "這是一個較長的通知訊息,用來展示組件在不同內容長度下的表現。")\n end\n\n def custom_type_message\n render FlashMessageComponent.new(type: :warning, message: "警告:您的帳戶即將到期。")\n end\nend\n
啟動 Rails 伺服器,然後訪問 http://localhost:3000/rails/view_components (在 Rails 6.1+)。您會看到一個專門的介面,列出所有 ViewComponent 預覽,您可以點擊查看它們在不同參數下的渲染效果,這對於 UI 的視覺校對和狀態測試非常有用。
注意事項
-
何時使用 ViewComponent?
- 當您有可重用、具有自己邏輯或樣式的 UI 片段時 (例如:按鈕、卡片、表單欄位、導航列、flash 訊息)。
- 當您需要為 UI 片段撰寫獨立測試時。
- 當您發現 partials 變得越來越複雜,包含太多條件判斷或輔助方法時。
-
何時不使用 ViewComponent?
- 非常簡單、一次性的 HTML 片段,且不包含任何邏輯或變數時。此時
partial仍是快速簡潔的選擇。 - 您的應用程式規模很小,且 View 層結構相對簡單,過度導入組件化可能會增加不必要的抽象層。
- 非常簡單、一次性的 HTML 片段,且不包含任何邏輯或變數時。此時
-
與前端框架的結合:
- ViewComponent 可以很好地與 StimulusJS 或其他輕量級 JavaScript 框架結合。您可以使用 ViewComponent 渲染出帶有
data-controller屬性的 HTML 結構,然後讓 Stimulus 掌控其行為。這提供了一個強大的前端功能封裝模式。
- ViewComponent 可以很好地與 StimulusJS 或其他輕量級 JavaScript 框架結合。您可以使用 ViewComponent 渲染出帶有
-
命名慣例:
- 遵循 Rails 的命名慣例,Component 類別名稱以
Component結尾,檔案放在app/components。
- 遵循 Rails 的命名慣例,Component 類別名稱以
結語
view_component gem 為 Rails 開發者提供了一條清晰、現代化的道路,來構建強大、可測試且易於維護的 UI。它讓我們在享受 Rails 生態系的同時,也能應用前端組件化的最佳實踐。
如果您長期為散亂的 partials 所困擾,或希望能為您的 UI 增加更高層次的抽象與測試,那麼 view_component 絕對值得您一試!它將使您的 View 層程式碼更加整潔,開發效率更上一層樓。
現在就動手,將您的應用程式 View 層升級到組件化的新境界吧!