把前端封装 (encapsulation) 是从 React.js 以来的趋势, Github 前年受 React 启發表了一个 view_component 的 gem,并用在 Github 裡。
Rails 的开發者应该本来就有把有複用的页面元素抽出来变成 partial 的习惯, 不过 view_component 想要解决 partail 常遇到的 3 种问题:
最近刚好研究了一下 view_component,分享心得。
在 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
# 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 来测速一下 我用的环境是
用 partial 档案 _hi.html.erb 跟上述的 ExampleComponent 来比较
# app/views/pages/_hi.html.erb
<span>hi!</span>
比较一下 3 种方式都给它印 10000 次 hi
<% 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)
这是最简单的测试, 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 的问题
如果有写 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>
可以加入 ActiveModel::Validations 来强迫 Component 一定要有某些 Slot 或传入变数的形式,避免错误。 或者是说可以抛出错误,帮助查错。
Component 可以有自己 scoped 的 css, js,这样就功能就可能可以跟前端框架比一比了。
前端自动测试常有个问题:测试都过,所有的行为都对了,但是真的打开浏览器时,显示错了。
目前最好的方式可能是靠人眼去看元件渲染出来的结果。
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 配合会有奇效喔。)