導語:在一些社交軟體中,經常可以看到各種聊天室的介面,接下來就總結一下聊天室的原理個實現方法,最後做一個簡易的聊天室,包括登入/登出、加入/離開房間、傳送接收聊天訊息等功能。
目錄
- 準備工作
- 原理分析
- 元件實現
- 實戰演練
- 服務端搭建
- 案例展示
準備工作
- 在
pages/index
資料夾下面新建一個名叫chat
的元件; - 按照前一篇所說的頁面結構,編寫好預定的聊天頁面;
原理分析
前端部分
此聊天室前端方面使用了 uniapp 提供的幾個 API 實現包括:
uni.connectSocket
:連線到 websocket 伺服器;SocketTask.onOpen
:監聽服務端連線開啟;SocketTask.onClose
:監聽服務端連線關閉;SocketTask.onError
:監聽服務端連線錯誤;SocketTask.onMessage
:監聽服務端的訊息;SocketTask.send
:向服務端傳送訊息;SocketTask.close
:關閉服務端連線;
後端部分
此聊天室服務端使用 npm 庫ws
搭建,另外頭像上傳部分使用原生node
實現,待會兒會詳細介紹實現方法。
元件實現
準備工作和原理分析完成後,接下來寫一個簡單的頁面,下面主要是展示主要的內容。
模板部分
- 登入部分
包括輸入使用者名稱和上傳頭像的頁面和功能。
<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>
- 加入房間部分
包括選擇房間的退出登入的頁面和功能。
<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>
- 聊天室介面
包括展示房間號,退出房間,線上使用者列表,聊天訊息區域以及輸入聊天內容和傳送訊息的頁面和功能。
<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>
樣式部分
- 登入和加入房間樣式
.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;
}
}
}
- 聊天房間頁面樣式
.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 資訊
// 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, // 服務端計時間隔
});
- 使用者資訊
// 使用者資訊
const userInfo = reactive({
id: "1", // 使用者id
name: "", // 使用者名稱稱
avatar: "", // 使用者頭像
users: [], // 使用者線上列表
usersText: "", // 使用者線上列表文字
});
- 房間資訊
// 房間資訊
const roomInfo = reactive({
id: 1, // 房間id
name: "房間1", // 房間名稱
list: ["房間1", "房間2", "房間3"], // 房間列表
mode: "avatar", // 模式:avatar頭像
scrollTop: 0, // 滾動到頂部距離
scrollHei: 0, // 滾動高度
goBottomTimer: null, // 到底部計時
goBottomTime: 500, // 到頂部計時間隔
});
- 聊天資訊
// 聊天資訊
const msg = reactive({
current: "", // 輸入框內容
list: [], // 聊天訊息列表
});
實戰演練
基本工作準備完畢後,接下來就開始實現功能。
連線 websocket
進入頁面後,首先連線上 websocket 服務端。
- 連線 websocket
// 連線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);
});
}
- 處理各種型別的訊息
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);
}
}
}
- 心跳檢測
// 心跳檢測
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);
}
- 斷線重連
// 斷線重連
function reConnect() {
if (wsInfo.lock) return;
wsInfo.lock = true;
wsInfo.reconnectTimer = setTimeout(() => {
connectWs();
wsInfo.lock = false;
}, wsInfo.reconnectTime);
}
- 斷開 websocket 連線
// 斷開連線
function closeWs() {
if (!wsInfo.alive) {
uni.showToast({
title: "請先連線!",
icon: "error",
});
return;
}
leaveRoom();
wsLogout();
wsInfo.ws.close();
}
- 上傳頭像
這個就用到之前封裝的檔案操作方法。
// 上傳操作
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",
});
}
}
- 使用者登入
// 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),
});
}
- 使用者退出
// 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),
});
}
- 加入房間
// 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),
});
}
- 離開房間
// 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),
});
}
- 傳送訊息
// 傳送訊息
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
;
<!-- 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>
- 引入依賴包
const fs = require("fs");
const http = require("http");
const path = require("path");
const { argv } = require("process");
- 定義域名和埠號
這裡使用 script 命令傳參來指定域名,在 APP 端不能使用本地 IP 地址,比如localhost
和127.0.0.1
,所以要判斷一下。
// 域名地址
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 格式型別列表
這一部分是為了準確返回對應的檔案格式。
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",
};
- 定義根目錄
這一部分是為了定義根目錄,所有的靜態檔案都在這個資料夾下面存放。
const directoryName = "./public";
const root = path.normalize(path.resolve(directoryName));
- 開啟 HTTP 服務
// 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);
}
});
});
- 監聽埠
// 監聽埠
server.listen(port, () => {
console.log(`Server is listening on port http://${base}:${port} !`);
});
寫好以後在package.json
檔案的scripts
中,寫入以下命令。
{
"scripts": {
"dev": "node index.js dev",
"pro": "node index.js pro",
"test": "echo \"Error: no test specified\" && exit 1"
}
}
執行一下npm run dev
,
> static@1.0.0 dev
> node index.js dev
Server is listening on port http://127.0.0.1:3000 !
上傳檔案服務實現
上傳檔案這裡主要是使用multiparty
來上傳,md5
來防止檔名重複上傳,造成資源浪費。
- 安裝依賴包
npm i multiparty md5
- 引入依賴包
依舊是在index.js
檔案裡面寫入,有些內容有省略,就不重複寫了。
// ...
const multiparty = require("multiparty");
const md5 = require("md5");
// ...
- 設定響應頭
這部分主要是為了解決跨域訪問的問題。
// 設定響應頭
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
來命名檔案。
// 檔案上傳
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
,安裝依賴包。
mkdir socket
cd socket
npm init -y
npm i ws
- 新建一個檔案
index.js
; - 引入依賴
const webSocket = require("ws");
const { createServer } = require("http");
const { argv } = require("process");
- 建立服務
// http服務
const server = createServer((res) => {
res.end("welcome to WS!");
});
// ws服務
const wss = new webSocket.WebSocketServer({
server,
});
- 初始化資料
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: [],
},
];
- 監聽埠
// 監聽埠
server.listen(port, () => {
console.log(`WebSocket is running on http://${domain}:${port} !`);
});
- 監聽 ws 連線
// 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);
});
});
- 心跳檢測
// 心跳檢測
function heartbeat(ws) {
let result = {
code: 200,
msg: "system",
data: {
info: "connected",
type: "ping",
},
};
returnMsg(ws, result);
}
- 使用者登入
// 使用者登入
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);
}
- 使用者登出
// 使用者登出
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);
}
- 加入房間
// 加入房間
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);
}
- 離開房間
// 離開房間
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);
}
- 傳送訊息
// 傳送訊息
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);
}
- 返回訊息
// 返回訊息
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 實現簡易聊天室的主要內容,有不足之處,請多多指正。