使用 Django-channels 和 Htmx 搭建聊天应用程序

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 元素的内部进行交换。

使用 Django-channels 和 Htmx 搭建聊天应用程序

左下方是我创建的各种房间/群组(称为活跃社区)。一旦我们点击任何一个群组,服务器发出的响应将在页面右侧灰色空白处进行交换。例如,我点击了开发组,响应将如下所示。

使用 Django-channels 和 Htmx 搭建聊天应用程序

右上角的通知是一个名为 Chris 的用户已加入该群组的通知,这是该用户首次加入该群组时发送的通知,该群组中的所有用户都可以看到它。在底部,我们有聊天输入元素标签,其中将写入和发送消息,响应将显示在灰色阴影区域。

使用 Django-channels 和 Htmx 搭建聊天应用程序

当我们点击开发组时,服务器发送的响应包含一些 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

(0)

发表回复

登录后才能评论