实时 Django 第 3 部分:使用 django、RabbitMQ 和 Vue.js 构建聊天应用程序(使用 django Rest 框架构建 API)

我们用来djoser构建身份验证后端,然后将前端 Vue.js 应用程序连接到它。

在这一部分中,我们将使用 django Rest 框架构建一个 API,该 API 应该为我们提供端点来启动新的聊天会话、加入聊天会话、发布新消息以及获取聊天会话的消息历史记录。

顶层架构

在开始之前,让我们从更高的层面讨论一切是如何运作的

实时 Django 3.1

概述

  • 当用户发送消息时,该消息将通过 API 转发到 django。
  • django收到消息后,也会转发给RabbitMQ。
  • RabbitMQ 使用exchange将消息广播到多个队列。队列是最终将消息传递给客户端的通信通道。Worker 是后台进程,负责广播和传递消息的实际工作。

    RabbitMQ 是将我们应用程序的两个重要部分(Django 和 uWSGI)连接在一起的粘合剂。它还使我们的应用程序非常灵活,因为除了 django 和 python 之外。它们有多种方法可以将消息发送到 RabbitMQ,甚至可以从命令行发送消息!这意味着不知道我们的聊天应用程序的其他应用程序仍然可以与其通信。

    例如,编写的桌面应用程序C#可以将消息放入 RabbitMQ 队列中,我们的客户端甚至移动应用程序都会收到该消息。 如果没有 RabbitMQ,uWSGI WebSocket 服务器是愚蠢的,并且对我们的 django 应用程序一无所知(如何访问数据库、身份验证等),因为它完全取决于您的设置在不同的进程甚至不同的网络服务器中运行。
  • uWSGI 充当 websocket 服务器。客户端建立连接并指定他们想要从中接收消息的通道(RabbitMQ 交换)后。我们将在收到消息后立即阅读消息,并使用 WebSocket 立即将其发送给用户。

如果您担心拥有额外的节点服务器。webpack 开发服务器只是为本地开发提供便利,当您准备好部署应用程序时,您可以通过运行以下命令来捆绑应用程序:

npm build

生成的静态文件可以由任何有能力的 Web 服务器提供服务,例如NginxApache甚至github pages. 本质上,Vue 层并不真正存在,它通常是用户的 Web 浏览器。

执行

在这一部分中,我们的目标是使用 django Rest 框架实现 API。该 API 将允许用户启动新的聊天会话、加入现有会话以及发送消息。它还允许我们从聊天会话中检索消息。

让我们启动一个新的 django 应用程序,名为chat

$ python manage.py startapp chat

请确保INSTALLED_APPS在继续之前将新应用程序添加到列表中。

接下来,我们将创建模型来保存消息、聊天会话和关联用户的数据。让我们在 中创建一些新模型models.py

"""Models for the chat app."""

from uuid import uuid4

from django.db import models
from django.contrib.auth import get_user_model


User = get_user_model()


def deserialize_user(user):
    """Deserialize user instance to JSON."""
    return {
        'id': user.id, 'username': user.username, 'email': user.email,
        'first_name': user.first_name, 'last_name': user.last_name
    }


class TrackableDateModel(models.Model):
    """Abstract model to Track the creation/updated date for a model."""

    create_date = models.DateTimeField(auto_now_add=True)
    update_date = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True


def _generate_unique_uri():
    """Generates a unique uri for the chat session."""
    return str(uuid4()).replace('-', '')[:15]


class ChatSession(TrackableDateModel):
    """ A Chat Session. The uri's are generated by taking the first 15 characters from a UUID """

    owner = models.ForeignKey(User, on_delete=models.PROTECT)
    uri = models.URLField(default=_generate_unique_uri)


class ChatSessionMessage(TrackableDateModel):
    """Store messages for a session."""

    user = models.ForeignKey(User, on_delete=models.PROTECT)
    chat_session = models.ForeignKey(
        ChatSession, related_name='messages', on_delete=models.PROTECT
    )
    message = models.TextField(max_length=2000)

    def to_json(self):
        """deserialize message to JSON."""
        return {'user': deserialize_user(self.user), 'message': self.message}


class ChatSessionMember(TrackableDateModel):
    """Store all users in a chat session."""

    chat_session = models.ForeignKey(
        ChatSession, related_name='members', on_delete=models.PROTECT
    )
    user = models.ForeignKey(User, on_delete=models.PROTECT)

确保在继续之前运行迁移,以便可以创建数据库表。

下一步是创建视图(API 端点),我们的 Vue 应用程序将使用该视图来操作服务器上的数据。

我们可以轻松地使用 django Rest 框架来创建它们(我们不会使用序列化器,因为我们的模型非常简单)。现在让我们这样做views.py

"""Views for the chat app."""

from django.contrib.auth import get_user_model
from .models import (
    ChatSession, ChatSessionMember, ChatSessionMessage, deserialize_user
)

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions


class ChatSessionView(APIView):
    """Manage Chat sessions."""

    permission_classes = (permissions.IsAuthenticated,)

    def post(self, request, *args, **kwargs):
        """create a new chat session."""
        user = request.user

        chat_session = ChatSession.objects.create(owner=user)

        return Response({
            'status': 'SUCCESS', 'uri': chat_session.uri,
            'message': 'New chat session created'
        })

    def patch(self, request, *args, **kwargs):
        """Add a user to a chat session."""
        User = get_user_model()

        uri = kwargs['uri']
        username = request.data['username']
        user = User.objects.get(username=username)

        chat_session = ChatSession.objects.get(uri=uri)
        owner = chat_session.owner

        if owner != user:  # Only allow non owners join the room             chat_session.members.get_or_create(
                user=user, chat_session=chat_session
            )

        owner = deserialize_user(owner)
        members = [
            deserialize_user(chat_session.user) 
            for chat_session in chat_session.members.all()
        ]
        members.insert(0, owner)  # Make the owner the first member 
        return Response ({
            'status': 'SUCCESS', 'members': members,
            'message': '%s joined the chat' % user.username,
            'user': deserialize_user(user)
        })
    

class ChatSessionMessageView(APIView):
    """Create/Get Chat session messages."""

    permission_classes = (permissions.IsAuthenticated,)

    def get(self, request, *args, **kwargs):
        """return all messages in a chat session."""
        uri = kwargs['uri']

        chat_session = ChatSession.objects.get(uri=uri)
        messages = [chat_session_message.to_json() 
            for chat_session_message in chat_session.messages.all()]

        return Response({
            'id': chat_session.id, 'uri': chat_session.uri,
            'messages': messages
        })

    def post(self, request, *args, **kwargs):
        """create a new message in a chat session."""
        uri = kwargs['uri']
        message = request.data['message']

        user = request.user
        chat_session = ChatSession.objects.get(uri=uri)

        ChatSessionMessage.objects.create(
            user=user, chat_session=chat_session, message=message
        )

        return Response ({
            'status': 'SUCCESS', 'uri': chat_session.uri, 'message': message,
            'user': deserialize_user(user)
        })

patch方法ChatSessionView是幂等的,因为多次向它发出请求会得到相同的结果。这意味着用户可以多次加入聊天室,但响应中(以及我们的数据库表中)只会有该用户的一个实例。

关于 patch 方法需要注意的另一件事是,它返回聊天室的所有者作为成员,但在我们的数据库中,我们从未将所有者添加为房间的成员,我们只是检索他的信息并将其插入到返回的列表中返回给客户端。让所有者作为数据库中聊天室的成员来复制信息是没有意义的。

patch我们可以通过调用轻松地在方法中获取用户request.user,而是从发布的数据中获取用户名并使用它来获取用户。这会导致额外的数据库SELECT,但我们为什么要这样做呢?

让我给您一个简单的场景,如果我们决定通过用户名邀请朋友参加聊天会话,会发生什么。request.user我们将无法做到这一点,因为将request.user引用发出请求的当前经过身份验证的用户。

https://googleads.g.doubleclick.net/pagead/ads?client=ca-pub-8618431079416074&output=html&h=200&slotname=8239403128&adk=2243974991&adf=1821865917&pi=t.ma~as.8239403128&w=867&fwrn=4&lmt=1616559018&rafmt=11&format=867×200&url=https%3A%2F%2Fdanidee10.github.io%2F2018%2F01%2F07%2Frealtime-django-3.html&wgl=1&uach=WyJXaW5kb3dzIiwiMTUuMC4wIiwieDg2IiwiIiwiMTE4LjAuNTk5My4xMjAiLG51bGwsMCxudWxsLCI2NCIsW1siQ2hyb21pdW0iLCIxMTguMC41OTkzLjEyMCJdLFsiR29vZ2xlIENocm9tZSIsIjExOC4wLjU5OTMuMTIwIl0sWyJOb3Q9QT9CcmFuZCIsIjk5LjAuMC4wIl1dLDBd&dt=1699282768319&bpp=1&bdt=143&idt=115&shv=r20231101&mjsv=m202311010101&ptt=9&saldr=aa&abxe=1&prev_fmts=0x0%2C922x280&nras=1&correlator=6044177851345&frm=20&pv=1&ga_vid=1624410613.1699282327&ga_sid=1699282768&ga_hid=221203546&ga_fc=1&rplot=4&u_tz=480&u_his=4&u_h=720&u_w=1280&u_ah=672&u_aw=1280&u_cd=24&u_sd=1.5&dmc=8&adx=198&ady=6660&biw=1263&bih=595&scr_x=0&scr_y=4380&eid=44759875%2C44759926%2C44759837%2C31079296%2C31079347%2C31079403%2C44807048%2C44807334%2C44807455%2C44807463%2C31078297%2C31079356%2C31078663%2C31078665%2C31078668%2C31078670&oid=2&pvsid=1680152337772961&tmod=2054120635&uas=3&nvt=1&ref=https%3A%2F%2Fdanidee10.github.io%2F2018%2F01%2F03%2Frealtime-django-2.html&fc=1920&brdim=0%2C0%2C0%2C0%2C1280%2C0%2C1280%2C672%2C1280%2C595&vis=1&rsz=%7C%7CpEebr%7C&abl=CS&pfx=0&fu=128&bc=31&td=1&psd=W251bGwsbnVsbCxudWxsLDNd&nt=1&ifi=3&uci=a!3&btvi=1&fsb=1&xpc=7SE1ARKZD3&p=https%3A//danidee10.github.io&dtd=M

另一方面,对于用户名来说,这是小菜一碟,我们只需将用户名发布到服务器,它就会使用它来检索用户并将其添加到聊天室。

此外,如果您决定添加“邀请多个用户”功能,您可以修改代码以读取用户名列表并从数据库中一次性获取它们。由你决定。

使用用户名使我们的代码更加灵活并且可以改进。

现在让我们添加视图的 URL。

"""URL's for the chat app."""

from django.contrib import admin
from django.urls import path

from . import views

urlpatterns = [
    path('chats/', views.ChatSessionView.as_view()),
    path('chats/<uri>/', views.ChatSessionView.as_view()),
    path('chats/<uri>/messages/', views.ChatSessionMessageView.as_view()),
]

urls.py不要忘记在基本文件中包含 URL

from django.contrib import admin
from django.uris import path, include

uripatterns = [
    path('admin/', admin.site.uris),

    # Custom URL's     path('auth/', include('djoser.uris')),
    path('auth/', include('djoser.uris.authtoken')),
    path('api/', include('chat.uris'))
]

我们的端点已准备就绪,任何经过身份验证的用户都可以向它们发出请求

让我们尝试一下:

$ curl -X POST http://127.0.0.1:8000/auth/token/create/ --data 'username=danidee&password=mypassword'
{"auth_token":"169fcd5067cc55c500f576502637281fa367b3a6"}

$ curl -X POST http://127.0.0.1:8000/api/chats/ -H 'Authorization: Token 169fcd5067cc55c500f576502637281fa367b3a6'
{"status":"SUCCESS","uri":"040213b14a02451","message":"New chat session created"}

$ curl -X POST http://127.0.0.1:8000/auth/users/create/ --data 'username=daniel&password=mypassword'
{"email":"","username":"daniel","id":2}

$ curl -X POST http://127.0.0.1:8000/auth/token/create/ --data 'username=daniel&password=mypassword'
{"auth_token":"9c3ea2d194d7236ac68d2faefba017c8426a8484"}

$ curl -X PATCH http://127.0.0.1:8000/api/chats/040213b14a02451/ --data 'username=daniel' -H 'Authorization: Token 9c3ea2d194d7236ac68d2faefba017c8426a8484'
{"status":"SUCCESS","members":[{"id":1,"username":"danidee","email":"osaetindaniel@gmail.com","first_name":"","last_name":""},{"id":2,"username":"daniel","email":"","first_name":"","last_name":""}],"message":"daniel joined the chat","user":{"id":2,"username":"daniel","email":"","first_name":"","last_name":""}}

让我们发送一些消息

$ curl -X POST http://127.0.0.1:8000/api/chats/040213b14a02451/messages/ --data 'message=Hello!' -H 'Authorization: Token 169fcd5067cc55c500f576502637281fa367b3a6'
{"status":"SUCCESS","uri":"040213b14a02451","message":"Hello!","user":{"id":1,"username":"danidee","email":"osaetindaniel@gmail.com","first_name":"","last_name":""}}

$ curl -X POST http://127.0.0.1:8000/api/chats/040213b14a02451/messages/ --data 'message=Hey whatsup!' -H 'Authorization: Token 9c3ea2d194d7236ac68d2faefba017c8426a8484'
{"status":"SUCCESS","uri":"040213b14a02451","message":"Hey whatsup! i dey","user":{"id":2,"username":"daniel","email":"","first_name":"","last_name":""}}

让我们请求消息历史记录

$ curl http://127.0.0.1:8000/api/chats/040213b14a02451/messages/ -H 'Authorization: Token 169fcd5067cc55c500f576502637281fa367b3a6'
{"id":1,"uri":"040213b14a02451","messages":[{"user":{"id":1,"username":"danidee","email":"osaetindaniel@gmail.com","first_name":"","last_name":""},"message":"Hello!"},{"user":{"id":2,"username":"daniel","email":"","first_name":"","last_name":""},"message":"Hey whatsup!"}]}

恭喜!如果到目前为止,您已经成功构建了一个 API,该 API 允许用户通过启动聊天会话并邀请其他用户加入会话来相互通信。

在下一部分中,我们将构建聊天 UI 并从 Vue 调用这些方法。

留下评论

您的邮箱地址不会被公开。 必填项已用 * 标注