代码仓库:github
聊天室 WebSocket+Vue
HTTP是不支持长连接的,WebSocket是一种通信协议,提供了在单一、长连接上进行全双工通信的方式。它被设计用于在Web浏览器和Web服务器之间实现,但也可以用于任何需要实时通信的应用程序。使用ws作为协议标识符,如果需要加密则使用wss作为协议标识符,类似于http和https的区别。
相比HTTP,WebSocket请求头多了
Upgrade: websocket
Connection: Upgrade
这一段是表明现在是用的WebSocket协议,而不是简单的HTTP。
优势:
- 全双工通信: 与传统的请求-响应通信模型(如HTTP)不同,WebSocket允许双向通信。客户端和服务器都可以独立地发送消息。
- 持久连接:WebSocket在客户端和服务器之间建立了一个持久连接,只要双方需要,连接就会保持打开状态。这消除了为每次通信重复打开和关闭连接的需要。
- 低延迟:WebSocket旨在最小化延迟并减少传统HTTP轮询所带来的开销。这使其非常适用于需要实时更新的应用程序,例如聊天应用程序、在线游戏、金融交易平台和协同编辑工具。
为建立WebSocket连接,客户端向服务器发送WebSocket握手请求,在握手成功后,连接将升级到WebSocket协议。一旦建立,客户端和服务器都可以随时相互发送消息。
这也就是最大的一个特点:服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话。
WebSocket适用于需要实时更新或连续的数据流,而HTTP更适用于获取旧数据或者只获得一次数据即可,不需要实时获取数据。
综上,WebSocket适用于需要实时的场景,例如实时聊天室、多玩家游戏、实时地图位置、在线协同编辑等场景,接下来主要运用于实时聊天室实现多人聊天室,可以做到将消息放松到群聊并实时推送给全体用户。
前端
具体框架如下图所示:
布局
页面布局主要分上下两部分,上面是导航栏,下面是登陆页面或者聊天页面。
在App.vue中进行初次布局,按照上图进行设计如下
<ContentBase>
<NavBar />
</ContentBase>
<div class="box">
<router-view/>
</div>
由于下面会根据登陆状态分LoginView.vue和HomeView.vue两种页面,所以需要设置为路由状态,根据路由选择显示指定页面。
对每一个方块可以设计个卡片包括起来,主要使用bootstrap
封装到ContentBase.vue 使用 接收传过来的children,之后只要把需要"框"起来只需要放到即可。
<template>
<div class="home">
<div class="container">
<div class="card">
<div class="card-body">
<slot></slot>
<!-- 存放父组件传过来的children -->
</div>
</div>
</div>
</div>
</template>
导航栏
首先是导航栏,根据登陆状态会有两种情况,当未登陆的时候,分为(聊天室,Home,登陆)。无论点击哪一个都自己路由到登陆页面,当登陆的时候分为(聊天室,username,退出)
当点击username或聊天室会显示登陆页面,如果点击退出会触发logout函数使用户退出并显示登陆状态。
其中logout函数简单介绍
const logout = () => {
store.commit('logout');
}
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<router-link v-if="$store.state.is_login" class="navbar-brand" :to="{name:'home'}">聊天室</router-link>
<router-link v-else class="navbar-brand" :to="{name:'login'}">聊天室</router-link>
<div class="collapse navbar-collapse" id="navbarNavDropdown">
<ul class="navbar-nav" v-if="!$store.state.is_login">
<li class="nav-item">
<router-link class="nav-link active" aria-current="page" :to="{name:'login'}">Home</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" :to="{name:'login'}" role="button" data-bs-toggle="dropdown" aria-expanded="false">
登陆
</router-link>
</li>
</ul>
<!-- 上面是未登录状态 下面是已登录状态 -->
<ul class="navbar-nav" v-else>
<li class="nav-item">
<router-link class="nav-link active" aria-current="page" :to="{name:'home'}">{{ $store.state.username }}</router-link>
</li>
<li class="nav-item">
<router-link @click="logout" class="nav-link" :to="{name:'login'}" role="button" data-bs-toggle="dropdown" aria-expanded="false">
退出
</router-link>
</li>
</ul>
</div>
</div>
</nav>
</div>
接下来就是下面主页面,分为登录页面和聊天页面,所以需要路由
在component不适用引入而是直接使用匿名函数是异步加载方式即页面没有被显示这个代码就不会被执行
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/login',
name: 'login',
component: () => import(/* webpackChunkName: "about" */ '../views/LoginView.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
登陆页面
“/login” 是登陆页面
在这里简单设计一下登录窗口即可
<ContentBase style="margin-top: 200px;;">
<form>
<div class="mb-3">
<input type="text" placeholder="请输入用户名" v-model="username" />
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="exampleCheck1">
<label class="form-check-label" for="exampleCheck1">Check me out</label>
</div>
<button type="submit" class="btn btn-primary" @click="handleEnter">Submit</button>
</form>
</ContentBase>
当submit之后会触发handleEnter函数,在script中进行声明
const handleEnter = () => {
const _username = username.value; //ref对象的值用value取得
//控制用户名格式
if (_username.length < 2) {
alert('用户名应不小于2位');
username.value = '';
return;
}
sessionStorage.setItem('username', _username); //存入localStorage
username.value = '';
store.commit('login',_username);
router.push('/');
};
用户名必须要不小于两位,确认成功之后存入到sessionStorage中并且调用store.commit上传到vuex中,这样就可以全局改变登陆状态,并且立马路由到聊天页面上。vuex的配置会在后续给出。
如果需要根据之前是否有所登录,如果有登录可以在挂载之后检查sessionStorage检查是否存在,如果存在立马跳转,比如下列代码
onMounted(()=>{
//查看sessionStorage中是否存在username
username.value = sessionStorage.getItem('username');
if(username.value){//存在的话直接跳转
router.push('/');
return ;
}
});
sessionStorage是位置
聊天页面
“/” 是聊天页面
聊天页面分成三块,左侧是用户列表UserList,右侧分上下两层分别为聊天记录框和发送窗口
在用户列表中会显示当前在线用户人数以及用户名称,对用户列表会排除掉自身,因为自身会放到首位显示出。
为了简单起见就不再分三个components,直接在HomeView.vue中设计下布局即可,使用bootstrap中的grid布局。
左侧右侧col-3 col-9即3:9分开,上下大概550px:150px。
<template>
<ContentBase>
<div class="row" style="width: 700px;">
<div class="col-3" style="width:150px; background-color: lightblue;">
<div>UserList : {{ userList.length+1 }} </div>
<hr>
<div>{{ username }}</div>
<div v-for="(item,index) in userList" :key="index"> {{ item }} </div>
</div>
<div class="col-9" style="height:500px; width: 550px; padding: 0px 0px;">
<div id="ltk">
<ul>
<li v-for="item of msgList" :key="item.id" style="list-style: none; height: 80px;">
<div>
<span style="display: block; text-align: center; font-size: small;">{{ item.dataTime }}</span>
<span v-if="item.user === username"
style="display: block; text-align: right; padding-right: 50px; padding-top: 0; font-size: small;">
{{ item.user }}
</span>
<span v-else style="text-align: left;">
{{ item.user }}
</span>
</div>
<div v-if="item.user === username"
style="display: block; text-align: right; padding-right: 50px;">
{{ item.msg }}
</div>
<div v-else style="text-align: left;">
{{ item.msg }}
</div>
</li>
</ul>
</div>
<div class="input">
<input type="text" placeholder="输入消息" v-model="msg"/>
<button @click="handleSend">发送</button>
</div>
</div>
</div>
</ContentBase>
</template>
这部分script部分比较重要
需要实现的功能主要有:
获取当前username
保证实时滚动即当消息过多的时候需要发消息会滚动到底层,使用scrollTop=scrollHeight即可(让顶层和当前高度一致)。
发送消息:需要向服务端发送,便于服务端群发到各个在线用户,为了便于显示当前时间格式设置为"时:分:秒",但要注意判断不能全是空格,利用trim()去掉空格来判断
为了便于获取数据,设置函数handleMessage来接收服务端广播的消息,以便实时获取到数据
为了便于新用户获得之前的聊天记录,可以让服务器记录历史记录,新用户初次登陆进行获取
<script>
import { reactive,toRefs,onMounted,ref } from 'vue';
import { useRouter } from 'vue-router';
import useWebSocket from '../hooks/websocket.js'
import store from '@/store';
import ContentBase from '@/components/ContentBase.vue';
import LoginView from './LoginView.vue';
export default {
name: 'HomeView',
components:{
ContentBase,
},
setup(){
const router = useRouter();
const state = reactive({
msg:'',
msgList:[],
});
let userList = ref([]);
let username = sessionStorage.getItem('username');
onMounted(() => {
if(!username || store.state.is_login===false){
router.push('/login');
return ;
}
//实现实时滚动
setInterval(()=>{
var e = document.getElementById('ltk');
e.scrollTop = e.scrollHeight;
//scrollTop:指的是滚动条卷去的距离(滚动条向下滚动之后距离顶部的距离)
//scrollHeight:指的是内容的高度
},20)
})
const ws = useWebSocket(handleMessage,username);
const handleSend = () => {
const _msg = state.msg;
if(!_msg.trim().length){
return ;//空
}
let time = new Date();
ws.send(JSON.stringify({
id: time.getTime(),
user: username,
dataTime:`${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}`,
msg:state.msg,
}));
state.msg = '';
}
function handleMessage(e){
const _msgData=JSON.parse(e.data);
if(_msgData.tag === 'userUpdate'){
for(let i = 0;i < _msgData.userList.length;i++){
var f = false;
for(let j = 0;j < userList.value.length;j++){
if(userList.value[j] === _msgData.userList[i]){
f = true;
break;
}
}
if(!f)
userList.value.push(_msgData.userList[i]);
}
}else if(_msgData.tag === 'msgUpdate'){
if(state.msgList.length === 0)
console.log(_msgData.chatList.length);
for(let i = 0;i < _msgData.chatList.length;i++){
state.msgList.push(_msgData.chatList[i]);
}
}
else
state.msgList.push(_msgData);
userList.value = userList.value.filter(item=>item!==username);
}
return {
...toRefs(state), //平铺开state
handleSend,
userList,
username,
}
}
}
</script>
vuex中存储当前用户的登陆状态和当前用户名
state: {
is_login : false,
username : "",
// count : 0,
},
getters: {
},
mutations: {
login(state,username){
state.username = username,
state.is_login = true;
},
logout(state){
state.is_login = false;
state.username = "";
}
},
WebSocket
最关键的是WebSocket的实现,WebSocket构造函数开启ws://localhost:8000端口,主要分为open、message、close、error。
当处理open的时候,可以默认发送I’m ${username} 作为问候语。
当处理message的时候,调用handleMessage让前端获取到服务端广播的消息。
当处理close的时候,打印一下表明已经退出即可。
当处理error的时候,简单打印下event。
function useWebSocket(handleMessage,username){
const ws = new WebSocket('ws://localhost:8000');
ws.addEventListener('open',(e)=>{
console.log('client open');
let time = new Date();
ws.send(JSON.stringify({
id: time.getTime(),
user: username,
dataTime:`${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}`,
msg:`Hello I'm ${username}`,
}));
},false);
ws.addEventListener('message',handleMessage);
ws.addEventListener('close',(event)=>{
console.log(`${username} closed`);
});
ws.addEventListener('error',(event)=>{
console.log('error: ',event);
})
return ws;
}
export default useWebSocket;
后端
ws后端,利用Server配置端口号,监听事件。分为open、error、connect、close。只有当connect成功的时候才会出现事件message。
主要对connect、message和close进行相应的处理。
首先定义了userNum、chatList和userList记录当前在线人数、历史群聊记录和当前用户列表。
connect:当触发该事件时,表明有了新客户端进行了连接,这时候可以增加当前在线人数、username到userList并且广播。
message:当触发该事件的时候,需要广播该消息到每个客户端并且广播chatList,因为新用户加入会发送一个消息所以必定会立马获得chatList,当然放到connect不放在message中也可以。
close:当触发该事件的时候,需要减少人数并且广播。
这么多种广播,客户端应该如何区分呢?可以在对象中增加tag属性来表明广播的是哪种类型的信息。(userUpdate、chatUpdate)
客户端在handleMessage区分广播的消息如下:
function handleMessage(e){
const _msgData=JSON.parse(e.data);
if(_msgData.tag === 'userUpdate'){
for(let i = 0;i < _msgData.userList.length;i++){
var f = false;
for(let j = 0;j < userList.value.length;j++){
if(userList.value[j] === _msgData.userList[i]){
f = true;
break;
}
}
if(!f)
userList.value.push(_msgData.userList[i]);
}
}else if(_msgData.tag === 'msgUpdate'){
if(state.msgList.length === 0)
console.log(_msgData.chatList.length);
for(let i = 0;i < _msgData.chatList.length;i++){
state.msgList.push(_msgData.chatList[i]);
}
}
else
state.msgList.push(_msgData);
userList.value = userList.value.filter(item=>item!==username);
}
var userNum = 0
var chatList = []
var userList = []
const WebSocket = require('ws');
const server = new WebSocket.Server({port:8000});
server.on('open',()=>{
console.log('server open');
})
server.on('error',()=>{
console.log('server error');
})
server.on('connection',(ws)=>{
console.log('has connected');
userNum++;
server.clients.forEach((item)=>{
if(item.readyState === WebSocket.OPEN)
item.send(JSON.stringify({tag:"userUpdate",userNum:userNum,userList:userList}));
})
ws.on('message',(msg)=>{
msg = msg.toString();
console.log(`received message ${msg}`);
var res = JSON.parse(msg);
server.clients.forEach((item)=>{
if(item.readyState === WebSocket.OPEN)
item.send(JSON.stringify({tag:"msgUpdate",chatList:chatList}));
})
chatList.push(res);
var f = false;
for(let i = 0;i < userList.length;i++)
if(userList[i] === res.user)
f = true;
if(!f){
userNum=server.clients.size;
userList.push(res.user);
server.clients.forEach((item)=>{
if(item.readyState === WebSocket.OPEN)
item.send(JSON.stringify({tag:"userUpdate",userNum:userNum,userList:userList}));
})
}
server.clients.forEach((client)=>{
if(client.readyState === WebSocket.OPEN)
client.send(msg);
})
})
})
server.on('close',()=>{
userNum--;
server.clients.forEach((item)=>{
if(item.readyState === WebSocket.OPEN)
item.send(JSON.stringify({tag:"userUpdate",userNum:userNum,userList:userList}));
})
console.log('disconnected');
})
设置下执行语句在package.json
"scripts": {
"dev": "nodemon index.js"
},
执行npm run dev 等效于 nodemon index.js 开启服务器。
运行结果
未登陆状态
导航栏
登陆页面
已登陆状态
导航栏
聊天界面(实时群聊)
异步调用
在JS中,同步任务在主线程中,会优先执行,碰到异步任务就会放入到异步队列中,而异步队列又分为两个队列,分别是宏队列和微队列。
JS执行时首先执行所有的初始化同步任务的代码,之后就会区分出宏队列和微队列执行。每执行一个宏任务之前都会执行一个微任务。
宏队列主要存储宏任务,包括定时器setTimeout、ajax回调等
微队列主要存储微任务,包括Promise回调等
AJAX
作用:可以在不刷新的情况下向服务端发送http请求并得到响应
最初使用XML格式的字符串,现在使用更加简洁的JSON格式,设置xhr.responseType='json’即可实现。
Ajax操作主要分为四步:
创建对象、设置请求类型和url、发送请求、绑定事件
当状态为4的时候表明服务器完成请求响应,之后可以根据xhr.status判断响应状态
//1. 创建对象
const xhr = new XMLHttpRequest();
//2. 设置类型和url
xhr.open('POST','http://127.0.0.1:8000/server');
//3. 发送
xhr.send();
//4. 绑定事件
xhr.onreadystatechange = ()=>{
if(xhr.readyState === 4){
// 判断响应状态是否成功
if(xhr.status >= 200 && xhr.status < 300){
//对xhr.response进行相关操作;
//xhr.response是响应体
}
}
}
原生的AJAX一般很少直接用到,更多的是用jQuery、fetch或axios间接运用AJAX。
Promise
Promise是ES6规范新技术、是实现异步编程的新解决方案。简单来说Promise就是一个容器,里面保存着某个未来才会结束的结果,等结果出来之后才执行相应的回调函数从而实现异步操作。
Promise支持链式调用,可以解决回调地域问题,但是一旦建立就会立即执行,无法发中途取消。
Promise主要有三种状态,分别是pending、fulfilled、rejected。初始为pending操作,执行异步操作会执行resolve()或reject()从而变成fulfilled状态或rejected状态,之后就会执行then()参数的成功或失败的回调函数。
对于Promise的状态,只会发生pending->fulfilled或pending->rejected,即只会改变一次,但是可以指定多个回调
let p = new Promise((resolve,reject) => {
resolve('ok'); //pending->fulfilled
})
p.then(value=>{
console.log(value);
})
p.then(value=>{
alert(value);
})
then()方法本身也是返回Promise对象,可以使用const result = p.then(…)
既然返回Promise对象,那么返回的对象状态是如何决定的?
抛出异常——失败状态
非Promise类型——成功状态且result和return结果保持一致
Promise对象——状态和结果都和该Promise对象一致
Promise还会有异常穿透,即进行then()链式调用时,可以在最后指定失败回调。
p.then(value=>{
console.log(111);
}).then(value=>{
console.log(222);
}).then(value=>{
throw '!';
console.log(333);
}).catch(reason=>{
console.warn(reason);
})
如何中断一个Promise链呢?
在Promise链中,对于fulfilled状态或者rejected状态,都会传递到后面,所以需要一个pending状态的Promise对象才可以完成!
即
return new Promise(()=>{})
async/await
是实现异步调用的一个手段,await必须放到async里面,await可以简单理解为async wait,后面一般修饰promise对象,等待pending状态被改变。
异步读取三个文件
async function main(){
try{
let data1 = await mineReadFile('./files/1.html');
let data2 = await mineReadFile('./files/2.html');
let data3 = await mineReadFile('./files/3.html');
console.log(data1+data2+data3);
}catch(e){
console.log(e);
}
}
如何使用async和await来实现发送AJAX呢?
先将AJAX用Promise封装起来得到sendAJAX函数
之后定义
async function(){
let data = await sendAJAX(url);
console.log(data);
}
将该函数绑定到某个触发事件中即可
axios
本质是基于promise的Ajax流行库,使用了promise并且底层发送Ajax。
请求流程
当使用axios返送请求时,会调用request(config),内部执行dispatchRequest(config),dispatchRequest()会调用适配器xhrAdapter,执行回调后并返回promise对象。
当数据完成响应后,会原路返回,最后根据响应结果axios.then()执行相应的回调函数。
request(config)会将请求拦截器、响应拦截器、dispatchRequest()进行promise串联,这样可以完成拦截的功能。
在内部主要存于chain数组中,只有经过请求拦截器才会执行dispatchRequest(),响应结束后会执行响应拦截器,都返回成功才是真的成功。
dispatchRequest()会转换请求数据和响应数据,并调用xhrAdapter()发送请求。
xhrAdapter()会创建XHR对象,发送Ajax请求。文章来源:https://www.toymoban.com/news/detail-797200.html
取消流程
当需要取消请求的时候,我们可以通过执行某个函数从而改变promise的状态,当promise状态从pending->fulfilled立马执行then的成功回调,而这个成功回调会执行xhr.abort(),所以可以暴露出来一个改变该promise对象状态的函数即可完成取消请求。文章来源地址https://www.toymoban.com/news/detail-797200.html
//取消请求
if(config.cancelToken){
//对cancelToken 对象身上的 promise对象指定成功的回调
config.cancelToken.promise.then(value=>{
xhr.abort();
})
//一直是pending状态 一旦改变成fulfilled状态立马执行回调
}
到了这里,关于WebSocket+Vue实现简易多人聊天室 以及 对异步调用的理解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!