在这部分中,我们将构建聊天 UI 并将其连接到我们API
之前构建的 UI。在本部分的最后,我们应该有一个完整的聊天应用程序,其中包含一个 URL,我们可以将其分享给我们想要聊天的朋友。
如果这让您兴奋不已,系好安全带,挂上高速档,我们就出发吧!
聊天屏幕的 UI/UX
慢一点!在您以光速离开之前,让我们先讨论一下聊天屏幕的 UI/UX。
用户界面原型
首先,用户应该单击后端的“开始聊天”按钮,这将创建一个以用户为所有者的新聊天会话,之后,他们将被重定向(我们只需更改 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
。
聊天组件如下所示:
开始聊天画面
如果您点击“开始聊天”按钮,它应该会更改 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" }
. 我们很快就会需要它。
重新加载页面,您应该会看到显示聊天屏幕
聊天画面
开始新会话
要开始新会话,我们只需发布到我们在第 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>
聊天屏幕现在应该如下所示:
显示来自阵列的消息的聊天屏幕
我们使用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.prevent
v-on:submit.prevent
是修饰符的缩写形式,.prevent
可防止发生表单的默认操作(即不会提交表单)。这是我喜欢 Vue.js 的另一个原因。它充满了简单的助手和适量的魔法。
您可以自由地调用event.preventDefault
该postMessage
方法,但这不是“类似 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
)与聊天应用程序集成,而无需真正更改我们当前的大部分代码。