在现代 web 开发中,实时聊天功能是非常常见的应用场景之一。它广泛应用于社交平台、客服系统、在线协作工具等。实现这种功能的核心技术之一是 WebSocket,它允许在客户端和服务器之间建立持久的、全双工的连接,支持实时数据的双向传输。
在这篇文章中,我们将介绍如何使用 WebSocket 实现一个简单的实时聊天功能。我们将从基础的 WebSocket 连接开始,逐步完善聊天功能,包括消息的发送与接收、用户连接管理以及一些进阶功能(例如自动滚动、显示消息时间等)。
一、WebSocket 简介
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它最初由 HTML5 提出,并且得到了各大浏览器的广泛支持。
与传统的 HTTP 协议不同,WebSocket 在建立连接后不会再进行请求和响应,它允许客户端和服务器之间随时发送和接收数据。这样可以避免频繁的请求和响应,从而提高实时性的体验。
二、如何使用 WebSocket 实现聊天功能
front
bash
npm i socket.io-client
js
"dependencies": {
"ant-design-vue": "^1.7.8",
"socket.io-client": "^4.8.1",
"vue": "2.7.16",
"vue-router": "3.0.1",
"vue-template-compiler": "^2.7.16"
}
js
//main.js
import io from 'socket.io-client'
const socket = io('http://localhost:3000') // 连接服务器域名
// 连接websocket服务器
Vue.prototype.$socket = socket;
html
<template>
<div class="chat-container">
<h1>基于websocket实时聊天</h1>
<div class="container">
<div class="chat-inner" ref='scrollBottom'>
<transition-group tag="div">
<div :class="'messages' + ` ${item.role === 'client' ? 'user' : 'service'}`"
v-for="(item, index) in messageList"
:key="index"
>
<span v-if="item.role === 'service'" class="typing">{{ item.content }}</span>
<span v-else>{{ item.content }}</span>
</div>
</transition-group>
</div>
<div class="chat-win">
<a-input-search
placeholder="请输入内容"
v-model="userInput"
size="large"
@search="onSend">
<a-button
:loading="loading"
type="primary"
slot="enterButton">
发送
</a-button>
</a-input-search>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'chatRoom',
data () {
return {
userInput: '',
loading: false,
messageList: [
{role: 'client', content: 'hello'},
{role: 'service', content: 'hello, user'}
],
typingMessage: null // 打字机效果
}
},
methods: {
onSend(){
if (!this.userInput) return false;
this.loading = true;
const msg = {
role: 'client',
content: this.userInput,
};
this.$socket.emit('chat message', msg); // 发送消息到服务端
this.messageList.push(msg); // 显示客户端消息
this.userInput = ''; // 清空输入框
this.saveMessages(); // 保存消息
this.scrollBottom(); // 滚动到最新消息
this.loading = false;
},
async typeBotMessage(content) {
this.typingMessage = { content: "", role: "service" };
this.messageList.push(this.typingMessage); // 在聊天框中添加服务端消息
await new Promise((resolve) => {
const chunkSize = 1; // 每次插入的字符数
let i = 0;
const renderChunk = () => {
this.typingMessage.content += content.slice(i, i + chunkSize); // 模拟打字机效果
this.saveMessages(); // 保存消息
this.scrollBottom(); // 滚动到最新消息
i += chunkSize;
if (i < content.length) {
setTimeout(renderChunk, 50); // 控制打字速度
} else {
this.typingMessage = null; // 清除打字消息
resolve();
}
};
renderChunk();
});
},
saveMessages() {
localStorage.setItem('chatMessages', JSON.stringify(this.messageList));
},
scrollBottom() {
this.$nextTick(() => {
const container = this.$refs.scrollBottom;
if (container) {
container.scrollTop = container.scrollHeight;
}
});
}
},
mounted() {
this.$socket.on('connect', () => {
console.log('connected to the server');
});
this.$socket.on('response', async (msg) => {
console.log('Server response:', msg);
await this.typeBotMessage(msg); // 模拟服务端的打字效果
this.scrollBottom(); // 滚动到最新消息
});
}
};
</script>
<style lang="less" scoped>
/* 进入动画 */
.v-enter-active, .v-leave-active {
transition: all 0.5s ease;
}
.v-enter {
opacity: 0;
transform: translateY(20px); /* 初始状态和离开时稍微向下偏移 */
}
.v-leave-to {
opacity: 1;
transform: translateY(0);
}
.chat-container {
text-align: center;
height: 440px;
.container {
position: relative;
display: flex;
justify-content: center;
width: 100%;
height: 100%;
.chat-inner {
position: absolute;
left: 50%;
transform: translateX(-50%);
border-radius: 2%;
border: 1px solid #ccc;
width: 60%;
height: 400px;
overflow-y: auto;
background-color: rgba(112, 112, 112, .1);
.messages {
margin: 10px auto;
width: 95%;
line-height: 35px;
background-color: white;
}
.user {
text-align: right;
padding-right: 10px;
}
.service {
background-color: aquamarine;
text-align: left;
padding-left: 10px;
}
}
.chat-win {
width: 60%;
position: absolute;
bottom: 0;
}
}
}
</style>
service
js
"dependencies"{
"cors": "^2.8.5",
"express": "^4.21.1",
"socket.io": "^4.8.1"
}
js
const express = require('express')
const cors = require('cors')
const socketIo = require('socket.io')
const http = require('http')
const app = express();
app.use(cors())
const server = http.createServer(app)
const io = socketIo(server, {
cors: {
allowOrigin: '*'
}
})
io.on('connection', (socket) =>{
console.log('user connected')
socket.on('chat message', (msg) =>{
console.log('msg=',msg)
io.emit('response', '我已经收到,我是服务端')
})
socket.on('disconnect', () =>{
console.log('user disconnected')
})
})
server.listen(3000, () => {
console.log(`服务启动在${3000}端口`)
})