You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rails 7浏览器后退后JavaScript事件监听器失效如何解决

Rails 7 选择类组件浏览器后退后交互失效修复方案

我在开发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,会导致搜索匹配逻辑永远无法生效。

修复步骤

  1. 重写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>`;
      }
    }
    
  2. 兜底配置(可选,针对极端缓存场景)
    如果修改后仍存在偶发失效,在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 19:54:16