DEV Community

loading...
Cover image for partial 的救贖?view_component 介紹

partial 的救贖?view_component 介紹

LuoKevin
Hi, I'm developer from Taiwan
・3 min read

把前端封裝(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 配合會有奇效喔。)

Discussion (0)