发布于 2026-01-06 0 阅读
0

使用 Rails 6 和 Hotwire 实现即时搜索

使用 Rails 6 和 Hotwire 实现即时搜索

去年我写了一篇关于使用 Rails 和 StimulusJS 构建即时搜索表单的文章。此后,Hotwire的另一半——Turbo也发布了。Turbo 为更简洁、更清晰地实现即时搜索表单打开了大门。

今天,我们将再次探讨如何构建一个边输入边搜索的界面,该界面允许我们查询数据库中的匹配项,并(几乎)立即用这些匹配项更新用户界面。

完成后,我们的代码看起来将与您习惯编写的用于构建 Rails 应用程序的代码几乎完全相同,我们唯一需要的 JavaScript 代码就是在用户输入时触发表单提交。

最终产品的工作原理如下:

一段屏幕录像,显示用户输入关键词搜索达拉斯小牛队篮球运动员列表。随着用户输入,球员列表自动更新。

本文假设您已熟悉 Ruby on Rails。您无需了解 Hotwire(Turbo 或 Stimulus)即可阅读本文。本指南最能帮助那些刚接触 Hotwire 和 Turbo,并希望了解如何使用 Hotwire 工具集构建常见用户体验示例的用户。

您可以在Github上找到本指南的完整代码

我们开始吧。

应用程序安装

首先,我们需要一个安装了Turbo和Stimulus插件并准备就绪的应用程序。如果您已经设置好了应用程序,可以直接使用。

要逐行跟随操作,请先在终端运行以下命令。您的机器需要配置好标准的 Rails 开发环境,安装 Ruby、Rails 和 Yarn。

作为参考,我编写本指南时使用的是 Rails 6.1 和 turbo-rails 7.0.0-rc.1。

rails new hotwire-search -T && cd hotwire-search
bundle add hotwire-rails
rails hotwire:install
rails g scaffold Player name:string
rails db:migrate
rails s
Enter fullscreen mode Exit fullscreen mode

运行上述命令后,我们应该会得到一个新的 rails 应用程序,访问http://localhost:3000/players应该会打开一个页面,其中列出了数据库中的玩家,并允许您添加和删除玩家。

请从用户界面添加几个新玩家,或者打开 Rails 控制台添加一些玩家。给他们取不同的名字,因为在这个项目中我们会按名字筛选玩家列表。

添加搜索表单和控制器操作

设置完成后,用你喜欢的编辑器打开代码。

首先,我们将更新玩家索引视图,添加一个用于筛选玩家列表的表单。更新玩家索引视图以添加表单:

<p id="notice"><%= notice %></p>

<h1>Players</h1>
<%= form_with(url: search_players_path) do |form| %>
  <%= form.label :query, "Search by name:" %>
  <%= form.text_field :query, data: { action: "input->form-submission#search" } %>
<% end %>
<table>
  <thead>
    <tr>
      <th>Name</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @players.each do |player| %>
      <tr>
        <td><%= player.name %></td>
        <td><%= link_to 'Show', player %></td>
        <td><%= link_to 'Edit', edit_player_path(player) %></td>
        <td><%= link_to 'Destroy', player, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New Player', new_player_path %>
Enter fullscreen mode Exit fullscreen mode

由于我们添加了一个form_with需要指定路由名称的组件search_players_path,我们将更新路由文件以添加该路径:

Rails.application.routes.draw do
  resources :players do
    collection do
      post 'search'
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

需要注意的是,由于我们将使用 Turbo Stream 进行响应,因此我们的表单需要发送 POST 请求,而不是 GET 请求。虽然有一些方法可以绕过这个限制,但我们不会使用这些变通方法,而是直接使用 POST 请求来发送搜索表单。

您可以在Github上阅读更多关于此行为的原因以及可能的解决方法

虽然我们可以添加一个专门的搜索控制器,但为了简单起见,这里我们只需将路由添加到现有的路由中即可PlayersController。路径添加完毕后,接下来让我们更新路由PlayersController以添加搜索方法:

def search
  if params[:query].present?
    @players = Player.where("name LIKE ?", "%#{params[:query]}%")
  else
    @players = Player.all
  end

  render turbo_stream: turbo_stream.replace(
    'players',
    partial: 'list',
    locals: {
      players: @players
    }
  )
end
Enter fullscreen mode Exit fullscreen mode

我们的数据库查询语句虽然称不上优雅,但方法的关键在于render调用本身。

对 players/search 的请求不会返回标准的 HTML 响应,而是返回 turbo_stream 响应,该响应会定位页面上 id 为 的元素players,并将该元素的内容替换为listpartial。

如果响应使用的是 turbo_stream 而不是 HTML,则发送到浏览器的有效负载将类似于这样:

<turbo-stream action="replace" target="players">
  <template>
    <!-- Content from the rendered partial -->
  </template>
</turbo-stream>
Enter fullscreen mode Exit fullscreen mode

请注意,这仍然是 HTML,只是被包裹在特殊<turbo-stream>元素中,并带有标头(text/vnd.turbo-stream.html; charset=utf-8),该标头表明响应是 turbo 流。

当 Turbo 看到带有 turbo-stream 标头和 turbo-stream 包装的 HTML 片段的响应时,它会从中读取actiontarget新添加的并使用该标头和新添加的 ,仅更新 DOM 的相关部分。targets<turbo-stream>

添加流目标

现在我们对Turbo的底层工作原理有了一些了解,但这些在我们的应用程序中还无法实现。我们目前使用尚未创建的局部视图来响应表单提交,而且由于没有提交按钮,表单也无法提交。

让我们回到代码中,创建列表局部视图。在终端中:

touch app/views/players/_list.html.erb
Enter fullscreen mode Exit fullscreen mode

然后将该部分内容填入:

<tbody id="players">
  <% players.each do |player| %>
    <tr>
      <td><%= player.name %></td>
      <td><%= link_to 'Show', player %></td>
      <td><%= link_to 'Edit', edit_player_path(player) %></td>
      <td><%= link_to 'Destroy', player, method: :delete, data: { confirm: 'Are you sure?' } %></td>
    </tr>
  <% end %>
</tbody>
Enter fullscreen mode Exit fullscreen mode

请注意元素id上的属性<tbody>。此 id 必须与控制器中传递的目标 ID 匹配turbo_stream.replace,否则搜索将不会发生任何操作。此局部视图的其余部分只是 Rails 脚手架生成的样板代码。

最后,更新玩家索引视图,使其使用我们刚刚创建的列表局部视图:

<!-- Snip -->
<table>
  <thead>
    <!-- Snip -->
  </thead>

  <%= render "list", players: @players %>
</table>
<!-- Snip -->
Enter fullscreen mode Exit fullscreen mode

提交搜索表单

列表片段准备就绪后,最后一步是让用户输入时自动提交搜索表单。为此,我们将创建一个简单的 Stimulus 控制器并将其附加到搜索表单。首先,在您的终端中:

touch app/javascript/controllers/form_submission_controller.js
Enter fullscreen mode Exit fullscreen mode

并将以下内容填入该控制器:

import { Controller } from "stimulus"
import Rails from "@rails/ujs";

export default class extends Controller {
  static targets = [ "form" ]

  search() {
    clearTimeout(this.timeout)
    this.timeout = setTimeout(() => {
      Rails.fire(this.formTarget, 'submit')
    }, 200)
  }
}
Enter fullscreen mode Exit fullscreen mode

这段search代码会等待用户停止输入,然后提交表单。为了触发表单提交,我们借鉴了Better Stimulus中概述的一种技术。

添加 Stimulus 控制器并创建列表局部视图后,最后一步是通过更新玩家索引视图中的搜索表单,将 Stimulus 控制器添加到我们的表单中,如下所示:

<%= form_with(url: search_players_path, data: { controller: 'form-submission', form_submission_target: "form" }) do |form| %>
  <%= form.label :query, "Search by name:" %>
  <%= form.text_field :query, data: { action: "input->form-submission#search" } %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

现在我们进入了常规的 Stimulus 框架。我们向元素添加数据属性<form>以连接控制器,并设置必要的表单目标,然后使用 data-action 属性更新文本字段,该属性会在输入时触发控制器search中的函数form-submission

最后一步完成后,在浏览器中打开/players,开始输入,你应该会看到玩家列表自动更新,如下所示:

一段屏幕录像,显示用户输入关键词搜索达拉斯小牛队篮球运动员列表。随着用户输入,球员列表自动更新。

总结

这个小例子应该能让你体会到使用 Ruby on Rails 和 Hotwire 构建现代、高度响应式应用程序是多么简单,它将 SPA 的感觉与 Rails 世界(通常)喜爱的开发者体验相结合。

要在现实世界中应用这项技术,我们需要考虑以下问题:

  • 加载状态:在表单提交开始时替换玩家<tbody>可能是一个不错的起点。
  • 空状态:当列表部分players为空时,它可以渲染不同的内容。
  • 更简洁、更高效的数据库查询:千万不要把查询语句直接放在控制器里!对于生产环境,可以考虑使用pg_search之类的工具。

虽然这个示例尚未达到生产就绪状态,但我希望它能为你开启基于 Hotwire 的 Rails 应用之旅提供一个良好的起点。下次构建新项目时,不妨考虑一下 Rails + Hotwire 是否足以满足用户期望的体验,同时又能保证团队的满意度和生产力。

一如既往,感谢阅读!

文章来源:https://dev.to/davidcolbyatx/instant-search-with-rails-6-and-hotwire-1anm