WebSocket 是一种计算机通信协议,通过单个传输控制协议连接提供同步双向通信通道。WebSockets 使服务器和客户端之间的通信无需轮询服务器以获得响应。
在 Django 中,Channels 封装了 Django 的本地异步视图支持,使 Django 项目不仅能处理 HTTP,还能处理需要长时间连接的协议,如 WebSockets、MQTT、聊天机器人、业余无线电等。WebSockets 常用于需要进行实时通信的场合,如聊天应用程序或从服务器流式传输事件。在本文中,我将使用 WebSockets 演示其在开发聊天应用程序中的应用。
首先,我们将安装 django 和 django-channels,我们将使用这些库来创建应用程序。假定您熟悉设置 Django 项目和创建 Python 环境。
#installing dependancies
python -m pip install django
python -m pip install -U 'channels[daphne]'
注意 “channel[daphne]”,这将安装用于开发目的的 channels 和 daphne ASGI 服务器。Daphne 服务器将从 Django 的默认管理命令中接管管理命令。官方渠道库使用 daphne 开发服务器,但你也可以安装你想要的 ASGI 服务器。Daphne 服务器位于 Django 设置中已安装应用程序列表的顶部。
安装完成后,我们将在 Django 设置中将频道添加到已安装的应用程序中。
# Application definition
INSTALLED_APPS = [
'daphne',
'django.contrib.admin',
...
'chat.apps.ChatConfig', #chat application
]
在 Django myproject / asgi.py文件中,添加以下代码行:
import os
from channels.routing import ProtocolTypeRouter
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')#replace with your app settings
# Initialize Django ASGI application early to ensure the AppRegistry
# is populated before importing code that may import ORM models.
django_asgi_app = get_asgi_application()
application = ProtocolTypeRouter({
"http": django_asgi_app,
# Just HTTP for now. (We can add other protocols later.)
})
我们将更新 settings.py 文件并添加以下配置,以指向上文创建的 ASGI 应用程序,这样 Django 中的所有 aync 请求(本例中为 WebSockets)都将路由到 ASGI 应用程序。
ASGI_APPLICATION = "myproject.asgi.application"
在设置好通道和 Django 之后,我们必须定义应用程序的目的。这将是一个简单的聊天应用程序,将有两个模型:消息模型和房间模型。我们还将创建一个包含聊天页面的模板。在聊天页面中,我们将添加一个 htmx cdn 链接。
先定义模型。
from django.contrib.auth.models import User
from django.db import models
class Room(models.Model):
name = models.CharField(max_length=128)
online = models.ManyToManyField(to=User, blank=True)
def get_online_count(self):
return self.online.count()
def join(self, user):
self.online.add(user)
self.save()
def leave(self, user):
self.online.remove(user)
self.save()
def __str__(self):
return f'{self.name} community'
class Message(models.Model):
user = models.ForeignKey(to=User, on_delete=models.CASCADE)
room = models.ForeignKey(to=Room, on_delete=models.CASCADE)
content = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f'{self.user.username}: {self.content} [{self.timestamp}]'
在上面的文件中,我们有三个模型:一个是上面导入的用户模型,第二个是房间模型,房间模型将包含聊天群组,就像 WhatsApp 和 Telegram 等流行社交媒体中常见的聊天群组一样,最后一个是消息模型,它将包含聊天内容,即从一个用户传递到另一个用户的消息。
我们要实现的目标是,用户登录应用程序,加入聊天室(即聊天群组),然后在聊天室中撰写信息,并将信息发送给群组中的所有用户。
为聊天页面创建一个模板。要创建一个模板,必须创建一个视图函数来渲染页面。我们将使用 Tailwind CSS 来设计页面样式。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- htmx -->
<script src="https://unpkg.com/htmx.org@1.9.6"
integrity="sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni"
crossorigin="anonymous"></script>
<script src="https://unpkg.com/htmx.org/dist/ext/ws.js"></script>
<!-- tailwind -->
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.1.1/flowbite.min.css" rel="stylesheet" />
<title>HTMXchat</title>
</head>
</body>
{% block main %}
{% endblock main %}
</html>
<body >
{% extends "base.html" %}
{% block main %}
<!-- component -->
<div class="flex h-screen antialiased text-gray-800">
<div class="flex flex-row h-full w-full overflow-x-hidden">
<div class="flex flex-col py-8 pl-6 pr-2 w-64 bg-white flex-shrink-0">
<div class="flex flex-row items-center justify-center h-12 w-full">
<div class="flex items-center justify-center rounded-2xl text-indigo-700 bg-indigo-100 h-10 w-10">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z">
</path>
</svg>
</div>
<div class="ml-2 font-bold text-2xl">HTMX-Chat</div>
</div>
<div class="flex flex-col mt-8">
<div class="flex flex-row items-center justify-between text-xs">
<span class="font-bold">Friends</span>
<span class="flex items-center justify-center bg-gray-300 h-4 w-4 rounded-full">4</span>
</div>
<div class="flex flex-col space-y-1 mt-4 -mx-2 h-48 overflow-y-auto">
<button class="flex flex-row items-center hover:bg-gray-100 rounded-xl p-2">
<div class="flex items-center justify-center h-8 w-8 bg-indigo-200 rounded-full">
H
</div>
<div class="ml-2 text-sm font-semibold">Henry Boyd</div>
</button>
<button class="flex flex-row items-center hover:bg-gray-100 rounded-xl p-2">
<div class="flex items-center justify-center h-8 w-8 bg-gray-200 rounded-full">
M
</div>
<div class="ml-2 text-sm font-semibold">Marta Curtis</div>
<div
class="flex items-center justify-center ml-auto text-xs text-white bg-red-500 h-4 w-4 rounded leading-none">
2
</div>
</button>
<button class="flex flex-row items-center hover:bg-gray-100 rounded-xl p-2">
<div class="flex items-center justify-center h-8 w-8 bg-orange-200 rounded-full">
P
</div>
<div class="ml-2 text-sm font-semibold">Philip Tucker</div>
</button>
<button class="flex flex-row items-center hover:bg-gray-100 rounded-xl p-2">
<div class="flex items-center justify-center h-8 w-8 bg-pink-200 rounded-full">
C
</div>
<div class="ml-2 text-sm font-semibold">Christine Reid</div>
</button>
<button class="flex flex-row items-center hover:bg-gray-100 rounded-xl p-2">
<div class="flex items-center justify-center h-8 w-8 bg-purple-200 rounded-full">
J
</div>
<div class="ml-2 text-sm font-semibold">Jerry Guzman</div>
</button>
</div>
</div>
<div class="flex flex-col mt-8">
<div class="flex flex-row items-center justify-between text-xs">
<span class="font-bold">Active Communities</span>
<span class="flex items-center justify-center bg-gray-300 h-4 w-4 rounded-full">4</span>
</div>
<div class="flex flex-col space-y-1 mt-4 -mx-2 h-48 overflow-y-auto">
{% for group in groups %}
<button class="flex flex-row items-center hover:bg-gray-100 rounded-xl p-2"
hx-post="{% url 'group' group.id %}" hx-target="#chat-window"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
<div class="flex items-center justify-center h-8 w-8 bg-gray-200 rounded-full">
{{forloop.counter}}
</div>
<div class="ml-2 text-sm font-semibold">{{group.name}}</div>
<div
class="flex items-center justify-center ml-auto text-xs text-white bg-red-500 h-4 w-4 rounded leading-none">
{{group.get_online_count}}
</div>
</button>
{% endfor %}
</div>
</div>
</div>
<div class="flex flex-col flex-auto h-full p-6" id="chat-window">
</div>
</div>
</div>
{% endblock main %}
请注意,在 base.html 文件中包含了 htmx cdn。
@login_decorator
def talk(request):
"""chat page view"""
return render(request, ".html", {'groups':Room.objects.all()})
上述方法将渲染 chat.html 页面。我们将渲染数据库中的所有房间对象。为简单起见,我们将使用 Django 管理页面创建房间对象。创建几个房间,如音乐、电影或游戏。记住,这些房间就是聊天群组。记住添加 @login_decorator 以限制只有通过身份验证的用户才能访问。因此,我们必须创建身份验证系统。
class RegistrationForm(UserCreationForm):
class Meta:
model = get_user_model()
fields = "__all__
{% extends "base.html" %}
{% block main %}
<div class="items-center p-5">
<form action="." method="post">
{% csrf_token %}
<div class="relative z-0 w-full mb-6 group">
<input type="username" name="username" id="floating_username"
class="block py-2.5 px-0 w-full text-sm text-gray-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer"
placeholder=" " required />
<label for="floating_username"
class="peer-focus:font-medium absolute text-sm text-gray-500 dark:text-gray-400 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:start-0 rtl:peer-focus:translate-x-1/4 rtl:peer-focus:left-auto peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6">Username</label>
</div>
<div class="relative z-0 w-full mb-6 group">
<input type="password" name="password" id="floating_password"
class="block py-2.5 px-0 w-full text-sm text-gray-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer"
placeholder=" " required />
<label for="floating_password"
class="peer-focus:font-medium absolute text-sm text-gray-500 dark:text-gray-400 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:start-0 rtl:peer-focus:translate-x-1/4 rtl:peer-focus:left-auto peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6">Username</label>
</div>
<button type="submit"
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Submit</button>
</form>
<div class="w-auto text-md mt-3">
<a href="{% url 'signup' %}" class="font-medium text-blue-600 underline dark:text-blue-500 hover:no-underline">Don't have an account?</a>
</div>
</div>
{% endblock main %}
{% extends "base.html" %}
{% block main %}
<!-- registration form -->
<div class="items-center p-5">
<form action="." method="post">
{% csrf_token %}
<div class="relative z-0 w-full mb-6 group">
<input type="username" name="username" id="floating_username"
class="block py-2.5 px-0 w-full text-sm text-gray-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer"
placeholder=" " required />
<label for="floating_username"
class="peer-focus:font-medium absolute text-sm text-gray-500 dark:text-gray-400 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:start-0 rtl:peer-focus:translate-x-1/4 rtl:peer-focus:left-auto peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6">Username</label>
</div>
<div class="grid md:grid-cols-2 md:gap-6">
<div class="relative z-0 w-full mb-6 group">
<input type="text" name="floating_first_name" id="floating_first_name"
class="block py-2.5 px-0 w-full text-sm text-gray-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer"
placeholder=" " required />
<label for="floating_first_name"
class="peer-focus:font-medium absolute text-sm text-gray-500 dark:text-gray-400 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:start-0 rtl:peer-focus:translate-x-1/4 peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6">First
name</label>
</div>
<div class="relative z-0 w-full mb-6 group">
<input type="text" name="last_name" id="floating_last_name"
class="block py-2.5 px-0 w-full text-sm text-gray-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer"
placeholder=" " required />
<label for="floating_last_name"
class="peer-focus:font-medium absolute text-sm text-gray-500 dark:text-gray-400 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:start-0 rtl:peer-focus:translate-x-1/4 peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6">Last
name</label>
</div>
</div>
<div class="relative z-0 w-full mb-6 group">
<input type="email" name="email" id="floating_email"
class="block py-2.5 px-0 w-full text-sm text-gray-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer"
placeholder=" " required />
<label for="floating_email"
class="peer-focus:font-medium absolute text-sm text-gray-500 dark:text-gray-400 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:start-0 rtl:peer-focus:translate-x-1/4 rtl:peer-focus:left-auto peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6">Email
address</label>
</div>
<div class="relative z-0 w-full mb-6 group">
<input type="password" name="password1" id="floating_password"
class="block py-2.5 px-0 w-full text-sm text-gray-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer"
placeholder=" " required />
<label for="floating_password"
class="peer-focus:font-medium absolute text-sm text-gray-500 dark:text-gray-400 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:start-0 rtl:peer-focus:translate-x-1/4 peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6">Password</label>
</div>
<div class="relative z-0 w-full mb-6 group">
<input type="password" name="password2" id="floating_repeat_password"
class="block py-2.5 px-0 w-full text-sm text-gray-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer"
placeholder=" " required />
<label for="floating_repeat_password"
class="peer-focus:font-medium absolute text-sm text-gray-500 dark:text-gray-400 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:start-0 rtl:peer-focus:translate-x-1/4 peer-focus:text-blue-600 peer-focus:dark:text-blue-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6">Confirm
password</label>
</div>
<button type="submit"
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Submit</button>
</form>
</div>
{% endblock main %}
class RegistrationView(CreateView): #signup
template_name = "signup.html"
model = get_user_model()
form_class = RegistrationForm
success_url = reverse_lazy("login")
class SigninView(LoginView): #login
next_page = "/chat/talk/"
template_name = "signin.html"
上面的代码为我们的应用程序演示了一个简单的身份验证系统。用户必须通过身份验证才能使用应用程序,因此我们将使用 “login required “装饰器。
让我们关注通道。要使用通道,我们必须创建两个文件 routing.py 和 consumers.py,这两个文件分别类似于 Django 的 urls.py 和 views.py。让我们定义 consumers.py 文件:
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
from django.template.loader import get_template
from .models import Room, Message, User
class TalkConsumer(WebsocketConsumer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = None
self.room = None
self.group_name = None
def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_id']
self.group_name = f'chat_{self.room}'
self.room = Room.objects.get(id=self.room_name)
self.user = self.scope["user"]
# accept connection
self.accept()
# join the room/group
async_to_sync(self.channel_layer.group_add)(self.group_name, self.channel_name)
# user joined notification
html = get_template("partial/join.html").render(context={"user":self.user})
self.send(text_data=html)
def disconnect(self, close_code):
async_to_sync(self.channel_layer.group_discard)(self.group_name,self.channel_name)
html = get_template("partial/leave.html").render(context={"user":self.user})
self.send(
text_data=html
)
self.room.online.remove(self.user)
def receive(self, text_data=None, bytes_data=None):
text_data_json = json.loads(text_data)
room = Room.objects.get(id=self.room_name)
Message.objects.create(user=self.user, room=room, content=text_data_json['message'])
html = get_template("chats.html").render(context={'messages': room.message_set.all()})
self.send(text_data=html)
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r"talk/(?P<room_id>\w+)/$", consumers.TalkConsumer.as_asgi()),
]
在 consumers.py 文件中,我们创建了一个名为 TalkConsumer 的类,它继承自 WebSocketConsumer 类。Talk 类将负责使用 WebSockets 接收和发送聊天。
TalkConsumer 类只有三个主要方法。
1. connect(self)
该方法在客户端初始连接时首先被调用,例如根据特定条件接受或拒绝连接,或向客户端发送初始消息。在上面的示例中,该方法的作用如下。获取房间 ID,然后创建组名。从作用域中获取房间实例。作用域包含 WebSocket 连接的所有信息。它类似于 Django 视图中的请求。我们还将从作用域中获取已登录的用户实例。self.accept(),该方法接受 WebSocket 连接,从而启动与客户端的双向连接。创建连接后,用户会被添加到一个组中,记住这个组就是我们根据 WebSocket 发送的 ID 检索到的房间,在频道中它们被称为一个组。频道会给组任意命名,但我们希望控制组的命名,这就是我们使用房间和组名的原因。
# 加入房间/组
async_to_sync(self.channel_layer.group_add)(self.group_name, self.channel_name)
aysnc_too_sync 是一个从 Django 的 async.grief 中导入的方法。它将异步方法转换为同步方法。它将 channel_layer.group_add 转换为同步方法,然后 (self.group_name, self.channel_name) 从作用域中获取组名和通道名,并将通道名添加到组中。需要注意的是,通过 WebSocket 建立的每个连接都会创建一个唯一的通道,现在我们把所有这些唯一的通道放到一个组中,这样所有这些通道就能与拥有自己通道的其他通道(用户)进行对话。聊天功能就是这样创建的。
你可能会问,为什么我们要将异步方法转换为同步方法?因为通道和 Django 不同,Django 在访问数据库时会同步执行代码,但通道会使用通道层,而通道必须异步访问。
在继续之前,我们必须配置通道层。通道层是一种通信系统,它允许应用程序的多个部分交换信息,而无需通过数据库传输所有信息或事件。我们需要一个通道层来为消费者提供相互对话的能力。
docker run -- rm -p 6379:6379 redis:7
python3 -m pip installchannels_redis
从上面的代码片段中,创建了一个 docker 容器,其中包含将用作通道层的 Redis 映像,然后安装channels_redis ,它将 Django 应用程序与 Redis 连接。安装 Redis 和 Channels_redis 后,我们必须在设置文件中对其进行配置。
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("127.0.0.1", 6379)],
},
},
}
html = get_template("partial/join.html").render(context={"user":self.user})
self.send(text_data=html)
从上面的代码片段中,我们使用一个名为join.html的模板文件,从范围传递用户实例并将其发送到客户端,该模板是对所有组成员的通知。
2. disconnect(self)
当客户端终止连接时调用此方法,它接受 close_code 参数,该参数是一个整数,表示连接关闭的原因。该方法在功能块内具有以下功能。
async_to_sync(self.channel_layer.group_discard)(self.group_name,self.channel_name)
html = get_template( "partial/leave.html" ).render(context={ "user" :self.user})
self.send(
text_data= html
)
self.room.online.remove(self.user)
上述方法将从群组中删除用户,并向客户端的群组发送通知,其中包含用户已离开群组并且用户已从房间中删除的消息。
3. receive(self, text_data=None, bytes_data=None)
当消费者通过 WebSocket 从客户端接收到消息时,就会调用该方法。它处理传入的数据,并定义服务器如何响应这些信息。在我们的例子中,该方法将反序列化客户端的消息,并创建一个消息实例,然后用一个包含该特定房间所有消息的 html 文件做出响应。它使用反向关系来获取特定房间的所有信息。
在此,我们将重点介绍 htmx。Htmx 是一个库,它允许你直接从 HTML 访问现代浏览器的功能,而不是使用 JavaScript。
在我们的聊天页面上,有一个代码块包含了应用程序中的所有群组。
{% for group in groups %}
<button class="flex flex-row items-center hover:bg-gray-100 rounded-xl p-2"
hx-post="{% url 'group' group.id %}" hx-target="#chat-window"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
<div class="flex items-center justify-center h-8 w-8 bg-gray-200 rounded-full">
{{forloop.counter}}
</div>
<div class="ml-2 text-sm font-semibold">{{group.name}}</div>
<div class="flex items-center justify-center ml-auto text-xs text-white bg-red-500 h-4 w-4 rounded leading-none">
{{group.get_online_count}}
</div>
</button>
{% endfor %}
本节包含一些与 HTML 标签属性类似的 htmx 属性,它们可作为 HTML 标签属性使用。
- hx-post = “{% url ‘group’ group.id %}” – 向组 URL 发送 post 请求,参数为组 id。
- hx-target=”#chat-window” – 将响应交换到 id 为 “#chat-window “的 div 中。
- hx-headers=”{ ‘X-CSRFToken”: {{csrf_token}}'”。}” – 此属性用于在请求标头中添加 CSRF 标记。我们添加 CSRF 标记是因为这是一个 POST 请求。
该 POST 请求的响应将在 #chat-window 元素的内部进行交换。
左下方是我创建的各种房间/群组(称为活跃社区)。一旦我们点击任何一个群组,服务器发出的响应将在页面右侧灰色空白处进行交换。例如,我点击了开发组,响应将如下所示。
右上角的通知是一个名为 Chris 的用户已加入该群组的通知,这是该用户首次加入该群组时发送的通知,该群组中的所有用户都可以看到它。在底部,我们有聊天输入元素标签,其中将写入和发送消息,响应将显示在灰色阴影区域。
当我们点击开发组时,服务器发送的响应包含一些 htmx 属性,使我们能够使用 WebSockets 与服务器建立连接。
<div class="flex flex-col flex-auto flex-shrink-0 rounded-2xl bg-gray-100 h-full p-4" hx-ext="ws"
ws-connect="/talk/{{room_name}}/">
ws-connect=’/talk/{{room_name}}/” 将与 WebSocket 的 URL 建立连接。请记住,我们已经在 routing.py 文件中定义了 WebSocket 的 URL。{{room_name}} 是我们加入的房间 ID 或群组 ID。{{room_name}}上下文变量由视图发送,视图会在选择组别时发送响应。
@login_required
def group(request, group_name):
"""chat window view"""
group = Room.objects.get(id=group_name)
messages = group.message_set.all()
return render(request, "partial/group-chat.html",{"messages":messages, "room_name":group_name,"name":group})
从上面的服务器响应中,我们在表单元素中有以下代码片段。
<form id="chat-form" class="w-100">
<input type="text"
class="flex w-full border rounded-xl focus:outline-none focus:border-indigo-300 pl-4 h-10 w-100"
name="message" placeholder="chat here" />
<button ws-send
class="absolute flex items-center justify-center h-full w-12 right-0 top-0 text-gray-400 hover:text-gray-600">
<svg class="w-4 h-4 transform rotate-45 -mt-px" fill="none"
stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
</svg>
</button>
</form>
请注意,我在按钮标签中添加了 ws-send htmx 属性,该属性将根据触发值向最近的 WebSocket 发送信息。由于这是一个按钮,触发值将始终是 onClick 事件或按下键盘上的 Enter 键时。Htmx 将以 JSON 格式发送消息,因此在 Consumer 的 receive 方法中,我们必须反序列化客户端发送的 text_data。当 Htmx 通过 WebSocket 向服务器发送消息时,消费者类中的 receive 方法将获取消息并将其保存到数据库中,然后检索该房间中的所有消息,并以 HTML 格式将其发送回客户端。
<form id="chat-form">
<input type="text" class="flex w-full border rounded-xl focus:outline-none focus:border-indigo-300 pl-4 h-10"
name="message" />
<button ws-send
class="absolute flex items-center justify-center h-full w-12 right-0 top-0 text-gray-400 hover:text-gray-600">
<svg class="w-4 h-4 transform rotate-45 -mt-px" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8">
</path>
</svg>
</button>
</form>
<div class="col-start-1 col-end-8 p-3 rounded-lg" id="chat-room" hx-swap-oob="true">
{% for message in messages %}
{% if message.user != request.user %}
<div class="col-start-6 col-end-13 p-3 rounded-lg mt-4">
<div class="flex items-center justify-start flex-row-reverse">
<div
class="flex items-center justify-center h-10 w-10 rounded-full bg-indigo-500 flex-shrink-0">
{{message.user}}
</div>
<div
class="relative mr-3 text-sm bg-indigo-100 py-2 px-4 shadow rounded-xl">
<div>{{message.content}}</div>
<div class="absolute text-xs bottom-0 right-0 -mb-5 mr-2 text-gray-500">
{{message.timestamp|time}}
</div>
</div>
</div>
</div>
{% else %}
<div class="flex flex-row items-center mt-4">
<div
class="flex items-center justify-center h-10 w-10 rounded-full bg-indigo-500 flex-shrink-0">
{{message.user}}
</div>
<div class="relative ml-3 text-sm bg-white py-2 px-4 shadow rounded-xl">
<div>{{message.content}}</div>
<div class="absolute text-xs bottom-0 right-0 -mb-5 mr-2 text-gray-500">
{{message.timestamp|time}}
</div>
</div>
</div>
{% endif %}
{% endfor %}
</div>
这将是服务器发送给客户端的响应。请注意上述代码片段第 14 行中的 htmx 属性 hx-swap-oob=”true”。这将促使 htmx 交换所有与响应中所有元素 ID 相同的元素,通过使用这种机制,通知就会发送给用户,因为服务器中的响应会交换一个空的通知元素。这个过程使我们能够更改输入标签并渲染所有聊天内容,因为响应会发送新元素来交换 DOM 中的当前元素。请查看 chats.html 文件。具有 ID 的元素会被响应中具有相同 ID 值的新元素交换。
结论
我们的消费者类是同步的,但也可以使用 Python asyncio 将其异步化。要创建异步消费者,必须创建一个继承自 AsyncWebsocketConsumer 的消费者类。
Htmx 是一个很棒的库,它与 Django 模板框架集成得很好,因为它使用 HTML 元素,Htmx 帮助您事半功倍。
作者:Christopher Gathuthi
原文:https://medium.com/@soverignchriss/chat-application-with-django-channels-htmx-dd8174f59a03
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/im/38697.html