Rails 7浏览器后退后JavaScript事件监听器失效如何解决
我在开发Rails 7应用时,尝试使用tom-select或slim-select库构建带标签功能的搜索栏,切换两个第三方库都会复现相同问题,因此判断问题根源出在Rails侧配置上。
相关实现代码
app/views/cities/index.html.erb
<%= form_for :city, url: cities_path, method: 'GET' do |f| %> <div class="mt-4 border bg-light px-4 py-3 rounded-3"> <%= f.select :search_city, [], {}, placeholder: 'Type to search', data: { controller: 'ts--search', ts__search_url_value: autocomplete_cities_path } %> <%= f.submit 'Search', class: 'btn mx-auto' %> </div> <% end %>
app/javascript/controllers/ts/search_controller.js
import { Controller } from "@hotwired/stimulus"; import { get } from "@rails/request.js"; import TomSelect from "tom-select"; export default class extends Controller { static values = { url: String }; connect() { var config = { plugins: ["input_autogrow", "remove_button", "no_active_items"], render: { option: this.render_option, item: this.render_option, }, valueField: "value", loadThrottle: 400, load: (q, callback) => this.search(q, callback), closeAfterSelect: true, persist: false, create: false, delimiter: ", ", maxItems: 10, }; new TomSelect(this.element, config); } async search(q, callback) { const response = await get(this.urlValue, { query: { query: q }, responseKind: "json", }); if (response.ok) { callback(await response.json); } else { console.log("Error in search_ctrl: "); callback(); } } render_option(data, escape) { return `<div>${escape(data.text)}</div>`; } }
app/controllers/cities_controller.rb
class CitiesController < ApplicationController def index end def autocomplete list = City.order(:name) .where("name ilike :q", q: "%#{params[:q]}%") render json: list.map { |u| { text: u.name, value: u.id, sub: u.state } } end end
复现步骤
- 打开城市索引页面,点击搜索栏
- 搜索下拉框正常弹出,可输入内容、选择建议项,选中后建议项会展示在搜索栏中,点击项旁的
x按钮可将其从搜索栏移除 - 添加任意数量(1-N个)的搜索标签
- 点击"Search"按钮,跳转至搜索结果页
- 点击浏览器的后退按钮(手机端可通过滑动手势返回上一页)
预期行为
返回后搜索栏状态与发起搜索前完全一致:点击x按钮可移除对应标签,点击搜索栏可正常输入查询词、添加更多搜索标签。
实际行为
页面可正常展示已选标签,但除Search按钮外,点击其他区域均无任何响应,多个同类公开实现demo也存在相同问题,需要找到正确配置方式让浏览器返回后页面的JavaScript交互功能正常生效。
问题原因
问题和tom-select、slim-select本身无关,核心是Rails 7默认启用的Turbo Drive机制:浏览器后退时Turbo会从往返缓存(bfcache)直接恢复页面DOM,此时之前初始化的选择组件实例没有被正确销毁,和恢复后的DOM节点产生绑定冲突,导致所有交互事件失效。
另外原代码存在一个隐性bug:后端autocomplete接口用params[:q]接收搜索关键词,但前端请求传参的键名是query,会导致搜索匹配逻辑永远无法生效。
修复步骤
重写Stimulus控制器,增加实例缓存、销毁逻辑,适配Turbo生命周期
替换app/javascript/controllers/ts/search_controller.js内容为以下代码:import { Controller } from "@hotwired/stimulus"; import { get } from "@rails/request.js"; import TomSelect from "tom-select"; export default class extends Controller { static values = { url: String }; connect() { // 初始化前先销毁已存在的实例,避免重复绑定 if (this.tomSelectInstance) { this.tomSelectInstance.destroy(); } const config = { plugins: ["input_autogrow", "remove_button", "no_active_items"], render: { option: this.renderOption, item: this.renderOption, }, valueField: "value", loadThrottle: 400, load: (q, callback) => this.fetchSearchResults(q, callback), closeAfterSelect: true, persist: false, create: false, delimiter: ", ", maxItems: 10, }; this.tomSelectInstance = new TomSelect(this.element, config); } // 页面被Turbo缓存、控制器断开连接时主动销毁实例 disconnect() { if (this.tomSelectInstance) { this.tomSelectInstance.destroy(); this.tomSelectInstance = null; } } async fetchSearchResults(q, callback) { const response = await get(this.urlValue, { // 修正参数键名,和后端接收逻辑对齐 query: { q: q }, responseKind: "json", }); if (response.ok) { callback(await response.json); } else { callback(); } } renderOption(data, escape) { return `<div>${escape(data.text)}</div>`; } }兜底配置(可选,针对极端缓存场景)
如果修改后仍存在偶发失效,在app/views/cities/index.html.erb顶部加入以下代码,关闭该页面的Turbo预渲染缓存:<% content_for :head do %> <meta name="turbo-cache-control" content="no-preview"> <% end %>
修改完成后重启Rails服务,硬刷新页面验证即可:提交搜索后通过浏览器后退返回,选择框的标签删除、输入唤起下拉、选择选项等所有交互都会正常工作。如果使用slim-select,逻辑完全一致,只需要把TomSelect的实例化、销毁方法替换成slim-select对应API即可。
内容的提问来源于stack exchange,提问作者zdebyman

