PAYATHON 2026

TurboFrame 与 Stimulus 搜索表单无法执行搜索

排障赵师傅

结论

这段代码需要检查三个环节:

  1. Stimulus 控制器是否已被浏览器加载并连接。如果 TypeScript 没有编译、构建进程未运行,或 form-request-submit-polyfill 无法解析,submit() 就不会执行。
  2. Turbo Frame 响应中是否包含 id="search-results"。即使请求成功,响应中没有同名 frame,Turbo 也无法更新页面。
  3. Controller 是否执行了搜索。当前的 Concert.all 会返回全部记录,只设置 @query 不会筛选结果。

input->search#submit 配合 requestSubmit() 的写法本身没有明显问题。可以先确认 Stimulus 是否连接,再看请求有没有发出,然后检查 Turbo 响应,暂时不用先查数据库查询。

确认 Stimulus 控制器是否连接

先在控制器中临时加入 connect() 和事件日志:

import { Controller } from "@hotwired/stimulus"
import "form-request-submit-polyfill"

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

  declare readonly formTarget: HTMLFormElement
  declare readonly inputTarget: HTMLInputElement

  connect(): void {
    console.log("search controller connected")
  }

  submit(event: Event): void {
    console.log("search submit", event)
    this.formTarget.requestSubmit()
  }
}

刷新页面后,Console 中应立即出现:

search controller connected

输入文字时还应出现:

search submit

如果第一条日志都没有出现,问题就在 JavaScript 加载阶段,与 Rails controller 或 Turbo Frame 无关。

此时检查以下内容:

  • 最终生成的 HTML 中有没有 data-controller="search"。
  • 入口文件是否导入了 app/javascript/controllers/index.js。
  • JavaScript 构建进程是否正在运行。
  • 构建终端中有没有 TypeScript 或模块解析错误。
  • 是否已经安装 form-request-submit-polyfill。
  • 浏览器加载的是不是最新的构建产物。

如果项目使用 jsbundling-rails,开发时通常需要启动项目配置的构建监听进程,例如:

bin/dev

具体命令取决于项目使用 esbuild、Webpack、Rollup 还是其他构建方案。如果只运行 bin/rails server,.ts 文件不一定会持续编译。

导入控制器时,可以让构建工具自行解析扩展名:

import { application } from "./application"
import SearchController from "./search_controller"

application.register("search", SearchController)

如果项目没有配置 TypeScript 编译,可以把文件改为 search_controller.js,同时去掉类型声明。浏览器无法直接执行下面这种 TypeScript 语法:

formTarget: HTMLFormElement

如果目标浏览器较新,也可以先移除 polyfill,看看控制器能否正常连接:

import { Controller } from "@hotwired/stimulus"

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

  declare readonly formTarget: HTMLFormElement

  submit(): void {
    this.formTarget.requestSubmit()
  }
}

如果移除 import 后控制器开始连接,问题出在 form-request-submit-polyfill 的安装或模块解析上。

检查最终生成的 HTML

除了 ERB 源码,还要在浏览器开发者工具的 Elements 面板中查看实际生成的 HTML。结构应接近下面这样:

<div data-controller="search">
  <form
    data-search-target="form"
    data-action="input->search#submit"
    data-turbo-frame="search-results"
  >
    <input
      name="query"
      type="search"
      data-search-target="input"
    >
  </form>

  <turbo-frame id="search-results"></turbo-frame>
</div>

ERB 中的 data 属性也可以改成较常见的 Rails 写法,减少属性名拼写错误:

<%= turbo_frame_tag "search-form" do %>
  <div data-controller="search">
    <%= form_with(
      url: concerts_path,
      method: :get,
      data: {
        turbo_frame: "search-results",
        search_target: "form",
        action: "input->search#submit"
      }
    ) do %>
      <div class="flex justify-center">
        <div class="w-4/5">
          <%= text_field_tag(
            :query,
            params[:query],
            type: "search",
            placeholder: "Search concerts",
            id: "search_query",
            data: { search_target: "input" },
            class: "w-full px-3 py-2 border border-gray-400 rounded-lg"
          ) %>
        </div>
      </div>
    <% end %>

    <%= turbo_frame_tag "search-results" %>
  </div>
<% end %>

input 事件会冒泡,因此可以把 data-action 放在 <form> 上。排查时也可以暂时把它放到输入框上,以便直接确认事件来源:

data: {
  search_target: "input",
  action: "input->search#submit"
}

用 Network 面板定位问题

打开浏览器开发者工具的 Network 面板,然后输入搜索词。正常情况下会出现类似请求:

GET /concerts?query=rock

可以根据请求情况判断问题所在:

  • 没有请求:Stimulus 没有连接、事件没有触发,或 JavaScript 没有编译加载。
  • 请求已经发出,但返回 4xx/5xx:检查 Rails 日志和响应正文。
  • 请求返回 200,但页面没有更新:检查响应中有没有 <turbo-frame id="search-results">。
  • 页面更新了,但记录没有筛选:为 controller 补上搜索条件。
  • 结果能够显示,但高亮时报错:检查查询词是否被直接当作正则表达式使用。

也可以在 Console 中直接测试表单提交:

document.querySelector('[data-search-target="form"]').requestSubmit()

如果这条命令能够发出请求,说明表单和 Turbo 基本正常,问题多半在 Stimulus 的加载或事件绑定上。

Turbo 响应必须包含同名 Frame

表单指定了:

data-turbo-frame="search-results"

所以 /concerts 返回的 HTML 中必须包含:

<turbo-frame id="search-results">
  ...
</turbo-frame>

例如,可以在 app/views/concerts/index.html.erb 中这样写:

<%= turbo_frame_tag "search-results" do %>
  <%= render(
    partial: "search_result",
    collection: @concerts,
    as: :concert,
    locals: { query: @query }
  ) %>
<% end %>

如果只返回 _search_result.html.erb 中的 <article>,Turbo 无法在响应中找到目标 frame。

最终页面中也不能同时出现两个 id="search-results"。初始页面可以放一个空 frame,搜索响应再返回一个同名 frame,Turbo 会用响应中的内容替换当前 frame。

Controller 要执行实际筛选

当前代码是:

def index
  @query = params[:query]
  @concerts = Concert.all
end

这段代码读取了参数,却没有用它筛选记录。简单的名称搜索可以写成:

def index
  @query = params[:query].to_s.strip
  @concerts = Concert.all

  if @query.present?
    pattern = "%#{ActiveRecord::Base.sanitize_sql_like(@query)}%"
    @concerts = @concerts.where("name LIKE ?", pattern)
  end
end

LIKE 是否区分大小写取决于数据库及其配置。PostgreSQL 项目如果需要忽略大小写,可以根据项目情况使用 ILIKE:

@concerts = @concerts.where("name ILIKE ?", pattern)

如果还要搜索乐队、场馆或标签,需要根据实际关联和表结构添加 joins、查询条件及必要的 distinct。仅凭现有代码无法确定完整 SQL。

高亮前转义搜索词

下面的写法会把用户输入直接插入正则表达式:

<%= highlight(concert.name, /#{query}/i) %>

用户输入 [、(、* 等字符时,生成的正则表达式可能无效,也可能改变原本的匹配含义。应先对搜索词进行转义:

<% if query.present? %>
  <% pattern = Regexp.new(Regexp.escape(query), Regexp::IGNORECASE) %>
  <%= highlight(concert.name, pattern) %>
<% else %>
  <%= concert.name %>
<% end %>

乐队名称也要使用同一个经过转义的 pattern:

<% band_names = concert.bands.map(&:name).join(", ") %>

<% if query.present? %>
  <%= highlight(band_names, pattern) %>
<% else %>
  <%= band_names %>
<% end %>

排查顺序

可以按以下顺序逐项检查,每一步只验证一个环节:

  1. 在 connect() 中输出日志,确认 Stimulus 控制器已经连接。
  2. 在 submit() 中输出日志,确认 input 事件已经触发。
  3. 在 Network 面板中确认 /concerts?query=... 请求已经发出。
  4. 查看请求参数中是否包含 query。
  5. 检查响应状态,并确认 Rails controller 是否执行。
  6. 查看响应 HTML 中是否有 <turbo-frame id="search-results">。
  7. 确认 controller 使用 @query 筛选了数据。
  8. 最后再处理高亮、关联查询和输入防抖等细节。

如果页面完全没有反应,Rails 也没有收到请求,应先检查 JavaScript 构建进程和 Stimulus 控制器的连接状态。Rails 没有日志,通常是因为请求根本没有离开浏览器,但后端搜索代码本身也仍需按前面的方式检查。

备注:内容仅供参考。