Ruby 在 Ruby On Rails 中使用 helper 实现 slot 功能

Ian · 2024年03月06日 · 最后由 kikyous-github 回复于 2024年03月06日 · 585 次阅读

Image: https://unsplash.com/photos/oUTugmSkagk

原文: https://mini-geek.com/posts/77

什么是 Slot?

我们这里说到的 slot 是 view 层面的,通常用于组件中。slot 的意思是插槽,同字面上的意思,它们是一些占位符,我们可以在调用到时才决定传递什么样的结构进去,这使得我们可以更加灵活的复用组件。

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>

这个就是我们的组件模板了,idtitle 是常规的变量,而 with_bodywith_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 中间的桥梁,最终实现了我们期望的效果

需要 登录 后方可回复, 如果你还没有账号请 注册新账号