从零打造 Vue 聊天组件
啥都不会最近听说了那个 socket.io,感觉就很吊,就学习了一下什么是 socket.io要理解 socket.io,不得不谈谈WebSocket在 HTML5 之前呢,因为 [http 协议是无状态的](#http 协议是无状态的),要实现浏览器与服务器的实时通讯,如果不使用 flash、applet 等浏览器插件的话,就需要定期轮询服务器来获取信息。这造成了一定的延迟和大量的网络通讯。随着
啥都不会
最近听说了那个 socket.io,感觉就很吊,就学习了一下
什么是 socket.io
要理解 socket.io,不得不谈谈WebSocket
在 HTML5 之前呢,因为 http 协议是无状态的,要实现浏览器与服务器的实时通讯,如果不使用 flash、applet 等浏览器插件的话,就需要定期轮询服务器来获取信息。这造成了一定的延迟和大量的网络通讯。随着 HTM5 的出现,这一情况有望彻底改观,它就是 WebSocket。
什么是 http 无状态协议
http 无状态协议,可以理解为 http 是无记忆的,举个例子就是说,客户端向服务端发送了一条数据,发送完之后它就不知道刚才向服务端发送过什么了,反之,服务端向客户端发送了一条数据,发送完之后它也就不知道刚才向客户端发送过什么了。
http 协议虽然是无状态的,但是使用它的应用们,为了获得状态,就必须等通过 cookie、session 等工具来获取状态
什么是轮询
轮询呢在 JavaScript 中就是说,设个定时器,过段时间呢就自动发一次请求,来获取数据,就这样反复循环下去。
WebSocket 的工作机制
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。因为 WebSocket 连接本质上就是一个 TCP 连接,所以在数据传输的稳定性和数据传输量的大小方面,和传统轮询以技术比较,具有很大的性能优势。
为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,其中附加头信息”Upgrade: WebSocket”表明这是一个申请协议升级的 HTTP 请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的 WebSocket 连接就建立起来了,双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。
好,那么接下来就是
Codding Time
使用 vue-socket.io
自行安装
- vue-socket.io
- socket.io-client
import VueSocketIO from "vue-socket.io";
import SocketIO from "socket.io-client";
Vue.use(
new VueSocketIO({
debug: false,
connection: SocketIO(),
vuex: {
store,
actionPrefix: "SOCKET_", //为vuex设置的两个前缀
mutationPrefix: "SOCKET_",
},
})
);
SocketIO() 括号里啥也没写,表示使用当前端口
解决跨域问题
配置 vue.config.js 文件
module.exports = {
devServer: {
proxy: {
"/socket.io": {
target: "http://127.0.0.1:8000",
// 允许跨域
changeOrigin: true,
ws: true,
// logLevel: "debug",
},
"sockjs-node": {
target: "http://127.0.0.1:8000",
ws: false,
changeOrigin: true,
},
},
},
};
target - 服务端端口
changeOrigin - 是否允许跨域
ws - 是否代理 WebSocket
logLevel - 是否打印日志
服务端代码
const path = require("path");
const express = require("express");
const app = express();
const server = require("http").createServer(app);
const io = require("socket.io")(server);
// app.get("/", function (req, res) {
// res.sendFile(__dirname + "/index.html");
// });
app.use(express.static(__dirname + "/public"))
let userList = [];
io.on("connection", (socket) => {
console.log("a user connected");
updateUser();
let userId = "";
socket.on("login", (userID, callback) => {
callback();
userId = userID;
userList.push(userId);
// console.log(userList);
updateUser();
});
socket.on("talk", (data) => {
// 接收连接中的 talk 事件
console.log(data);
data.role = "your";
io.emit("output", data);
});
socket.on("disconnect", () => {
console.log("user disconnected");
userList.splice(userList.indexOf(userId), 1);
updateUser();
});
function updateUser() {
io.emit("loadUser", userList);
}
});
var port = process.env.PORT || 8000;
server.listen(port, () => {
console.log("服务启动成功啦~");
});
查看 socket 连接是否渲染成功
在 vue 文件中进行查看
sockets: {
//查看socket是否渲染成功
connect() {
console.log("连接成功");
},
disconnect(){
console.log("断开链接");
},//检测socket断开链接
reconnect(){
console.log("重新链接");
},
},
发送消息
this.$socket.emit("自定义事件名", 发送的数据..., ...)
接收消息
需要在 mounted 生命周期内进行监听
this.sockets.subscribe("接收的事件名", (data, ...) => {
})
模拟登录
在登录后获取用户头像信息
登录时选择的头像作为聊天时的头像,跳过则使用默认头像(男生)
data() {
return {
show: true, // 是否显示蒙层
userObj: {
avatar: require("@/assets/me.png")
},
}
}
// 点击男生头像
onCMe() {
this.show = false
this.userObj.avatar = require("@/assets/me.png")
},
// 点击女生头像
onCYou() {
this.show = false
this.userObj.avatar = require("@/assets/you.png")
},
// 跳过
onCOver() {
this.show = false
},
现在要实现的就是上面这种效果,“你”发送的消息默认在左侧,“我”发送的消息在右侧,也就是在“我”发送消息时设一个标志,表示这是“我”发送的消息,就不需要 subscribe 后端发送的消息了。
但是,只是这样子处理的话会发现一个问题,那就是你发送的这条消息在右侧,同样也在你的对话者的右侧,这显然是不合理的,它应该在你的对话者的左侧出现。
这就需要在后端或是前端进行处理了,我是在后端进行处理的,因为在前端处理的话会比较不安全,但是我会给出在前端进行处理的方案。
在后端进行处理
后端接收到前端发送的数据对象后,对 role(角色)属性进行修改,改为 your 即是靠左的默认样式
在接收消息后进行处理
socket.on("talk", (data) => {
// 接收连接中的 talk 事件
console.log(data);
data.role = "your";
io.emit("output", data);
});
OR 在前端进行处理
在前端进行处理的话会用到对象的深拷贝,因为我们不能直接去修改原对象中的 role,因为它现在可能为 “mine”,改了他之后就会出现靠左的错误。
在发送消息之前进行处理
// 把 obj 深拷贝到 sendObj
let sendObj = Object.assign({}, obj)
// 发送 socket 消息
this.$socket.emit("talk", sendObj)
解决消息发送重复显示问题
问题描述
上面不是已经解决了消息位置问题吗,现在已经达到了我发送的消息在右侧,其他人的都在左侧,但是我发送一条消息右边显示一次,左边显示一次,这很明显不对嘛
解决方案
我需要为每条消息都加上一个 msgID,也就是在每发送一条消息时就为消息添加一个 msgID,这个 ID 为了防止重复,用了以下写法
parseInt(Date.parse(new Date()) + Math.random() * 100)
然后在 subscribe 接收服务端发送来的消息的时候进行判断,判断服务端发送过来的消息中的 msgID 是否和我发送的这条消息为同一条消息(同一个 ID),如果是,则直接 return,如果不是,那就渲染服务端发来的消息
判断写法如下参考
this.sockets.subscribe('output', (data) => {
if(!!data && !!this.list && !!this.list[this.list.length - 1] && data.msgID === this.list[this.list.length - 1].msgID) {
return
}
// 渲染服务端发来的消息
this.list.push(data)
// 此处省略20行...
})
这样就实现了基本的聊天功能了
接下来的就是简单的实现了一个动画表情特效
Lottie 动画表情
现在我要实现以下特效
引入文件
首先引入 lottie 库及动画表情文件
import lottie from "@/assets/lottie.min.js"
import bomb from "@/assets/3145-bomb.json"
import pumpkin from "@/assets/43215-pumpkins-sticker-4.json"
import explosion from "@/assets/9990-explosion.json"
基本用法
// 对每个表情创建 lottie 播放器
const player = lottie.loadAnimation ({
container: lottieEle, // 包含lottie的DOM元素
renderer: "svg",
loop: true, // 循环播放
autoplay: true, // 自动播放
animationData: this.stickers[key].path, // 动画路径
});
发送消息时使消息面板滚动到最底部
// 滚动到最新消息
this.$nextTick(()=>{
this.$refs.panel.scrollTop = this.$refs.panel.scrollHeight
})
需要操作DOM的功能一定要等到DOM加载完毕后再进行操作,否则可能出现误差,我使用的是 this.$nextTick(),也可以使用其他异步操作来完成
播放完成后,销毁爆炸相关的动画和元素
// 播放完成后,销毁爆炸相关的动画和元素
explosionPlayer.addEventListener("complete", () => {
explosionPlayer.destroy();
explosionEle.remove();
});
这里用到了 lottie 的 complete 动画播放完成事件
获取数组的后五项
let list = ["a", "b", "c", "d", "e", "f", "g"]
list.slice(-5)
用的是 this.$nextTick(),也可以使用其他异步操作来完成
播放完成后,销毁爆炸相关的动画和元素
// 播放完成后,销毁爆炸相关的动画和元素
explosionPlayer.addEventListener("complete", () => {
explosionPlayer.destroy();
explosionEle.remove();
});
这里用到了 lottie 的 complete 动画播放完成事件
获取数组的后五项
let list = ["a", "b", "c", "d", "e", "f", "g"]
list.slice(-5)
部署上线
Let’s Chat!
感谢阅读,感谢三连支持,关注我,后面还会写很多好文章的~
更多推荐
所有评论(0)