Rails partial 的救赎?介绍 view_component

kevinluo201 · March 14, 2021 · Last by kxu1988 replied at March 15, 2021 · 968 hits

把前端封装 (encapsulation) 是从 React.js 以来的趋势, Github 前年受 React 启發表了一个 view_component 的 gem,并用在 Github 裡。

Rails 的开發者应该本来就有把有複用的页面元素抽出来变成 partial 的习惯, 不过 view_component 想要解决 partail 常遇到的 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"

安装完后,新增一个 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 把内容先改成这样
# app/components/example_component.rb
# frozen_string_literal: true
class ExampleComponent < ViewComponent::Base
  def initialize(greeting:)
    @greeting = greeting
  end
end


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


# 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

之后跟的用法跟 partial 很像,将 instance 丢入 render 即可

<%= render ExampleComponent.new(greeting: 'hi') %>

即会将 erb 的渲染至网页中

<div>
  <span>hi</span>
</div>

可利用bundle 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

  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)
  1. Inline 是最快 2ms, 平均 0.0002ms/次
  2. partial 花了 80 秒,平均 8ms / 次
  3. Component 62ms, 平均 0.0062ms / 次 Partial 虽然最慢,但其实 8ms 也是满快的 不过 view_component 好像快太多了...快 1000 倍 xD

这是最简单的测试, view_component 的官网上是说比 partial 快 10 倍以上

祕密应该就是在 view_component 改写了 render 的方法, 略过一堆找 template 的过程,而且有加 cache, 所以上面的实验才会快那麽多

资料流

Component 很明确限制只能使用在 Component Class 内定义的变数跟方法。 instance_variable 跟 helper 的方法在 erb 中可以当成是全域的。 用 partial 常投机狂用 instance variable 而不是 locals,造成维护困难 大大减少非预期的內容,比较好追 code

测试

Rails 的 MVC 架构的 model 跟 controller 都很容易来写测试, 唯独 view 的不是很好写 test,但偏偏 view 又是佔大量程式码的部分, 造成 test coverage 普遍不高。 partial 更是不能单独测试,一定是配合 controller 真的画出整个网页的 html 或 end2end 的 system test。 ViewComponent 可以在测试 中渲染单一 component,即解决 partial 无法 unit test 的问题

其它用法

Slot

如果有写 Vue.js,对 Slot 的概念应该是不陌生,就是可从 template 外部塞入 html。 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.rb
<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,这样就功能就可能可以跟前端框架比一比了。

preview

前端自动测试常有个问题:测试都过,所有的行为都对了,但是真的打开浏览器时,显示错了。 目前最好的方式可能是靠人眼去看元件渲染出来的结果。 test/components/previews/example_component_preview.rb

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

  def with_long_text
    render(ExampleComponent.new(greeting: "This is a really long title to see how the component renders this"))
  end
end

可以利用产生出的路径 http://localhost:3000/rails/view_components/example_component/with_default_title 就可以直接点进去看渲染出的元件了

对前端熟的人可能会知道 Storybook.js 这个框架, 一个 ViewComponent 用的 storybook 的 gem 已经有人开發了 GitHub - jonspalmer/view_component_storybook: ViewComponent previews and testing in Storybook

小结

虽然较进阶的用法还在实验阶段, 但已可看的出 Github 对他们这个设计非常满意 😂 Github 的人为了 view_component, 已在在 Rails 6.1 加入允许物件实作自己的 render 的方式: 只要该物件有render_in 的方法,render 时就会改去呼叫物件的 render_in。 完全为 view_component 量身打造,不用再去 monkey patch ActionView 的 render。

而且已经把非常多元件做成 ViewComponent 的形式,
打算做成一个元件库 Primer ViewComponents
还在 Beta,但看来很完整,应该不久后就正式推出了 (?)

我个人是觉得很值得尝试用来取代 partial, 但因为跟前端框架 React, Vue 来比较时,view_component 没有天生支援 track 状态变化, 可能等 Sidecar 的功能正式推出后,会有更好的做法。

(不过也许跟 DHH 主推的 stimulus.js 或 Hotwire 配合会有奇效喔。)

You need to Sign in before reply, if you don't have an account, please Sign up first.