Django Channels与信号联动问题:后台新增通知前端不更新
问题背景
使用Vue从Django获取通知,需在徽章中显示并实时更新数量。当前技术栈:Django 4.2.8、Channels 4、uvicorn 0.26、websockets 12.0。通过Django的post_save信号触发consumers.py中的update_notification_count方法更新计数,浏览器端操作正常,但从Django后台新增通知时,post_save事件已触发,update_notification_count却未被调用,前端无法实时更新,仅刷新浏览器后同步数据。
相关代码配置
settings.py
# settings.py CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels.layers.InMemoryChannelLayer', 'CONFIG': { 'capacity': 1000, 'expiry': 60, }, } }
asgi.py
import os from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter, URLRouter from notifapi.routing import websocket_urlpatterns os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproj.settings') application = ProtocolTypeRouter({ "http": get_asgi_application(), "websocket": URLRouter(websocket_urlpatterns), })
signals.py
from django.dispatch import receiver from django.db.models.signals import post_save from channels.layers import get_channel_layer from asgiref.sync import async_to_sync from .models import NotifyModel as Notification from .consumers import NotificationConsumer @receiver(post_save, sender=Notification) def notification_created(sender, instance, created, **kwargs): try: if created: print(f"A new notification was created {instance.message}") channel_layer = get_channel_layer() async_to_sync(channel_layer.group_send)( "public_room", { "type": "update_notification_count", "message": instance.message } ) except Exception as e: print(f"Error in group_send: {e}")
consumers.py
from channels.generic.websocket import AsyncWebsocketConsumer from django.apps import apps from django.core.serializers import serialize from asgiref.sync import sync_to_async import json import logging class NotificationConsumer(AsyncWebsocketConsumer): async def connect(self): # Allow all connections await self.channel_layer.group_add("public_room", self.channel_name) await self.accept() await self.update_notification_count() async def update_notification_count(self, event=None): print("update_notification_count method called with event:", event) NotifyModel = apps.get_model('notifapi', 'NotifyModel') notifications = await sync_to_async(list)(NotifyModel.objects.all().values('is_read')) messages = await sync_to_async(list)(NotifyModel.objects.all().values('message')) # Get the notification count asynchronously using a custom utility method notification_count = len(notifications) #print(f"Notification count is {notification_count}") # Extracting is_read values from notifications is_read_values = [notification['is_read'] for notification in notifications] messages_values = [notification['message'] for notification in messages] #print("Am I here?") print(f"Messages values are: {messages_values}") await self.send(text_data=json.dumps({ "type": "notification.update", "count": notification_count, "is_read_values": is_read_values, "messages_values": messages_values })) async def disconnect(self, close_code): print("Consumer disconnected") # Remove the channel from the public_room group when the WebSocket connection is closed await self.channel_layer.group_discard( "public_room", self.channel_name ) async def receive(self, text_data): # Handle incoming messages (if any) data = json.loads(text_data) if data['type'] == 'update.notification.count': await self.update_notification_count()
Vue相关代码
App.vue
// App.vue <template> <div id="app"> <div id="wrapper"> <NotificationBell /> </div> </div> </template> <script> import NotificationBell from './components/NotificationBell.vue'; export default { components: { NotificationBell, }, }; </script>
NotificationBell.vue
<template> <div class="fancy-container"> <a href="#" class="position-relative"> <i class="fa fa-bell _gray" style="font-size:24px"></i> <span class="my-text btnLnk">Visits</span> <span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger _reduced"> {{ notificationCount }} </span> </a> </div> </template> <script> //import Axios from 'axios'; export default { data() { return { notifications: [], webSocket: null }; }, computed: { notificationCount() { // Calculate the notification count based on the current state of notifications return this.notifications.filter(notification => !notification.fields.is_read).length; } }, mounted() { this.establishWebSocketConnection(); }, methods: { async establishWebSocketConnection() { this.webSocket = new WebSocket('ws://127.0.0.1:8001/websocket/ws/notifications/', 'echo-protocol'); this.webSocket.onopen = () => { console.log('WebSocket connection established!'); this.updateNotificationCount(); }; this.webSocket.onmessage = (event) => { console.log("Message received:", event.data); const message = JSON.parse(event.data); console.log("Received type:", message.type); // Log the type field to identify the message type if(message.type === 'notification.update'){ this.notifications = message.is_read_values.map(is_read => ({ fields: { is_read } })); }else{ console.log("Notification count was not updated!") } }; this.webSocket.onclose = () => { console.log('WebSocket connection closed.'); // implement reconnect logic if desired }; }, updateNotificationCount() { console.log('updateNotificationCount called!'); // Send a message to the WebSocket server to request updated notification count this.webSocket.send(JSON.stringify({ "type": "update.notification.count" // Define a custom type to trigger the count update })); }, }, }; </script>
核心问题排查与解决步骤
1. 替换InMemoryChannelLayer为RedisChannelLayer
InMemoryChannelLayer是进程内通信实现,若用runserver启动Django,后台管理和Channels会运行在不同进程中,信号发送的消息无法跨进程传递到Consumer。切换到RedisChannelLayer解决跨进程通信问题:
- 安装依赖:
pip install channels-redis - 修改
settings.py中的CHANNEL_LAYERS配置:CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": { "hosts": [("127.0.0.1", 6379)], }, }, } - 确保本地Redis服务处于运行状态。
2. 确认信号模块已正确注册
Django信号需要被应用加载才能生效,在notifapi/apps.py中添加信号导入:
from django.apps import AppConfig class NotifapiConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'notifapi' def ready(self): import notifapi.signals # 注册信号
同时在settings.py的INSTALLED_APPS中替换为:
INSTALLED_APPS = [ # ...其他应用 'notifapi.apps.NotifapiConfig', ]
3. 用uvicorn正确启动ASGI应用
不要用python manage.py runserver启动,改用uvicorn直接启动ASGI入口:
uvicorn myproj.asgi:application --host 127.0.0.1 --port 8001
确保所有请求(HTTP和WebSocket)都通过同一个ASGI进程处理,避免进程隔离导致的消息丢失。
4. 验证group_send的正确性
在信号中添加更多日志,确认channel_layer实例有效:
@receiver(post_save, sender=Notification) def notification_created(sender, instance, created, **kwargs): try: if created: print(f"A new notification was created {instance.message}") channel_layer = get_channel_layer() print(f"Channel layer instance: {channel_layer}") # 确认channel_layer不为None async_to_sync(channel_layer.group_send)( "public_room", { "type": "update_notification_count", "message": instance.message } ) print("Group send executed successfully") except Exception as e: print(f"Error in group_send: {e}")
如果group_send执行无报错但Consumer没收到消息,检查Consumer是否正确加入了public_room组(代码中connect方法已添加,可保留日志确认)。
重构优化建议(可选)
- 合并数据库查询:Consumer中两次查询NotifyModel可合并为一次,减少数据库交互:
async def update_notification_count(self, event=None): NotifyModel = apps.get_model('notifapi', 'NotifyModel') # 一次查询获取所有需要的字段 notifications = await sync_to_async(list)(NotifyModel.objects.all().values('is_read', 'message')) notification_count = len(notifications) is_read_values = [n['is_read'] for n in notifications] messages_values = [n['message'] for n in notifications] await self.send(text_data=json.dumps({ "type": "notification.update", "count": notification_count, "is_read_values": is_read_values, "messages_values": messages_values })) - 直接传递未读计数:后端计算未读数量后传给前端,避免前端重复过滤:
前端直接使用# 在Consumer中计算未读数量 unread_count = await sync_to_async(lambda: NotifyModel.objects.filter(is_read=False).count())() # 发送时替换count为unread_count await self.send(text_data=json.dumps({ "type": "notification.update", "count": unread_count, # ...其他字段 }))message.count即可:computed: { notificationCount() { return this.notifications.length ? this.notifications[0].count : 0; } }
内容的提问来源于stack exchange,提问作者ionecum

