partial 的救贖？view_component 介紹
Tags: rails, viewcomponent
Published: 2021-03-15 14:15:51

把前端封裝(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
* [view_component 官網](https://viewcomponent.org/)
* [RailsConf 2019 - Rethinking the View Layer with Components by Joel Hawksley](https://www.youtube.com/watch?v=y5Z5a6QdA-M)
* [Encapsulating Ruby on Rails views - The GitHub Blog](https://github.blog/2020-12-15-encapsulating-ruby-on-rails-views/)

# 使用方式
在 Gemfile 中加入 view_component
```ruby
# 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 檔案，把另兩個檔案內容改成以下內容：
```ruby
# 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 即可：
```erb
<%= render ExampleComponent.new(greeting: 'hi') %>
```

結果是會將 erb 的渲染至網頁中：
```html
  <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 檔案：

```ruby
# 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
![alt text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7pok6dozfa068nvr8wtc.png)
 

# 關於速度
可以直接用 benchmark 來測速一下
我用的環境是
* ruby 2.7.2p137
* Rails 6.1.3

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

```erb
# app/views/pages/_hi.html.erb
<span>hi!</span>
```

以下程式碼用 3 種方式印 10000 次 `hi` ，並用 benchmark 紀錄：
1. 直接寫在 erb 中就稱為 inline
2. partial
3. component 

```erb
<% 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)
```
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](https://github.com/github/view_component/blob/53992be98461a7d405ceb1d2fa5db07b28802d42/lib/view_component/base.rb#L76))。

# 其它用法
## Slot
如果有寫 Vue.js ，對 Slot 的概念應該是不陌生，就是可從 template 外部塞入 html。
Slot 設定方法很多種，下面用 Named slot 方式加入一個 header slot
```ruby
# 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 的聯結：
```erb
<%= render ExampleComponent.new(greeting: 'hi') do |c| %>
  <%= c.header do %>
    <%= link_to "Home", root_path %>
  <% end %>
<% end %>
```

結果就是 Home 的聯結會加入 header slot 的位置
```html
<body>
  <div>
    <a href="/">Home</a>
    <span>hi</span>
  </div>
</body>
```

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

## Sidecar(實驗中)
Component 可以有自己 scoped 的 css, js ，這樣就功能就可能可以跟前端框架比一比了。因為實驗中暫先不測試
但概念是這樣：
![alt text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/36z57hxagxws78bxfo25.png)
順便查一下什麼叫Sidecar
![alt text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/at1x5l3d7b6eymbwfu3g.png)
  
## preview
前端自動測試常有個問題：測試都過，所有的行為都對了，但是真的打開瀏覽器時，顯示破版十分尷尬。
目前最好的方式還是靠人眼去看元件渲染出來的結果，
view_compoent 提供了預覽的輔助工具。
`test/components/previews/example_component_preview.rb`
```ruby
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](https://github.com/jonspalmer/view_component_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](https://primer.style/view-components/)
還在 Beta 版本，但看來很完整，應該不久後就正式推出了(？)

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

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

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