实时 Django 第 4 部分:使用 django、RabbitMQ 和 Vue.js 构建聊天应用程序(将 Vue 前端插入 django API)

在这部分中,我们将构建聊天 UI 并将其连接到我们API之前构建的 UI。在本部分的最后,我们应该有一个完整的聊天应用程序,其中包含一个 URL,我们可以将其分享给我们想要聊天的朋友。

如果这让您兴奋不已,系好安全带,挂上高速档,我们就出发吧!

聊天屏幕的 UI/UX

慢一点!在您以光速离开之前,让我们先讨论一下聊天屏幕的 UI/UX。

实时 Django 4.1

用户界面原型

首先,用户应该单击后端的“开始聊天”按钮,这将创建一个以用户为所有者的新聊天会话,之后,他们将被重定向(我们只需更改 URL 和显示聊天界面)到聊天界面,他们可以在其中与其他用户聊天,并通过与其他用户共享聊天链接来邀请其他人。

在“开始聊天”和“加入聊天”屏幕周围绘制了蓝色波浪线,以表明它们将由一个 Vue 组件处理。此外,“加入聊天”实际上并不是一个单独的屏幕。这是一种行为,一旦打开有效聊天会话的 URL,他们就会自动看到一个聊天窗口,其中显示了之前的消息,以便他们可以跟上。

执行

我已经Chat.vue使用引导程序在组件中设计了聊天界面。

<template>
  <div class="container">
    <div class="row">
      <div class="col-sm-6 offset-3">

        <div v-if="sessionStarted" id="chat-container" class="card">
          <div class="card-header text-white text-center font-weight-bold subtle-blue-gradient">
            Share the page URL to invite new friends
          </div>

          <div class="card-body">
            <div class="container chat-body">
              <div class="row chat-section">
                <div class="col-sm-2">
                  <img class="rounded-circle" src="http://placehold.it/40/f16000/fff&text=D" />
                </div>
                <div class="col-sm-7">
                  <span class="card-text speech-bubble speech-bubble-peer">Hello!</span>
                </div>
              </div>
              <div class="row chat-section">
                <div class="col-sm-7 offset-3">
                  <span class="card-text speech-bubble speech-bubble-user float-right text-white subtle-blue-gradient">
                    Whatsup, another chat app?
                  </span>
                </div>
                <div class="col-sm-2">
                  <img class="rounded-circle" src="http://placehold.it/40/333333/fff&text=A" />
                </div>
              </div>
              <div class="row chat-section">
                <div class="col-sm-2">
                  <img class="rounded-circle" src="http://placehold.it/40/f16000/fff&text=D" />
                </div>
                <div class="col-sm-7">
                  <p class="card-text speech-bubble speech-bubble-peer">
                    Yes this is Chatire, it's pretty cool and it's Open source
                    and it was built with Django and Vue JS so we can tweak it to our satisfaction.
                  </p>
                </div>
              </div>
              <div class="row chat-section">
                <div class="col-sm-7 offset-3">
                  <p class="card-text speech-bubble speech-bubble-user float-right text-white subtle-blue-gradient">
                    Okay i'm already hacking around let me see what i can do to this thing.
                  </p>
                </div>
                <div class="col-sm-2">
                  <img class="rounded-circle" src="http://placehold.it/40/333333/fff&text=A" />
                </div>
              </div>
              <div class="row chat-section">
                <div class="col-sm-7 offset-3">
                  <p class="card-text speech-bubble speech-bubble-user float-right text-white subtle-blue-gradient">
                    We should invite james to see this.
                  </p>
                </div>
                <div class="col-sm-2">
                  <img class="rounded-circle" src="http://placehold.it/40/333333/fff&text=A" />
                </div>
              </div>
            </div>
          </div>

          <div class="card-footer text-muted">
            <form>
              <div class="row">
                <div class="col-sm-10">
                  <input type="text" placeholder="Type a message" />
                </div>
                <div class="col-sm-2">
                  <button class="btn btn-primary">Send</button>
                </div>
              </div>
            </form>
          </div>
        </div>

        <div v-else>
          <h3 class="text-center">Welcome !</h3>

          <br />

          <p class="text-center">
            To start chatting with friends click on the button below, it'll start a new chat session
            and then you can invite your friends over to chat!
          </p>

          <br />

          <button @click="startChatSession" class="btn btn-primary btn-lg btn-block">Start Chatting</button>
        </div>

      </div>
    </div>
  </div>
</template>

<script>
const $ = window.jQuery

export default {
  data () {
    return {
      sessionStarted: false
    }
  },

  created () {
    this.username = sessionStorage.getItem('username')
  },

  methods: {
    startChatSession () {
      this.sessionStarted = true
      this.$router.push('/chats/chat_url/')
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1,
h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}

.btn {
  border-radius: 0 !important;
}

.card-footer input[type="text"] {
  background-color: #ffffff;
  color: #444444;
  padding: 7px;
  font-size: 13px;
  border: 2px solid #cccccc;
  width: 100%;
  height: 38px;
}

.card-header a {
  text-decoration: underline;
}

.card-body {
  background-color: #ddd;
}

.chat-body {
  margin-top: -15px;
  margin-bottom: -5px;
  height: 380px;
  overflow-y: auto;
}

.speech-bubble {
  display: inline-block;
  position: relative;
  border-radius: 0.4em;
  padding: 10px;
  background-color: #fff;
  font-size: 14px;
}

.subtle-blue-gradient {
  background: linear-gradient(45deg,#004bff, #007bff);
}

.speech-bubble-user:after {
  content: "";
  position: absolute;
  right: 4px;
  top: 10px;
  width: 0;
  height: 0;
  border: 20px solid transparent;
  border-left-color: #007bff;
  border-right: 0;
  border-top: 0;
  margin-top: -10px;
  margin-right: -20px;
}

.speech-bubble-peer:after {
  content: "";
  position: absolute;
  left: 3px;
  top: 10px;
  width: 0;
  height: 0;
  border: 20px solid transparent;
  border-right-color: #ffffff;
  border-top: 0;
  border-left: 0;
  margin-top: -10px;
  margin-left: -20px;
}

.chat-section:first-child {
  margin-top: 10px;
}

.chat-section {
  margin-top: 15px;
}

.send-section {
  margin-bottom: -20px;
  padding-bottom: 10px;
}
</style>

请注意,这@click是一个缩写形式v-on:click

由于 HTML/CSS 和虚拟聊天,它相当庞大(超过 200 行代码)。遗憾的是,本教程不会涉及太多有关设计的内容,因此我们唯一感兴趣的是该组件中的 JavaScript。但请注意虚拟聊天的标记,因为它对我们区分用户的消息和其他消息很有用

我们创建了一个名sessionStarted为此属性的属性,允许我们确定聊天会话是否处于活动状态。如果聊天会话处于活动状态,我们将呈现聊天框,否则我们将显示“开始聊天”视图。

created钩子中,我们从 中检索用户名sessionStorage并将其存储为组件的属性。

您可能会问自己为什么我们不将其作为组件的一部分data。我们没有这样做,因为用户名属性不是反应性的。我们不需要 UI 对其值的变化做出反应/响应。

就我们而言,一旦用户登录,用户名就永远不会改变(如果改变了就会很奇怪)。

您应该只存储 Component 中的反应性属性data。Vue 不会查看函数外部添加的任何属性data

聊天组件如下所示:

实时 Django 4.2

开始聊天画面

如果您点击“开始聊天”按钮,它应该会更改 URL 并显示一个空白页面。由于没有路由与 url 匹配,因此显示空白页面/chats/chat_url。值得庆幸的是,Vue 路由器允许我们动态匹配和捕获 URL 中的参数。

返回路由器的index.js文件并将Chat路由更改为:

{
    path: '/chats/:uri?',
    name: 'Chat',
    component: Chat
},

最后的问号告诉 vue 路由器该uri参数是可选的,因此它会匹配裸露的/chats/chats/chat_url偶数/chats/abazaba。斜杠之后的任何内容都将被匹配。

我们还可以uri通过访问来获取组件中的:

this.$route.params它返回一个对象:Object { uri: "chat_url" }. 我们很快就会需要它。

重新加载页面,您应该会看到显示聊天屏幕

实时 Django 4.2

聊天画面

开始新会话

要开始新会话,我们只需发布到我们在第 3 部分中创建的 API 端点

created () {
  this.username = sessionStorage.getItem('username')

  // Setup headers for all requests
  $.ajaxSetup({
    beforeSend: function(xhr) {
      xhr.setRequestHeader('Authorization', `JWT ${sessionStorage.getItem('authToken')}`)
    }
  })
},

methods: {
  startChatSession () {
      $.post('http://localhost:8000/api/chats/', (data) => {
        alert("A new session has been created you'll be redirected automatically")
        this.sessionStarted = true
        this.$router.push(`/chats/${data.uri}/`)
      })

      .fail((response) => {
        alert(response.responseText)
      })
    }
}

created挂钩中,我们为所有 Ajax 请求设置授权标头。如果没有这个,请求就会失败,因为我们将尝试以未经身份验证的用户身份发布。

发送消息

那么我们如何发送消息呢?

你说对了。通过发布消息端点。在此之前,让我们删除虚拟消息并将消息data作为数组存储在组件中。

这是Chat组件(没有 CSS)

<template>
  <div class="container">
    <div class="row">
      <div class="col-sm-6 offset-3">

        <div v-if="sessionStarted" id="chat-container" class="card">
          <div class="card-header text-white text-center font-weight-bold subtle-blue-gradient">
            Share the page URL to invite new friends
          </div>

          <div class="card-body">
            <div class="container chat-body">
              <div v-for="message in messages" :key="message.id" class="row chat-section">
                <template v-if="username === message.user.username">
                  <div class="col-sm-7 offset-3">
                    <span class="card-text speech-bubble speech-bubble-user float-right text-white subtle-blue-gradient">
                      {{ message.message }}
                    </span>
                  </div>
                  <div class="col-sm-2">
                    <img class="rounded-circle" :src="`http://placehold.it/40/007bff/fff&text=${message.user.username[0].toUpperCase()}`" />
                  </div>
                </template>
                <template v-else>
                  <div class="col-sm-2">
                    <img class="rounded-circle" :src="`http://placehold.it/40/333333/fff&text=${message.user.username[0].toUpperCase()}`" />
                  </div>
                  <div class="col-sm-7">
                    <span class="card-text speech-bubble speech-bubble-peer">
                      {{ message.message }}
                    </span>
                  </div>
                </template>
              </div>
            </div>
          </div>

          <div class="card-footer text-muted">
            <form>
              <div class="row">
                <div class="col-sm-10">
                  <input type="text" placeholder="Type a message" />
                </div>
                <div class="col-sm-2">
                  <button class="btn btn-primary">Send</button>
                </div>
              </div>
            </form>
          </div>
        </div>

        <div v-else>
          <h3 class="text-center">Welcome !</h3>
          <br />
          <p class="text-center">
            To start chatting with friends click on the button below, it'll start a new chat session
            and then you can invite your friends over to chat!
          </p>
          <br />
          <button @click="startChatSession" class="btn btn-primary btn-lg btn-block">Start Chatting</button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>

const $ = window.jQuery

export default {
  data () {
    return {
      sessionStarted: false,
      messages: [
        {"status":"SUCCESS","uri":"040213b14a02451","message":"Hello!","user":{"id":1,"username":"danidee","email":"osaetindaniel@gmail.com","first_name":"","last_name":""}},
        {"status":"SUCCESS","uri":"040213b14a02451","message":"Hey whatsup! i dey","user":{"id":2,"username":"daniel","email":"","first_name":"","last_name":""}}
      ]
    }
  },

  created () {
    this.username = sessionStorage.getItem('username')

    // Setup headers for all requests
    $.ajaxSetup({
      beforeSend: function(xhr) {
        xhr.setRequestHeader('Authorization', `JWT ${sessionStorage.getItem('authToken')}`)
      }
    })
  },

  methods: {
    startChatSession () {
      $.post('http://localhost:8000/api/chats/', (data) => {
        alert("A new session has been created you'll be redirected automatically")
        this.sessionStarted = true
        this.$router.push(`/chats/${data.uri}/`)
      })
      .fail((response) => {
        alert(response.responseText)
      })
    }
  }
}
</script>

聊天屏幕现在应该如下所示:

实时 Django 4.4

显示来自阵列的消息的聊天屏幕

我们使用v-if指令将消息发送者与当前登录的用户进行比较。根据结果​​,我们可以确定消息应如何显示。

用户发送的消息以蓝色背景右对齐,而其他用户发送的消息以白色背景左对齐。

通过我们所做的一切,我们应该如何处理消息已经非常明显了。当我们发布新消息时,我们只需将其添加到消息列表中,Vue 就会照顾好 UI!

<script>

const $ = window.jQuery

export default {
  data () {
    return {
      sessionStarted: false, messages: [], message: ''
    }
  },

  created () {
    ...
  },

  methods: {
    ...

    postMessage (event) {
      const data = {message: this.message}

      $.post(`http://localhost:8000/api/chats/${this.$route.params.uri}/messages/`, data, (data) => {
        this.messages.push(data)
        this.message = '' // clear the message after sending
      })
      .fail((response) => {
        alert(response.responseText)
      })
    }
  }
}
</script>

我们已经向数据对象添加了另一个属性message,我们将使用它来跟踪输入字段中输入的文本。

让我们在模板中告诉 Vue:

<form @submit.prevent="postMessage">
  <div class="row">
    <div class="col-sm-10">
      <input v-model="message" type="text" placeholder="Type a message" />
    </div>
    <div class="col-sm-2">
      <button class="btn btn-primary">Send</button>
    </div>
  </div>
</form>

@submit.preventv-on:submit.prevent是修饰符的缩写形式,.prevent可防止发生表单的默认操作(即不会提交表单)。这是我喜欢 Vue.js 的另一个原因。它充满了简单的助手和适量的魔法。

您可以自由地调用event.preventDefaultpostMessage方法,但这不是“类似 Vue”。

如果一切顺利,我们应该能够发送消息并让它们显示在聊天 UI 中,太棒了!

加入会话

我们终于可以发送消息了,但是聊天会很无聊,因为我们只是在自言自语。我们如何邀请朋友加入我们?

我们还有另一个问题,在浏览器中点击刷新即可!我们被重定向回“开始聊天”页面。聊天会话的所有者及其朋友都无法加入或恢复聊天会话。

为了解决这个问题,我们需要发送一个PATCH请求/api/chats/,如果我们可以在服务器返回的结果中找到该用户,则意味着他们已成功添加到聊天会话中(或者他们已经是会员)。然后我们可以获取聊天记录并将其显示给他们。

<script>
const $ = window.jQuery

export default {
  data () {
    return {
      sessionStarted: false, messages: [], message: ''
    }
  },

  created () {
    this.username = sessionStorage.getItem('username')

    // Setup headers for all requests
    $.ajaxSetup({
      beforeSend: function(xhr) {
        xhr.setRequestHeader('Authorization', `JWT ${sessionStorage.getItem('authToken')}`)
      }
    })

    if (this.$route.params.uri) {
      this.joinChatSession()
    }
  },

  methods: {
    startChatSession () {
      ...
    },

    postMessage (event) {
      ...
    },

    joinChatSession () {
      const uri = this.$route.params.uri

      $.ajax({
        url: `http://localhost:8000/api/chats/${uri}/`,
        data: {username: this.username},
        type: 'PATCH',
        success: (data) => {
          const user = data.members.find((member) => member.username === this.username)

          if (user) {
            // The user belongs/has joined the session
            this.sessionStarted = true
            this.fetchChatSessionHistory()
          }
        }
      })
    },

    fetchChatSessionHistory () {
      $.get(`http://127.0.0.1:8000/api/chats/${this.$route.params.uri}/messages/`, (data) => {
        this.messages = data.messages
      })
    }
  }
}
</script>

现在刷新浏览器,您应该能够恢复聊天并查看聊天记录。

另请打开另一个选项卡,登录并导航到聊天 URL。如果一切顺利,您应该会将聊天记录转发给您。这意味着其他用户可以加入聊天会话。

实时消息传递

现在我们的聊天应用程序很糟糕,因为用户必须手动点击刷新按钮来检查新消息。理想情况下,我们希望这个过程是自动的。

解决方案已经触手可及

  • 您有一个从服务器获取所有消息的方法。
  • 你有这个setInterval功能。
  • 你有 JavaScript。
你得到了这个
created () {
  this.username = sessionStorage.getItem('username')

  // Setup headers for all requests
  $.ajaxSetup({
    beforeSend: function(xhr) {
      xhr.setRequestHeader('Authorization', `JWT ${sessionStorage.getItem('authToken')}`)
    }
  })

  if (this.$route.params.uri) {
    this.joinChatSession()
  }

  setInterval(this.fetchChatSessionHistory, 3000)
},

嗯,这非常简单,我们只需要在钩子中添加一行即可created

setInterval(this.fetchChatSessionHistory, 3000)

它每 3 秒检索一次聊天历史记录,给最终用户带来实时消息传递的错觉。

您刚刚实施了polling. 对于小型应用程序来说这很好。但如果您的应用程序拥有庞大的用户群,则轮询的效率可能会非常低。你就会明白为什么。

https://googleads.g.doubleclick.net/pagead/ads?client=ca-pub-8618431079416074&output=html&h=200&slotname=8239403128&adk=2243974991&adf=3419487806&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%2F10%2Frealtime-django-4.html&wgl=1&uach=WyJXaW5kb3dzIiwiMTUuMC4wIiwieDg2IiwiIiwiMTE4LjAuNTk5My4xMjAiLG51bGwsMCxudWxsLCI2NCIsW1siQ2hyb21pdW0iLCIxMTguMC41OTkzLjEyMCJdLFsiR29vZ2xlIENocm9tZSIsIjExOC4wLjU5OTMuMTIwIl0sWyJOb3Q9QT9CcmFuZCIsIjk5LjAuMC4wIl1dLDBd&dt=1699282933021&bpp=1&bdt=908&idt=484&shv=r20231101&mjsv=m202311010101&ptt=9&saldr=aa&abxe=1&prev_fmts=0x0%2C922x280&nras=1&correlator=5084094267623&frm=20&pv=1&ga_vid=1624410613.1699282327&ga_sid=1699282933&ga_hid=568988903&ga_fc=1&rplot=4&u_tz=480&u_his=5&u_h=720&u_w=1280&u_ah=672&u_aw=1280&u_cd=24&u_sd=1.5&dmc=8&adx=198&ady=17988&biw=1263&bih=595&scr_x=0&scr_y=15634&eid=44759875%2C44759926%2C44759837%2C44807048%2C44807337%2C44807455%2C44807464%2C31078297%2C31079356%2C44807749%2C44806139%2C31078663%2C31078665%2C31078668%2C31078670&oid=2&pvsid=4039615989419796&tmod=2054120635&uas=1&nvt=1&ref=https%3A%2F%2Fdanidee10.github.io%2F2018%2F01%2F07%2Frealtime-django-3.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=ngKfHxJrnG&p=https%3A//danidee10.github.io&dtd=54009

让我们做一些数学题:

对于一个会话中的两个用户(假设他们同时登录)。3 秒内,他们将提出 2 个请求。一分钟内,他们将提出 40 个请求。一小时内就有 2400 个请求。仅适用于 2 个用户!对于 100 个活跃用户一小时,我们将收到 240,000 个请求!

一个像样的服务器应该能够轻松处理每小时 240k 请求,但这里的主要问题是服务器必须执行不必要的轮询和不必要的工作。(请记住每个请求也会触发数据库SELECT)。

从长远来看,这很容易损害我们的服务器,最糟糕的是,即使用户空闲,他们的浏览器也会继续发出请求,无论是否有新消息。我们可以通过跟踪他们上次输入的时间来监控他们何时空闲,然后调用clearInterval停止轮询 url,但即使这样,我们仍然会有不必要的请求,因为我们无法预测用户空闲的确切时刻。他们可以在等待其他用户回复时停止打字,但这并不意味着他们对接收新消息不感兴趣。

此外,就带宽而言,每个请求都会浪费带宽,因为它们包含headers我们并不真正需要的 cookie 和身份验证信息,我们只对消息感兴趣。

必须有一种更有效的方法来处理这个问题。

这正是 WebSockets 通过在服务器和客户端之间打开持久的双向连接来解决的问题,这意味着客户端永远不需要向服务器询问新信息。当它可用时,服务器只需将其推送到客户端即可。

此外,如果客户端需要向服务器发送信息,它可以使用相同的连接。

WebSocket 比轮询更高效,在下一部分中,我将向您展示如何将其(使用uWSGI)与聊天应用程序集成,而无需真正更改我们当前的大部分代码。

留下评论

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