人生就是不斷學習,調整與更新持續前進。

Rails使用 view_component gem 基礎應用

好的,身為您的專業科技部落格作家,我將為您撰寫一篇關於 Rails view_component gem 基礎應用的技術文章。這篇文章將專注於實用性,並提供工程師可立即應用的程式碼範例。


告別 Rails Partial Hell:使用 ViewComponent 打造可測試、可重用的 UI 組件

親愛的工程師與資訊應用夥伴們:

Rails 以其「約定優於配置」的哲學和快速開發聞名,特別是在處理 View 層時,_partials.erb 的設 計讓許多重複的 UI 片段得以重用。然而,隨著應用程式的成長與複雜化,_partials.erb 的檔案數量可能會爆炸性增長,邏輯與標籤交織不清,導致:

這是不是聽起來很熟悉呢?今天,我們要介紹一個能徹底改變您 Rails View 層開發體驗的強大工具:view_component gem。它將「組件化」的思維帶入 Rails,讓您像在前端框架(如 React, Vue)中一樣,將 UI 元素封裝成獨立、可測試、可重用的組件。

為何需要 ViewComponent?

想像一下,您正在用樂高積木建造一座城市。傳統的 Rails partials 就像您每次都得從零開始雕刻一塊塊石頭來蓋房子。而 ViewComponent 則提供了已經組裝好的「門」、「窗戶」、「屋頂」等標準積木,您可以直接拿來堆疊,甚至可以為這些積木設計測試,確保它們在各種情境下都能正常運作。

具體來說,ViewComponent 帶來以下核心優勢:

  1. 強化的封裝性 (Encapsulation): 將 UI 邏輯、資料處理與 HTML 模板完全封裝在一個獨立的 Ruby 類別中,擺脫 View helper 的散 佈問題。
  2. 優異的可測試性 (Testability): 由於 ViewComponent 是一個普通的 Ruby 類別,您可以輕鬆地為其撰寫單元測試,確保組件在各種輸入下的行為正確,這是傳統 partials 難以做到的。
  3. 高度的可重用性 (Reusability): 設計好的組件可以在應用程式的任何地方重複使用,大幅減少重複程式碼,提高開發效率。
  4. 清晰的關注點分離 (Separation of Concerns): ViewComponent 強迫您將展示邏輯從 View 本身或 Controller 中分離出來,讓程式碼結構更清晰。
  5. 性能提升 (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

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 參數,則 messagecontent 會同時存在。您需要根據設計決定是要顯示 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 的視覺校對和狀態測試非常有用。

注意事項

結語

view_component gem 為 Rails 開發者提供了一條清晰、現代化的道路,來構建強大、可測試且易於維護的 UI。它讓我們在享受 Rails 生態系的同時,也能應用前端組件化的最佳實踐。

如果您長期為散亂的 partials 所困擾,或希望能為您的 UI 增加更高層次的抽象與測試,那麼 view_component 絕對值得您一試!它將使您的 View 層程式碼更加整潔,開發效率更上一層樓。

現在就動手,將您的應用程式 View 層升級到組件化的新境界吧!