把前端封裝(encapsulation)是從 React.js 以來的趨勢,
Github 前年受 React 啟發表了一個 view_component
的 gem,
並用在 Github 裡。
不過講到抽出重複利用的 html,
Rails 的開發者應該本來就有把有複用的頁面元素抽出來變成 partial 的習慣,
那 view_component 是在重造輪子嗎?
view_component 想要解決 partial 常遇到的 3 種問題:
- Partial render 的速度慢
- Partial 裡常有天外飛來的 instance variable 或 helper
- Partial 不好寫單元測試
最近研究了一下 view_component,分享一下心得。
References
- view_component 官網
- RailsConf 2019 - Rethinking the View Layer with Components by Joel Hawksley
- Encapsulating Ruby on Rails views - The GitHub Blog
使用方式
在 Gemfile 中加入 view_component
# Gemfile
gem "view_component", require: "view_component/engine"
安裝完後,新增一個 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>
view_component 的用法跟 partial 很像,一樣是 render。
將 new 出來的物件丟入 render 即可:
<%= render ExampleComponent.new(greeting: 'hi') %>
結果是會將 erb 的渲染至網頁中:
<div>
<span>hi</span>
</div>
大致上最簡單的用法就是這樣了
關於資料流
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
利用rails test test/components/example_component_test.rb
即可對 ExampleComponent 單獨做 Unit Test
關於速度
可以直接用 benchmark 來測速一下
我用的環境是
- ruby 2.7.2p137
- Rails 6.1.3
新增一個 partial 檔案 _hi.html.erb 跟上述的 ExampleComponent 來比較
# app/views/pages/_hi.html.erb
<span>hi!</span>
以下程式碼用 3 種方式印 10000 次 hi
,並用 benchmark 紀錄:
- 直接寫在 erb 中就稱為 inline
- partial
- 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)
- Inline 2ms, 平均 0.0002ms/次
- partial 80 sec,平均 8ms / 次
- 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>
使用時,在 render 後帶入一個 block,並在指定 slot 中放入一個 Home 的聯結:
<%= render ExampleComponent.new(greeting: 'hi') do |c| %>
<%= c.header do %>
<%= link_to "Home", root_path %>
<% end %>
<% end %>
結果就是 Home 的聯結會加入 header slot 的位置
<body>
<div>
<a href="/">Home</a>
<span>hi</span>
</div>
</body>
Validations
可以加入 ActiveModel::Validations 來強迫 Component 一定要有某些 Slot 或傳入變數的形式,避免錯誤。
或者是說可以拋出錯誤,幫助查錯。
Sidecar(實驗中)
Component 可以有自己 scoped 的 css, js ,這樣就功能就可能可以跟前端框架比一比了。因為實驗中暫先不測試
但概念是這樣:
順便查一下什麼叫Sidecar
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
可以利用產生出的路徑
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 配合會有奇效喔。)
Top comments (0)