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

Turbo Frame与Turbo Stream在后台任务更新记录时的异常问题

Turbo Stream更新消息未同步问题排查与解决方案

环境配置

  • Ruby 3.2.0
  • Rails 8.0.1

代码实现

路由配置(config/routes.rb)

Rails.application.routes.draw do
  mount MissionControl::Jobs::Engine, at: "/jobs"
  get "up" => "rails/health#show", as: :rails_health_check
  resources :messages
  root "messages#index"
end

消息控制器(app/controllers/messages_controller.rb)

class MessagesController < ApplicationController
  def index
    @messages = Message.all.order(created_at: :asc)
  end

  def create
    @user_message = Message.create(
      content: params[:content],
      role: "user"
    )

    @assistant_message = Message.create(
      content: "Searching the answer for you...",
      role: "assistant"
    )

    ProcessChatMessageJob.perform_later(@user_message, @assistant_message)

    respond_to(&:turbo_stream)
  end
end

消息模型(app/models/message.rb)

class Message < ApplicationRecord
  validates :content, presence: true, length: { minimum: 3, maximum: 1000 }

  after_create_commit do
    broadcast_append_to "messages", target: "messages", partial: "messages/message", locals: { message: self }
  end

  after_update_commit do
    broadcast_update_to "messages", target: "message_#{id}", partial: "messages/message", locals: { message: self }
  end
end

消息列表视图(app/views/messages/index.html.erb)

<div class="max-w-2xl mx-auto p-4 space-y-6">
  <div id="messages" class="border p-4 rounded shadow">
    <%= turbo_stream_from "messages" %>
    <% @messages.each do |message| %>
      <%= render partial: "messages/message", locals: { message: message } %>
    <% end %>
  </div>

  <div id="message_form">
    <%= render "messages/form" %>
  </div>
</div>

创建消息Turbo Stream视图(app/views/messages/create.turbo_stream.erb)

<%= turbo_stream.replace "message_form" do %>
  <%= render "messages/form" %>
<% end %>

消息表单视图(app/views/messages/_form.html.erb)

<%= form_with url: messages_path, method: :post, data: { turbo_frame: "messages" }, class: "flex flex-col gap-2" do |f| %>
  <%= f.text_area :content, placeholder: "Enter a new message...", class: "p-2 border rounded", rows: 3 %>
  <%= f.submit "Send", class: "px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" %>
<% end %>

消息部分视图(app/views/messages/_message.html.erb)

<%= turbo_frame_tag dom_id(message) do %>
  <div class="p-4">
    <div class="flex my-1 <%= message.role == 'user' ? 'justify-end' : 'justify-start' %>">
      <div class="<%= message.role == 'user' ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-800' %> max-w-xs md:max-w-md p-4 rounded-lg shadow-lg">
        <%= markdown(message.content) %>
      </div>
    </div>
  </div>
<% end %>

处理消息任务(app/jobs/process_chat_message_job.rb)

class ProcessChatMessageJob < ApplicationJob
  queue_as :default

  def perform(user_message, assistant_message)
    user_message_content = user_message.content

    sql_query = Ai::SqlQueryGenerator.new(user_message_content).call

    if sql_query.include?("Sorry")
      assistant_message.update!(content: sql_query)
      return
    end

    puts "Executing SQL query: #{sql_query}"

    result = execute_sql_query(sql_query)
    puts "Result of sql query: #{result}"

    formatted_answer = Ai::ResultFormatter.new(result).call
    puts "Formatted answer: #{formatted_answer}"

    assistant_message.update!(content: formatted_answer)
  end

  private

  def execute_sql_query(sql_query)
    ActiveRecord::Base.connection.execute(sql_query)
  rescue => e
    puts "Error executing SQL query: #{e.message}"
    nil
  end
end

日志摘要

# Job Enqueue & Execution
[ActiveJob] Enqueued ProcessChatMessageJob (...) with arguments: Message/406, Message/407
...
[ActiveJob] Performing ProcessChatMessageJob (...)
Executing SQL query: SELECT ... FROM tournament_years ty ... LIMIT 20;
...
Message Update (...) UPDATE "messages" SET "content" = '# Grand Slam Wins by Legendary Tennis Players ...' WHERE "messages"."id" = 407
...

# Broadcasting Updated Message
[ActiveJob] Enqueued Turbo::Streams::ActionBroadcastJob ... with arguments: "messages", { action: :replace, target: "message_407", partial: "messages/message", locals: { message: Message/407 } }
...
[ActiveJob] Performed Turbo::Streams::ActionBroadcastJob (Job ID: ...) from SolidQueue(default) in ...ms

问题详情

当前行为

  • 表单提交时:MessagesController#create创建用户消息与带"Searching the answer for you..."占位文本的助手消息,两者通过after_create_commit广播追加到"messages"流;表单容器会被Turbo Stream替换。
  • 后台任务处理后:ProcessChatMessageJob更新助手消息内容,触发after_update_commit广播更新对应Turbo Frame,日志显示广播已执行。

问题

后台任务完成后,数据库中助手消息已更新,但页面上的消息未同步刷新。

排查思路与解决方案

1. 修正表单的Turbo Frame配置错误

表单中data: { turbo_frame: "messages" }指定的messages是普通div元素,并非Turbo Frame标签,这会导致Turbo处理表单提交时出现异常,干扰后续WebSocket广播的接收。

解决方案:移除表单的turbo_frame数据属性:

<%= form_with url: messages_path, method: :post, class: "flex flex-col gap-2" do |f| %>
  <%= f.text_area :content, placeholder: "Enter a new message...", class: "p-2 border rounded", rows: 3 %>
  <%= f.submit "Send", class: "px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" %>
<% end %>

2. 验证WebSocket连接状态

  • 打开浏览器开发者工具,切换到Network标签页,检查是否存在cable开头的WebSocket连接,确认连接状态为Open。
  • 若连接失败,检查config/cable.yml配置,确保开发环境的adapter为async或正确的Redis配置,同时确认config/environments/development.rb中启用了Action Cable:
    config.action_cable.mount_path = '/cable'
    config.action_cable.url = 'ws://localhost:3000/cable'
    config.action_cable.allowed_request_origins = [/http:\/\/*/, /https:\/\/*/]
    

3. 确认广播目标ID匹配

  • 查看页面渲染后的HTML,确认助手消息的Turbo Frame ID为message_xxx(如message_407),与模型中broadcast_update_to指定的target: "message_#{id}"完全一致。
  • 可在模型的after_update_commit中添加日志,确认广播的目标ID和内容:
    after_update_commit do
      Rails.logger.info "Broadcasting update to message_#{id}"
      broadcast_update_to "messages", target: "message_#{id}", partial: "messages/message", locals: { message: self }
    end
    

4. 检查前端JavaScript错误

  • 打开浏览器开发者工具的Console标签页,查看是否有Turbo相关的JavaScript错误,若存在错误需先修复,确保Turbo脚本正常加载并运行。

5. 验证广播内容正确性

  • 可手动触发广播测试,在rails控制台执行:
    message = Message.find(407)
    message.broadcast_update_to "messages", target: "message_#{message.id}", partial: "messages/message", locals: { message: message }
    
    观察页面是否更新,若仍未更新,检查partial渲染的内容是否正确。

内容的提问来源于stack exchange,提问作者elviajero34

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 02:54:51