Image: https://unsplash.com/photos/oUTugmSkagk
原文: https://mini-geek.com/posts/77
我们这里说到的 slot 是 view 层面的,通常用于组件中。slot 的意思是插槽,同字面上的意思,它们是一些占位符,我们可以在调用到时才决定传递什么样的结构进去,这使得我们可以更加灵活的复用组件。
举个例子,我们的 web 应用中通常都会使用到 Modal(模态框)这个组件,通常 Modal 都是结构都是固定的。下面是 Bootstrap 中一个 Modal 使用的例子:
<div id="exampleModal" class="modal" tabindex="-1"> <!-- 1. id -->
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modal title</h5> <!-- 2. 标题 -->
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Modal body text goes here.</p> <!-- 3. 模态框主体 -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <!-- 4. 模态框的操作按钮 -->
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>
上面标出来的 4 个部分:id,标题,主体,操作按钮,通常是一个模态框会变动的地方(样式也可能会变,不过样式变化的问题比较容易解决,这里先忽略)。如果在每一个用到模态框的位置都写上这么一大串代码,显然是会影响代码的可读性和可维护性的。这个时候就可以用上我们之前说到的插槽的概念了。
接下来我们以 Modal 为例子,介绍一下如何使用 Rails 的 helper + partial + 一个简单的对象,来将这类代码封装成通用的组件。(ViewComponent 这个 Gem 也提供了 slot 的功能,这里我们介绍如何使用 Rails 原生的方式来实现)
首先我们要初始化一个项目:
$ rails new demo -c bootstrap
初始化完成之后,我们先生成一个控制器用于演示:
$ cd demo
$ rails generate controller home show
这里我们生成了一个只有 show action 的控制器 HomeController,并且生成了对应的页面:
新建 app/helpers/component_context.rb
内容如下:
class ComponentContext < BasicObject
attr_accessor :slots
def initialize(view_context)
@view_context = view_context
@slots = {}
end
private
def method_missing(name, *args, &block)
@slots[name] = @view_context.capture(&block)
end
end
ComponentContext 是实现 slot 功能的关键。这里简单介绍下 ComponentContext 的作用,这个类首先定义了一个 slots 的 accessor,这里是为了后面我们能保存以及获取到 slot 里边的内容。然后定义了一个 initialize 方法,接收一个 view_context 参数,这个 view_context 可以在 view 或 helper 中直接调用 self 获取到,默认是 ActionView::Base 的实例。接下来定义了 method_missing 方法,这个方法会在对象找不到方法时被调用。里边用到了 helper 里边的 capture 方法,这个方法可以捕捉 block 里边的内容并返回,这里我们用它接受 slot 的传值,这意味着调用 ComponentContext 中任意不存在的方法时,实际上都会往 @slots 里边存传入的 block 里边的内容,键就是调用时的方法名,同时也是相应 slot 的名字。使用 method_missing 是为了能让这个 ComponentContext 可以被所有的组件复用,它可以接受任意的 slot。同时,因为这个类只用于保存 slot 的值,所以使用 method_missing 的副作用也就没那么大了。
新建 app/helpers/components_helper.rb
,内容如下:
module ComponentsHelper
# Usage:
#
# <%= modal id: "modalExample", title: "A simple modal example" do |m| %>
# <% m.with_body do %>
# <span>Modal content</span>
# <% end %>
#
# <% m.with_footer do %>
# <button type="button" class="btn btn-primary">OK</button>
# <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
# <% end %>
# <% end %>
#
def modal(id:, title:)
yield component_context = ComponentContext.new(self)
render "components/modal", id:, title:, **component_context.slots
end
end
我们可以把需要的组件都放在这个 helper 里边,不过你也可以根据自己的需求决定 helper 方法的位置,毕竟 Rails 中的 helper 是没有命名空间这个概念的。
新建 app/views/components 文件夹
mkdir app/views/components
新建 app/views/components/_modal.html.erb
:
<%# locals: (id:, title:, with_body:, with_footer:) %>
<div id="<%= id %>" class="modal fade" tabindex="-1"> <!-- 1. id -->
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><%= title %></h5> <!-- 2. title -->
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<%= with_body %> <!-- 3. with_body -->
</div>
<div class="modal-footer">
<%= with_footer %> <!-- 4. with_footer -->
</div>
</div>
</div>
</div>
这个就是我们的组件模板了,id
和 title
是常规的变量,而 with_body
和 with_footer
就是我们的 slot 了,我们在给 slot 传值的时候也要使用相同的名字。
修改 app/views/home/show.html.erb
的内容为:
<div class="p-5">
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModal">
Launch demo modal
</button>
<%= modal id: "exampleModal", title: "Example modal" do |m| %>
<% m.with_body do %> <!-- 这里我们给 with_body 这个 slot 传值 -->
<span>Modal content</span>
<% end %>
<% m.with_footer do %> <!-- 这里我们给 with_footer 这个 slot 传值 -->
<button type="button" class="btn btn-primary">OK</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<% end %>
<% end %>
</div>
启动服务:
$ bin/dev
打开浏览器,访问 http://localhost:3000/home/show ,点击按钮,可以看到模态框按照我们预想中的呈现了
在实现 slot 这个功能的过程中,我们使用了 Rails 里边的 helper 和 partial,并且定义了一个名为 ComponentContext 的类来作为 helper 和 partial 中间的桥梁,最终实现了我们期望的效果