DEV Community

Cover image for partial 的救贖?view_component 介紹
Kevin Luo
Kevin Luo

Posted on

4 1

partial 的救贖?view_component 介紹

把前端封裝(encapsulation)是從 React.js 以來的趨勢,
Github 前年受 React 啟發表了一個 view_component 的 gem,
並用在 Github 裡。

不過講到抽出重複利用的 html,
Rails 的開發者應該本來就有把有複用的頁面元素抽出來變成 partial 的習慣,
那 view_component 是在重造輪子嗎?
view_component 想要解決 partial 常遇到的 3 種問題:

  1. Partial render 的速度慢
  2. Partial 裡常有天外飛來的 instance variable 或 helper
  3. Partial 不好寫單元測試

最近研究了一下 view_component,分享一下心得。

References

使用方式

在 Gemfile 中加入 view_component

# Gemfile
gem "view_component", require: "view_component/engine"
Enter fullscreen mode Exit fullscreen mode

安裝完後,新增一個 ExampleComponent
$ bundle exec rails g component ExampleComponent greeting
這樣會新增 3 個檔案:

  • app/components/example_component.rb
  • app/components/example_component.html.erb
  • test/components/exmample_component_test.rb

我們先忽略 test 檔案,把另兩個檔案內容改成以下內容:

# app/components/example_component.rb
class ExampleComponent < ViewComponent::Base
  def initialize(greeting:)
    @greeting = greeting
  end
end

# app/components/example_component.html.erb
<div>
  <span><%= @greeting %></span>
</div>
Enter fullscreen mode Exit fullscreen mode

view_component 的用法跟 partial 很像,一樣是 render。
將 new 出來的物件丟入 render 即可:

<%= render ExampleComponent.new(greeting: 'hi') %>
Enter fullscreen mode Exit fullscreen mode

結果是會將 erb 的渲染至網頁中:

  <div>
    <span>hi</span>
  </div>
Enter fullscreen mode Exit fullscreen mode

大致上最簡單的用法就是這樣了

關於資料流

ViewComponent 很明確能限制在 template 中出現的變數、方法,
一定是來自 ViewComponent Class 內定義的。

由於 instance_variable 跟 helper 的方法在 erb 中可以當成是全域的,
用 partial 如果不注意狂用的話,多年後的維護真的滿困難的...

知道所有東西都來自 ViewComponent class ,如此可大大減少天外飛來的非預期的現象,比較好追 code

關於測試

Rails 的 MVC 架構的 model 跟 controller 都很容易來寫測試,
唯獨 view 的不是很好寫 test。
但偏偏 view 除了佔大量程式碼,而且也是實際給用戶的介面,結果最少測,滿矛盾的。
partial 更是不能單獨測試,一定是配合 controller 真的畫出整個網頁的 html 或 end-to-end 的 system test(如用 capybara 測),對小型團隊來說,成本十分高。

ViewComponent 可以在測試中渲染單一 component,即解決 partial 無法 unit test 的問題。

修改 generator 產生的 test 檔案:

# test/components/example_component_test.rb
require "test_helper"
class ExampleComponentTest < ViewComponent::TestCase
  def test_component_rendering
    assert_equal(
      %(<span>Hello!</span>),
      render_inline(ExampleComponent.new(greeting: "Hello!")).css("span").to_html
    )
  end
end
Enter fullscreen mode Exit fullscreen mode

利用rails test test/components/example_component_test.rb
即可對 ExampleComponent 單獨做 Unit Test
alt text

關於速度

可以直接用 benchmark 來測速一下
我用的環境是

  • ruby 2.7.2p137
  • Rails 6.1.3

新增一個 partial 檔案 _hi.html.erb 跟上述的 ExampleComponent 來比較

# app/views/pages/_hi.html.erb
<span>hi!</span>
Enter fullscreen mode Exit fullscreen mode

以下程式碼用 3 種方式印 10000 次 hi ,並用 benchmark 紀錄:

  1. 直接寫在 erb 中就稱為 inline
  2. partial
  3. component
<% require 'benchmark'
   Benchmark.bmbm do |x|
     x.report "inline" do
       10000.times do %>
         <p>hi</p>
    <% end
     end
     x.report "partial" do
       10000.times do %>
        <%= render "hi" %>
    <% end
     end
     x.report "component" do
       10000.times do %>
        <%= render ExampleComponent.new(greeting: 'hi') %>
    <% end
     end%>
<% end %>

#結果(秒為單位)
#inline      0.002143   0.000183   0.002326  (  0.002353)
#partial     78.692460  0.785214   79.477674 ( 80.162131)
#component   0.061728   0.000816   0.062544  (  0.062694)
Enter fullscreen mode Exit fullscreen mode
  1. Inline 2ms, 平均 0.0002ms/次
  2. partial 80 sec,平均 8ms / 次
  3. Component 62ms, 平均 0.0062ms / 次

Inline 最快。Partial 雖然最慢,但其實 8ms 也是滿快的,
不過 view_component 好像快太多了...快1000倍xD

這是最簡單的測試,
view_component 的官網上是說比 partial 快 10 倍以上,應該有測過多一點情形啦

我想祕密應該就是在 view_component 改寫了 render 的方法
略過一堆找 template 的過程,
(我猜應該有加 cache 但找原始碼看不太出來,只有這裡有 link)。

其它用法

Slot

如果有寫 Vue.js ,對 Slot 的概念應該是不陌生,就是可從 template 外部塞入 html。
Slot 設定方法很多種,下面用 Named slot 方式加入一個 header slot

# app/components/example_component.rb
class ExampleComponent < ViewComponent::Base
  include ViewComponent::SlotableV2
  renders_one :header

  def initialize(greeting:)
    @greeting = greeting
  end
end

# app/components/example_component.html.erb
<div>
  <%= header %>
  <span><%= @greeting %></span>
</div>
Enter fullscreen mode Exit fullscreen mode

使用時,在 render 後帶入一個 block,並在指定 slot 中放入一個 Home 的聯結:

<%= render ExampleComponent.new(greeting: 'hi') do |c| %>
  <%= c.header do %>
    <%= link_to "Home", root_path %>
  <% end %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

結果就是 Home 的聯結會加入 header slot 的位置

<body>
  <div>
    <a href="/">Home</a>
    <span>hi</span>
  </div>
</body>
Enter fullscreen mode Exit fullscreen mode

Validations

可以加入 ActiveModel::Validations 來強迫 Component 一定要有某些 Slot 或傳入變數的形式,避免錯誤。
或者是說可以拋出錯誤,幫助查錯。

Sidecar(實驗中)

Component 可以有自己 scoped 的 css, js ,這樣就功能就可能可以跟前端框架比一比了。因為實驗中暫先不測試
但概念是這樣:
alt text
順便查一下什麼叫Sidecar
alt text

preview

前端自動測試常有個問題:測試都過,所有的行為都對了,但是真的打開瀏覽器時,顯示破版十分尷尬。
目前最好的方式還是靠人眼去看元件渲染出來的結果,
view_compoent 提供了預覽的輔助工具。
test/components/previews/example_component_preview.rb

class ExampleComponentPreview < ViewComponent::Preview
  def with_default_greeting
    render(ExampleComponent.new(greeting: "Example component default"))
  end
end
Enter fullscreen mode Exit fullscreen mode

可以利用產生出的路徑
http://localhost:3000/rails/view_components/example_component/with_default_title
就可以直接點進去看渲染出的元件了

對前端熟的人可能會知道 Storybook.js 這個測試框架,
可在瀏覽器中一次秀出不同情境下,元件顯示的樣貌,
方便 Developer 跟 UI 檢查。

ViewComponent 用的 storybook 的 gem 已經有人開發了
GitHub - jonspalmer/view_component_storybook: ViewComponent previews and testing in Storybook

小結

雖然較進階的用法還在實驗階段,
但已可看的出 Github 對他們這個設計非常滿意😂
怎麼說呢?view_component 的官網裡有寫說:

ViewComponent 完全不是一個創新的點子!有一堆 Gem 都做了一樣的事

基本上對自己的東西超有自信才會這樣寫吧xD
好像鼎泰豐說:小籠包是台灣常見的食物,哪裡都吃的到,沒什麼特別的。

Github 的人為了 view_component ,
已在 Rails 6.1 加入允許物件實作自己的 render 的方式:
只要該物件有render_in 的方法, render 時就會改去呼叫物件的 render_in。完全為 view_component 量身打造,不用再去 monkey patch ActionView 的 render。

而且 Github 已經把非常多元件做成 ViewComponent 的形式,
打算做成一個元件庫 Primer ViewComponents
還在 Beta 版本,但看來很完整,應該不久後就正式推出了(?)

我個人是覺得可嘗試把一些有自己行為的小元素做成 Component 看看,
比如按鈕、tag 之類的,長的很像但因狀態會有不同的顏色、大小之類的,搭配單元測試的話,可以確保它們在能預期的情形下是顯示正確的。

跟前端框架 React, Vue 來比較時,
view_component 沒有天生支援 track 狀態變化,
可能等 Sidecar 的功能正式推出後,會有更好的做法。

(不過也許跟 DHH 主推的 stimulus.js 或 Hotwire 配合會有奇效喔。)

Image of Docusign

Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more

Top comments (0)

Billboard image

Create up to 10 Postgres Databases on Neon's free plan.

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Try Neon for Free →