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

Django Channels Consumer推送WebSocket消息前端重复显示两次问题

问题根因

消息重复显示是消息推送/渲染链路存在冗余执行导致,按出现概率从高到低排序:

  • 前端同时写了两套消息渲染逻辑:调用send()发消息时本地直接插入一条消息,后续收到服务端广播的同一条消息时又插入一次
  • Consumer端重复推送:receive方法中既单独调用self.send()给当前发送方连接回推消息,又调用group_send()向群组广播消息——发送方本身在群组内,会收到两次同源消息
  • 前端未做WebSocket实例单例,组件重渲染时创建了多个WS连接,单条广播会被多连接接收,触发多次渲染
修复方案

后端Consumer逻辑修正

核心规则:所有消息统一走群组广播,不要单独给发送方回推;消息体必须携带发送方唯一标识,供前端区分消息归属。
参考实现:

import json
from channels.generic.websocket import AsyncWebsocketConsumer

class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        self.room_id = self.scope["url_route"]["kwargs"]["room_id"]
        self.room_group = f"chat_room_{self.room_id}"
        # 加入聊天组
        await self.channel_layer.group_add(
            self.room_group,
            self.channel_name
        )
        await self.accept()

    async def disconnect(self, code):
        # 退出聊天组
        await self.channel_layer.group_discard(
            self.room_group,
            self.channel_name
        )

    async def receive(self, text_data=None, bytes_data=None):
        req_data = json.loads(text_data)
        content = req_data.get("content", "").strip()
        if not content:
            return
        # 取当前登录用户名作为发送者标识,未登录场景可用self.channel_name替代
        sender_name = self.scope["user"].username if not self.scope["user"].is_anonymous else self.channel_name
        # 禁止在此处单独调用self.send()给当前连接回消息
        # 所有消息统一广播到群组,发送方作为群组成员自然能收到
        await self.channel_layer.group_send(
            self.room_group,
            {
                "type": "push_chat_msg",
                "content": content,
                "sender": sender_name
            }
        )

    async def push_chat_msg(self, event):
        # 组广播触发的推送方法,直接把消息发给当前连接
        await self.send(text_data=json.dumps({
            "content": event["content"],
            "sender": event["sender"]
        }))

前端逻辑修正

核心规则:所有消息统一在WS的onmessage回调中渲染,发送时不要做本地回显;全局缓存WS实例,避免重复创建连接。
参考实现:

// 全局缓存WS实例,防止重复创建
let chatWs = null;
const chatBox = document.getElementById("chat-box");
const inputEl = document.getElementById("msg-input");
const sendBtn = document.getElementById("send-btn");
// 替换为当前登录的用户名,和后端返回的sender标识对齐
const currentUser = window.currentUsername;

function initChatWs() {
  // 存在旧实例先销毁
  if (chatWs) chatWs.close();
  const protocol = window.location.protocol === "https:" ? "wss" : "ws";
  chatWs = new WebSocket(`${protocol}://${window.location.host}/ws/chat/替换为你的房间ID/`);

  chatWs.onmessage = (event) => {
    const msgData = JSON.parse(event.data);
    // 统一在这渲染所有消息,不管是自己发的还是其他人发的
    const msgNode = document.createElement("div");
    msgNode.className = msgData.sender === currentUser ? "msg-item self" : "msg-item other";
    msgNode.innerText = `${msgData.sender}: ${msgData.content}`;
    chatBox.appendChild(msgNode);
    chatBox.scrollTop = chatBox.scrollHeight;
  };

  chatWs.onclose = () => {
    // 断线3秒后重连
    setTimeout(initChatWs, 3000);
  };
}

// 页面加载完成初始化WS
window.addEventListener("DOMContentLoaded", initChatWs);

// 发送消息逻辑
sendBtn.addEventListener("click", () => {
  const msgContent = inputEl.value.trim();
  if (!msgContent || chatWs?.readyState !== WebSocket.OPEN) return;
  chatWs.send(JSON.stringify({ content: msgContent }));
  // 禁止在这里写插入消息到聊天框的逻辑,等onmessage收到广播再渲染
  inputEl.value = "";
});
快速排查步骤
  1. 打开浏览器F12控制台,进入网络面板筛选WS连接,查看消息帧:发送单条消息后如果服务端返回2条同源消息,就是后端重复推送问题;如果只返回1条,就是前端重复渲染问题
  2. 如果服务端返回2条以上消息,直接在new WebSocket初始化位置打断点,统计执行次数,大概率是重复创建连接导致

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 20:51:31