导语:在一些社交软件中,经常可以看到各种聊天室的界面,接下来就总结一下聊天室的原理个实现方法,最后做一个简易的聊天室,包括登录/登出、加入/离开房间、发送接收聊天消息等功能。
目录
- 准备工作
- 原理分析
- 组件实现
- 实战演练
- 服务端搭建
- 案例展示
准备工作
- 在
pages/index
文件夹下面新建一个名叫chat
的组件; - 按照前一篇所说的页面结构,编写好预定的聊天页面;
原理分析
前端部分
此聊天室前端方面使用了 uniapp 提供的几个 API 实现包括:
uni.connectSocket
:连接到 websocket 服务器;SocketTask.onOpen
:监听服务端连接打开;SocketTask.onClose
:监听服务端连接关闭;SocketTask.onError
:监听服务端连接错误;SocketTask.onMessage
:监听服务端的消息;SocketTask.send
:向服务端发送消息;SocketTask.close
:关闭服务端连接;
后端部分
此聊天室服务端使用 npm 库ws
搭建,另外头像上传部分使用原生node
实现,待会儿会详细介绍实现方法。
组件实现
准备工作和原理分析完成后,接下来写一个简单的页面,下面主要是展示主要的内容。
模板部分
- 登录部分
包括输入用户名和上传头像的页面和功能。
html
<view class="chat-login" v-if="!wsInfo.isLogin">
<view class="chat-login-item">
<input
v-model="userInfo.name"
class="chat-login-ipt"
type="text"
:maxlength="10"
placeholder="请输入用户名" />
</view>
<view class="chat-login-item">
<button class="chat-login-ipt" @click="uploadAvatar">上传头像</button>
</view>
<view class="chat-login-item">
<button class="chat-login-btn" type="primary" @click="wsLogin">用户登录</button>
</view>
</view>
- 加入房间部分
包括选择房间的退出登录的页面和功能。
html
<view class="chat-login" v-if="wsInfo.isLogin && !wsInfo.isJoin">
<view class="chat-login-item">
<picker mode="selector" :range="roomInfo.list" :value="roomInfo.id" @change="changeRoom">
请选择房间号:{{roomInfo.name}}
</picker>
</view>
<view class="chat-login-item">
<button class="chat-login-btn" type="primary" @click="joinRoom">进入房间</button>
</view>
<view class="chat-login-item">
<button type="warn" @click="wsLogout">退出登录</button>
</view>
</view>
- 聊天室界面
包括展示房间号,退出房间,在线用户列表,聊天消息区域以及输入聊天内容和发送消息的页面和功能。
html
<view class="chat-room" v-if="wsInfo.isLogin && wsInfo.isJoin">
<view class="chat-room-set">
<text class="chat-room-name">房间{{ roomInfo.id }}</text>
<button class="chat-room-logout" size="mini" type="warn" @click="leaveRoom">退出房间</button>
</view>
<view class="chat-room-users">
在线人数({{userInfo.users.length}}人):{{ userInfo.usersText }}</view
>
<scroll-view
:scroll-y="true"
:scroll-top="roomInfo.scrollTop"
@scroll="handlerScroll"
class="chat-room-area">
<view
:class="{'chat-room-area-item': true, 'active': item.name == userInfo.name}"
v-for="(item, index) in msg.list"
:key="`msg${index+1}`">
<view class="chat-room-user">
<text
v-if="roomInfo.mode == 'name'"
:class="{'chat-room-username': true, 'active': item.name == userInfo.name}"
>{{ item.name }}</text
>
<text v-if="roomInfo.mode == 'name'" class="chat-room-time"> ({{item.createTime}}): </text>
</view>
<image
v-if="roomInfo.mode == 'avatar'"
class="chat-room-avatar"
:src="item.name == userInfo.name ? userInfo.avatar : item.avatar"></image>
<view class="chat-room-content"> {{item.content}} </view>
</view>
<view id="chat-room-area-pos"></view>
</scroll-view>
<view class="chat-room-bot">
<input
class="chat-room-bot-ipt"
type="text"
placeholder="请输入内容"
:maxlength="100"
v-model="msg.current" />
<button class="chat-room-bot-btn" size="mini" type="primary" @click="sendMsg">发送</button>
</view>
</view>
样式部分
- 登录和加入房间样式
scss
.chat-login {
.chat-login-item {
margin-bottom: 20rpx;
height: 80rpx;
.chat-login-ipt,
uni-picker {
box-sizing: border-box;
padding: 10rpx 30rpx;
height: 100%;
font-size: 26rpx;
border: 1rpx solid $e;
border-radius: 10rpx;
color: var(--UI-BG-4);
}
uni-picker {
display: flex;
align-items: center;
}
.chat-login-btn {
background: $mainColor;
}
}
}
- 聊天房间页面样式
scss
.chat-room {
display: flex;
flex-direction: column;
height: 100%;
.chat-room-set {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
font-size: 31rpx;
font-weight: bold;
.chat-room-name {
padding: 10rpx;
}
.chat-room-logout {
margin: 0;
}
}
.chat-room-users {
margin: 20rpx 0;
padding: 20rpx 0;
font-size: 28rpx;
line-height: 1.5;
border-bottom: 2rpx solid $e;
word-break: break-all;
}
.chat-room-area {
box-sizing: border-box;
padding: 20rpx;
height: calc(100% - 280rpx);
background: $f8;
.chat-room-area-item {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
margin-bottom: 20rpx;
padding-bottom: 20rpx;
.chat-room-user {
.chat-room-username {
color: $iptBorColor;
font-size: 31rpx;
line-height: 2;
&.active {
color: $mainColor;
}
}
}
.chat-room-avatar {
width: 68rpx;
height: 68rpx;
background: $white;
border-radius: 10rpx;
}
.chat-room-time {
font-size: 26rpx;
color: $iptBorColor;
}
.chat-room-content {
box-sizing: border-box;
margin-left: 12rpx;
padding: 15rpx;
max-width: calc(100% - 80rpx);
text-align: left;
font-size: 24rpx;
color: $white;
border-radius: 10rpx;
word-break: break-all;
background: $mainColor;
}
&.active {
flex-direction: row-reverse;
.chat-room-content {
margin-left: 0;
margin-right: 12rpx;
text-align: right;
color: $uni-text-color;
background: $white;
}
}
}
}
.chat-room-bot {
position: fixed;
bottom: 0;
left: 0;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
padding: 0 30rpx;
width: 100%;
height: 100rpx;
background: $white;
border-top: 3rpx solid $f8;
.chat-room-bot-ipt {
flex: 1;
box-sizing: border-box;
padding: 0 30rpx;
height: 70rpx;
font-size: 24rpx;
border: 2rpx solid $iptBorColor;
border-radius: 10rpx;
}
.chat-room-bot-btn {
margin-left: 50rpx;
width: 150rpx;
height: 70rpx;
line-height: 70rpx;
font-size: 26rpx;
color: $white;
background: $mainColor;
}
}
}
脚本部分
引入依赖包和属性设置
- websocket 信息
js
// ws
let wsInfo = reactive({
ws: null, // ws对象
alive: false, // 是否连接
isLogin: false, // 是否登录
isJoin: false, // 是否加入
lock: false, // 锁住重连
reconnectTimer: null, // 重连计时
reconnectTime: 5000, // 重连计时间隔
clientTimer: null, // 客户端计时
clientTime: 10000, // 客户端计时间隔
serverTimer: null, // 服务端计时
serverTime: 30000, // 服务端计时间隔
});
- 用户信息
js
// 用户信息
const userInfo = reactive({
id: "1", // 用户id
name: "", // 用户名称
avatar: "", // 用户头像
users: [], // 用户在线列表
usersText: "", // 用户在线列表文本
});
- 房间信息
js
// 房间信息
const roomInfo = reactive({
id: 1, // 房间id
name: "房间1", // 房间名称
list: ["房间1", "房间2", "房间3"], // 房间列表
mode: "avatar", // 模式:avatar头像
scrollTop: 0, // 滚动到顶部距离
scrollHei: 0, // 滚动高度
goBottomTimer: null, // 到底部计时
goBottomTime: 500, // 到顶部计时间隔
});
- 聊天信息
js
// 聊天信息
const msg = reactive({
current: "", // 输入框内容
list: [], // 聊天消息列表
});
实战演练
基本工作准备完毕后,接下来就开始实现功能。
连接 websocket
进入页面后,首先连接上 websocket 服务端。
- 连接 websocket
js
// 连接ws
function connectWs() {
wsInfo.ws = null;
wsInfo.ws = uni.connectSocket({
url: proxy.$apis.urls.wsUrl,
success() {
wsInfo.alive = true;
console.log("ws连接成功!");
},
fail() {
wsInfo.alive = false;
console.log("ws连接失败!");
},
});
console.log("ws信息:", wsInfo.ws);
// ws打开
wsInfo.ws.onOpen((res) => {
wsInfo.alive = true;
// 开启心跳
heartBeat();
console.log("ws开启成功!");
});
// ws消息
wsInfo.ws.onMessage((res) => {
heartBeat();
// 处理消息
let data = JSON.parse(res.data);
handlerMessage(data);
console.log("ws接收消息:", data);
});
// ws关闭
wsInfo.ws.onClose((res) => {
wsInfo.alive = false;
reConnect();
console.log("ws连接关闭:", res);
});
// ws错误
wsInfo.ws.onError((err) => {
wsInfo.alive = false;
reConnect();
console.log("ws连接错误:", res);
});
}
- 处理各种类型的消息
js
function handlerMessage(data) {
let { code } = data;
let { type } = data.data;
// 用户登录
if (type == "login") {
wsInfo.isLogin = code === 200;
uni.showToast({
title: data.data.info,
icon: code === 200 ? "success" : "error",
});
}
// 退出登录
if (code === 200 && type == "logout") {
wsInfo.isLogin = false;
userInfo.name = "";
uni.showToast({
title: "退出登录成功!",
icon: "success",
});
}
// 加入房间成功
if (code === 200 && type == "join") {
if (data.data.user.id == userInfo.id) {
wsInfo.isJoin = true;
}
if (data.data.roomId == roomInfo.id) {
let users = data.data.users,
list = [];
for (let item of users) {
list.push(item.name);
}
userInfo.users = list;
userInfo.usersText = list.join(",");
if (data.data.user.id == userInfo.id) {
msg.current = "";
msg.list = [];
}
}
}
// 加入房间失败
if (code === 101 && type == "join") {
uni.showToast({
title: data.data.info,
icon: "error",
});
}
// 离开房间
if (code === 200 && type == "leave") {
if (data.data.user.id == userInfo.id) {
wsInfo.isJoin = false;
}
if (data.data.roomId == roomInfo.id) {
let users = data.data.users,
list = [];
for (let item of users) {
list.push(item.name);
}
userInfo.users = list;
userInfo.usersText = list.join(",");
if (data.data.user.id == userInfo.id) {
msg.current = "";
msg.list = [];
}
}
}
// 聊天内容
if (code === 200 && type == "chat") {
if (data.data.roomId == roomInfo.id) {
let list = data.data.msg;
msg.list = list;
roomInfo.goBottomTimer = setTimeout(() => {
goBottom();
}, roomInfo.goBottomTime);
}
}
}
- 心跳检测
js
// 心跳检测
function heartBeat() {
clearTimeout(wsInfo.clientTimer);
clearTimeout(wsInfo.serverTimer);
wsInfo.clientTimer = setTimeout(() => {
if (wsInfo.ws) {
let pong = {
type: "ping",
};
wsInfo.ws.send({
data: JSON.stringify(pong),
fail() {
wsInfo.serverTimer = setTimeout(() => {
closeWs();
}, wsInfo.serverTime);
},
});
}
}, wsInfo.clientTime);
}
- 断线重连
js
// 断线重连
function reConnect() {
if (wsInfo.lock) return;
wsInfo.lock = true;
wsInfo.reconnectTimer = setTimeout(() => {
connectWs();
wsInfo.lock = false;
}, wsInfo.reconnectTime);
}
- 断开 websocket 连接
js
// 断开连接
function closeWs() {
if (!wsInfo.alive) {
uni.showToast({
title: "请先连接!",
icon: "error",
});
return;
}
leaveRoom();
wsLogout();
wsInfo.ws.close();
}
- 上传头像
这个就用到之前封装的文件操作方法。
js
// 上传操作
async function uploadAvatarSet(filePath) {
let opts = {
url: proxy.$apis.urls.upload,
filePath,
};
let data = await proxy.$http.upload(opts);
if (data.code == 200) {
let url = data.data.url;
userInfo.avatar = url;
} else {
uni.showToast({
title: data.data.info,
icon: "error",
});
}
}
- 用户登录
js
// ws登录
function wsLogin() {
if (!wsInfo.alive) {
uni.showToast({
title: "请先连接!",
icon: "error",
});
return;
}
if (!userInfo.name) {
uni.showToast({
title: "请输入用户名!",
icon: "error",
});
return;
}
if (!userInfo.avatar) {
uni.showToast({
title: "请上传头像!",
icon: "error",
});
return;
}
let id = proxy.$apis.utils.uuid();
userInfo.id = id;
let { name, avatar } = userInfo;
let authInfo = {
type: "login",
data: {
id,
name,
avatar,
},
};
wsInfo.ws.send({
data: JSON.stringify(authInfo),
});
}
- 用户退出
js
// ws退出
function wsLogout() {
if (!wsInfo.alive) {
uni.showToast({
title: "请先连接!",
icon: "error",
});
return;
}
let { id, name, avatar } = userInfo;
let chatInfo = {
type: "logout",
data: {
id,
name,
avatar,
},
};
chatInfo.data.roomId = roomInfo.id;
wsInfo.ws.send({
data: JSON.stringify(chatInfo),
});
}
- 加入房间
js
// ws加入房间
function joinRoom() {
if (!wsInfo.alive) {
uni.showToast({
title: "请先连接!",
icon: "error",
});
return;
}
if (!roomInfo.id) {
uni.showToast({
title: "请选择房间号!",
icon: "error",
});
return;
}
let { id, name, avatar } = userInfo;
let room = {
type: "join",
data: {
id,
name,
avatar,
},
};
room.data.roomId = roomInfo.id;
wsInfo.ws.send({
data: JSON.stringify(room),
});
}
- 离开房间
js
// ws离开房间
function leaveRoom() {
if (!wsInfo.alive) {
uni.showToast({
title: "请先连接!",
icon: "error",
});
return;
}
let { id, name, avatar } = userInfo;
let room = {
type: "leave",
data: {
id,
name,
avatar,
},
};
room.data.roomId = roomInfo.id;
wsInfo.ws.send({
data: JSON.stringify(room),
});
}
- 发送消息
js
// 发送消息
function sendMsg() {
if (!wsInfo.alive) {
uni.showToast({
title: "请先连接!",
icon: "error",
});
return;
}
if (msg.current == "") {
uni.showToast({
title: "请输入内容!",
icon: "error",
});
return;
}
let { id, name, avatar } = userInfo;
let { current } = msg;
let chatInfo = {
type: "chat",
data: {
id,
name,
avatar,
message: current,
},
};
chatInfo.data.roomId = roomInfo.id;
wsInfo.ws.send({
data: JSON.stringify(chatInfo),
});
msg.current = "";
}
服务端搭建
上述聊天室需要用到静态文件服务和 ws 服务,下面讲解一下搭建过程。
静态文件服务搭建
这个就用原生的 node 知识搭建一下即可。
-
准备文件夹和文件
1.新建一个文件夹
static
,npm 初始化npm init -y
;2.在
static
文件夹下面新建一个文件index.js
;3.在
static
文件夹下面新建一个文件夹public
;4.在
public
文件夹下面新建一个文件index.html
;
html
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Node Static File Server</title>
</head>
<body>
<h1>Node Static File Server</h1>
<p>Welcome to Node Static File Server!</p>
</body>
</html>
- 引入依赖包
js
const fs = require("fs");
const http = require("http");
const path = require("path");
const { argv } = require("process");
- 定义域名和端口号
这里使用 script 命令传参来指定域名,在 APP 端不能使用本地 IP 地址,比如localhost
和127.0.0.1
,所以要判断一下。
js
// 域名地址
const dev = "127.0.0.1";
const pro = "192.168.1.88";
const base = argv[2] && argv[2] == "pro" ? pro : dev;
const port = 3000;
- 定义 MINE 格式类型列表
这一部分是为了准确返回对应的文件格式。
js
const types = {
html: "text/html",
css: "text/css",
txt: "text/plain",
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
gif: "image/gif",
ico: "image/x-icon",
js: "application/javascript",
json: "application/json",
xml: "application/xml",
zip: "application/zip",
rar: "application/x-rar-compressed",
apk: "application/vnd.android.package-archive",
ipa: "application/iphone",
webm: "application/webm",
mp4: "application/mp4",
mp3: "application/mpeg",
pdf: "application/pdf",
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
doc: "application/msword",
ppt: "application/vnd.ms-powerpoint",
xls: "application/vnd.ms-excel",
};
- 定义根目录
这一部分是为了定义根目录,所有的静态文件都在这个文件夹下面存放。
js
const directoryName = "./public";
const root = path.normalize(path.resolve(directoryName));
- 开启 HTTP 服务
js
// http服务
const server = http.createServer((req, res) => {
console.log(`${req.method} ${req.url}`);
// 判断文件MINE类型
const extension = path.extname(req.url).slice(1);
const type = extension ? types[extension] : types.html;
const supportedExtension = Boolean(type);
if (!supportedExtension) {
res.writeHead(404, { "Content-Type": "text/html" });
res.end("404: File not found.");
return;
}
let fileName = req.url;
if (req.url === "/") {
fileName = "index.html";
} else if (!extension) {
try {
fs.accessSync(path.join(root, req.url + ".html"), fs.constants.F_OK);
fileName = req.url + ".html";
} catch (e) {
fileName = path.join(req.url, "index.html");
}
}
// 判断文件路径
const filePath = path.join(root, fileName);
const isPathUnderRoot = path.normalize(path.resolve(filePath)).startsWith(root);
if (!isPathUnderRoot) {
res.writeHead(404, { "Content-Type": "text/html" });
res.end("404: File not found.");
return;
}
//读取文件并返回
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404, { "Content-Type": "text/html" });
res.end("404: File not found.");
} else {
res.writeHead(200, { "Content-Type": type });
res.end(data);
}
});
});
- 监听端口
js
// 监听端口
server.listen(port, () => {
console.log(`Server is listening on port http://${base}:${port} !`);
});
写好以后在package.json
文件的scripts
中,写入以下命令。
json
{
"scripts": {
"dev": "node index.js dev",
"pro": "node index.js pro",
"test": "echo \"Error: no test specified\" && exit 1"
}
}
运行一下npm run dev
,
cmd
> static@1.0.0 dev
> node index.js dev
Server is listening on port http://127.0.0.1:3000 !
上传文件服务实现
上传文件这里主要是使用multiparty
来上传,md5
来防止文件名重复上传,造成资源浪费。
- 安装依赖包
shell
npm i multiparty md5
- 引入依赖包
依旧是在index.js
文件里面写入,有些内容有省略,就不重复写了。
js
// ...
const multiparty = require("multiparty");
const md5 = require("md5");
// ...
- 设置响应头
这部分主要是为了解决跨域访问的问题。
js
// 设置响应头
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader(
"Access-Control-Allow-Headers",
"Content-Type,Access-Control-Allow-Headers,Authorization,X-Requested-With"
);
res.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
res.setHeader("Access-Control-Allow-Methods", "POST,GET,PUT,OPTIONS,DELETE");
if (req.method === "OPTIONS") {
res.writeHead(200);
res.end("ok");
return;
}
- 文件上传
这部分就是解析文件,并且返回文件信息数据。
其中的重命名文件是为了不浪费资源,存储相同的图片,统一使用 md5
来命名文件。
js
// 文件上传
if (req.url === "/upload") {
let formdata = new multiparty.Form({
uploadDir: "./public/upload",
});
formdata.parse(req, (err, fields, files) => {
if (err) {
let data = {
code: 102,
msg: "get_fail",
data: {
info: "上传失败!",
},
};
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(data));
} else {
let file = files.file[0];
let fileName = file.path.slice(14);
let ext = file.originalFilename.split(".");
ext = ext[ext.length - 1];
let md5File = md5(file.originalFilename);
let oldPath = `./public/upload/${fileName}`,
newPath = `./public/upload/${md5File}.${ext}`;
let statRes = fs.statSync(file.path);
let isFile = statRes.isFile();
if (isFile) {
fs.renameSync(oldPath, newPath);
let data = {
code: 200,
msg: "get_succ",
data: {
url: `http://${base}:${port}/upload/${md5File}.${ext}`,
filename: md5File,
ext,
},
};
res.end(JSON.stringify(data));
} else {
let data = {
code: 102,
msg: "get_fail",
data: {
info: "文件不存在!",
},
};
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(data));
}
}
});
return;
}
ws 服务器的搭建
- 初始化服务
新建一个文件夹socket
,初始化npm
,安装依赖包。
shell
mkdir socket
cd socket
npm init -y
npm i ws
-
新建一个文件
index.js
; -
引入依赖
js
const webSocket = require("ws");
const { createServer } = require("http");
const { argv } = require("process");
- 创建服务
js
// http服务
const server = createServer((res) => {
res.end("welcome to WS!");
});
// ws服务
const wss = new webSocket.WebSocketServer({
server,
});
- 初始化数据
js
const domainDev = "127.0.0.1"; // 开发域名
const domainPro = "192.168.1.88"; // 生产域名
const domain = argv[2] && argv[2] == "pro" ? domainPro : domainDev;
const port = 3001; // 端口
const MAX_ROOM_NUM = 10; // 每个房间最大用户数
const MAX_USER_NUM = 30; // 用户登录总数
let users = [], // 用户列表
rooms = [
// 房间列表
{
id: 1, // 房间id
name: "房间1", // 房间名称
users: [], // 房间的用户列表
messages: [], // 房间的消息列表
},
{
id: 2,
name: "房间2",
users: [],
messages: [],
},
{
id: 3,
name: "房间3",
users: [],
messages: [],
},
];
- 监听端口
js
// 监听端口
server.listen(port, () => {
console.log(`WebSocket is running on http://${domain}:${port} !`);
});
- 监听 ws 连接
js
// wss连接
wss.on("connection", (ws, req) => {
// 获取ip
const ip = req.socket.remoteAddress;
console.log("ip:", ip);
// 连接状态
ws.isAlive = true;
// 监听错误
ws.on("error", (err) => {
console.log("error:", err);
ws.send(err);
});
// 监听消息
ws.on("message", (data) => {
let res = new TextDecoder().decode(data),
info = JSON.parse(Object.assign(res)),
type = info.type;
info.ip = ip;
switch (type) {
case "ping":
heartbeat(ws);
break;
case "login":
userLogin(ws, info);
break;
case "logout":
userLogout(ws, info);
break;
case "join":
joinRoom(ws, info);
break;
case "leave":
leaveRoom(ws, info);
break;
case "chat":
sendMSg(ws, info);
break;
default:
break;
}
console.log("message:", info);
console.log("users:", users);
console.log("rooms:", rooms);
});
// 断开连接
ws.on("close", (data) => {
console.log("close:", data);
});
});
- 心跳检测
js
// 心跳检测
function heartbeat(ws) {
let result = {
code: 200,
msg: "system",
data: {
info: "connected",
type: "ping",
},
};
returnMsg(ws, result);
}
- 用户登录
js
// 用户登录
function userLogin(ws, info) {
let ip = info.ip;
let { id, name, avatar } = info.data,
result = null;
if (users.length >= MAX_USER_NUM) {
result = {
code: 101,
msg: "system",
data: {
info: "服务器用户已满!",
type: info.type,
},
};
} else if (users.length === 0) {
users.push({
id,
name,
avatar,
ip,
status: 1, // 1.在线 2.离线
roomId: 1,
});
result = {
code: 200,
msg: "system",
data: {
info: "登录成功!",
type: info.type,
},
};
} else {
let userInfo = users.find((s) => name === s.name);
if (userInfo && userInfo.id) {
let index = users.findIndex((s) => name == s.name);
if (users[index].status === 2) {
users[index].status = 1;
result = {
code: 200,
msg: "system",
data: {
info: "登录成功!",
type: info.type,
},
};
} else {
result = {
code: 101,
msg: "system",
data: {
info: "用户已登录!",
type: info.type,
},
};
}
} else {
users.push({
id,
name,
avatar,
ip,
status: 1,
roomId: 1,
});
result = {
code: 200,
msg: "system",
data: {
info: "登录成功!",
type: info.type,
},
};
}
}
returnMsg(ws, result);
}
- 用户登出
js
// 用户登出
function userLogout(ws, info) {
let { name } = info.data;
if (users.length === 0) return;
let index = users.findIndex((s) => s.name == name);
if (!users[index]) return;
users[index].status = 2;
let result = {
code: 200,
msg: "system",
data: {
info: "退出成功!",
type: info.type,
user: info.data,
},
};
returnMsg(ws, result);
}
- 加入房间
js
// 加入房间
function joinRoom(ws, info) {
let { name, roomId } = info.data;
let roomInfo = rooms[roomId - 1];
if (!roomInfo) return;
if (!roomInfo.users) return;
let roomUser = roomInfo.users;
let result = null;
if (roomUser.length >= MAX_ROOM_NUM) {
result = {
code: 101,
msg: "system",
data: {
info: "房间已满!",
type: info.type,
},
};
} else {
if (!roomUser.includes(name)) {
rooms[roomId - 1].users.push(name);
}
let userList = users.filter((s) => {
if (rooms[roomId - 1].users.includes(s.name)) {
return s.name;
}
});
result = {
code: 200,
msg: "system",
data: {
info: "加入成功!",
type: info.type,
roomId,
user: info.data,
users: userList,
},
};
}
returnMsg(ws, result);
}
- 离开房间
js
// 离开房间
function leaveRoom(ws, info) {
let { name, roomId } = info.data;
let roomUser = rooms[roomId - 1].users;
if (!roomUser.includes(name)) return;
let userIndex = roomUser.findIndex((s) => s == name);
rooms[roomId - 1].users.splice(userIndex, 1);
let userList = users.filter((s) => {
if (rooms[roomId - 1].users.includes(s.name)) {
return s;
}
});
let result = {
code: 200,
msg: "system",
data: {
info: "离开房间!",
type: info.type,
roomId,
user: info.data,
users: userList,
},
};
returnMsg(ws, result);
}
- 发送消息
js
// 发送消息
function sendMSg(ws, info) {
let { roomId, name, avatar, id, message } = info.data,
createTime = new Date().toLocaleString();
createTime = createTime.replace(/\//gi, "-");
let roomIndex = rooms.findIndex((s) => roomId === s.id);
let roomParam = {
id,
name,
avatar,
content: message,
createTime,
};
rooms[roomIndex].messages.push(roomParam);
let msg = rooms[roomId - 1].messages;
let result = {
code: 200,
msg: "user",
data: {
info: "接收成功!",
type: info.type,
roomId,
user: info.data,
msg,
},
};
returnMsg(ws, result);
}
- 返回消息
js
// 返回消息
function returnMsg(ws, result) {
let type = result.data.type;
result = JSON.stringify(result);
if (["join", "leave"].includes(type)) {
wss.clients.forEach((item) => {
console.log("room:", item.readyState, webSocket.OPEN);
if (item.readyState === webSocket.OPEN) {
item.send(result);
}
});
} else if (type === "chat") {
wss.clients.forEach((item) => {
if (item !== ws && item.readyState === webSocket.OPEN) {
item.send(result);
}
if (item === ws && item.readyState === webSocket.OPEN) {
ws.send(result);
}
});
} else {
ws.send(result);
}
}
案例展示
- h5 端效果
- 小程序端效果
- APP 端效果
- 服务端
最后
以上就是使用 websocket 实现简易聊天室的主要内容,有不足之处,请多多指正。