socket.io-client实现实前后端时通信功能
这里我使用的后端 基于node.js的koa框架 前端使用的是vite
{
"name": "hou",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node app.js",
"dev": "nodemon app.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"@koa/cors": "^5.0.0",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2",
"koa": "^2.15.3",
"koa-bodyparser": "^4.4.1",
"koa-jwt": "^4.0.4",
"koa-router": "^13.0.1",
"nodemon": "^3.1.7",
"ws": "^8.18.0"
}
}
const Koa = require("koa");
const Router = require("koa-router");
const bodyParser = require("koa-bodyparser");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");
const koaJwt = require("koa-jwt");
const cors = require("@koa/cors");
const app = new Koa();
const router = new Router();
const secret = "supersecretkey"; // 用于签发 JWT 的密钥
app.use(cors());
app.use(bodyParser());
// 模拟用户数据库
const users = [
{ id: 1, username: "user1", password: bcrypt.hashSync("password", 10) },
{ id: 2, username: "user2", password: bcrypt.hashSync("password", 10) },
{ id: 3, username: "user3", password: bcrypt.hashSync("password", 10) },
{ id: 4, username: "user4", password: bcrypt.hashSync("password", 10) },
{ id: 5, username: "user5", password: bcrypt.hashSync("password", 10) },
];
// 登录路由
router.post("/login", async (ctx) => {
const { username, password } = ctx.request.body;
const user = users.find((u) => u.username === username);
if (!user) {
ctx.status = 400;
ctx.body = { message: "没有此用户" };
return;
}
const validPassword = bcrypt.compareSync(password, user.password);
if (!validPassword) {
ctx.status = 400;
ctx.body = { message: "密码错误" };
return;
}
const token = jwt.sign({ id: user.id, username: user.username }, secret, {
expiresIn: "1h",
});
ctx.body = { token };
});
// 获取用户信息路由(登录后可用)
router.get("/me", koaJwt({ secret }), async (ctx) => {
const user = users.find((u) => u.id === ctx.state.user.id);
ctx.body = user;
});
// 中间件:使用 JWT 验证
app.use(koaJwt({ secret }).unless({ path: [/^\/login/] }));
// 搜索用户接口(登录后可用)
router.get("/search", async (ctx) => {
const query = ctx.query.q;
const result = users.filter((user) => user.username.includes(query));
ctx.body = result;
});
// 添加好友接口(登录后可用)
let friends = {}; // { userId: [friendId1, friendId2, ...] }
router.post("/add-friend", async (ctx) => {
const { friendId } = ctx.request.body;
const userId = ctx.state.user.id; // 从 JWT 中提取用户信息
if (!friends[userId]) {
friends[userId] = [];
}
if (!friends[userId].includes(friendId)) {
friends[userId].push(friendId);
}
ctx.body = { message: "Friend added" };
});
// WebSocket 服务同样需要身份验证
const WebSocket = require("ws");
const http = require("http");
const server = http.createServer(app.callback());
const wss = new WebSocket.Server({ server });
wss.on("connection", (ws, req) => {
const token = req.url.split("?token=")[1];
if (!token) {
ws.close();
return;
}
try {
const decoded = jwt.verify(token, secret);
ws.userId = decoded.id;
ws.send("Connected to chat");
ws.on("message", (message) => {
// 广播消息仅限好友之间
const friendIds = friends[ws.userId] || [];
wss.clients.forEach((client) => {
if (
client.readyState === WebSocket.OPEN &&
friendIds.includes(client.userId)
) {
client.send(message);
}
});
});
} catch (error) {
ws.close();
}
});
app.use(router.routes()).use(router.allowedMethods());
server.listen(3000, () => {
console.log("Server running on http://localhost:3000");
});
前端
{
"name": "vite-project",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"socket.io-client": "^4.8.0",
"vue": "^3.4.37",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.2",
"vite": "^5.4.1"
}
}
登录
<template>
<div>
<input v-model="username" placeholder="Username" />
<input type="password" v-model="password" placeholder="Password" />
<button @click="login">Login</button>
</div>
</template>
<script>
export default {
data() {
return {
username: "",
password: "",
};
},
methods: {
async login() {
const response = await fetch("http://localhost:3000/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: this.username,
password: this.password,
}),
});
const data = await response.json();
if (response.ok) {
localStorage.setItem("token", data.token);
this.$router.push("/"); // 登录成功后跳转到聊天页面
} else {
alert(data.message);
}
},
},
};
</script>
<template>
<div>
<div>
<span>欢迎;{{ user.username }}</span>
</div>
<!-- 搜索和添加好友功能 -->
<div>
<input v-model="searchQuery" placeholder="Search for users" />
<button @click="searchUsers">Search</button>
<div v-if="searchResults.length > 0">
<h3>Search Results</h3>
<div v-for="user in searchResults" :key="user.id">
{{ user.username }}
<button @click="addFriend(user.id)">Add Friend</button>
</div>
</div>
</div>
<!-- 好友列表 -->
<div v-if="friends.length > 0">
<h3>Friends List</h3>
<ul>
<li
v-for="friend in friends"
:key="friend.id"
@click="startChat(friend)"
>
{{ friend.username }}
</li>
</ul>
</div>
<!-- 聊天窗口 -->
<div v-if="currentChatUser">
<h3>Chatting with {{ currentChatUser.username }}</h3>
<div class="chat-window">
<div
v-for="(msg, index) in messages"
:key="index"
:class="{ sent: msg.sentByUser, received: !msg.sentByUser }"
>
{{ msg.text }} <span class="timestamp">{{ msg.timestamp }}</span>
</div>
</div>
<input
v-model="newMessage"
@keyup.enter="sendMessage"
placeholder="Type a message..."
/>
<button @click="sendMessage">Send</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
searchQuery: "", // 搜索框的输入
searchResults: [], // 搜索结果
friends: [], // 好友列表
socket: null, // WebSocket 连接
messages: [], // 聊天记录
newMessage: "", // 输入的新消息
currentChatUser: null, // 当前聊天的好友
user: {}, // 当前用户信息
};
},
created() {
// 获取当前用户信息
const token = localStorage.getItem("token");
fetch("http://localhost:3000/me", {
headers: { Authorization: `Bearer ${token}` },
})
.then((response) => response.json())
.then((user) => {
this.user = user;
});
},
methods: {
// 搜索用户
async searchUsers() {
const token = localStorage.getItem("token");
const response = await fetch(
`http://localhost:3000/search?q=${this.searchQuery}`,
{
headers: { Authorization: `Bearer ${token}` },
}
);
this.searchResults = await response.json();
},
// 添加好友
async addFriend(friendId) {
const token = localStorage.getItem("token");
const response = await fetch("http://localhost:3000/add-friend", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ friendId }),
});
if (response.ok) {
alert("Friend added!");
// 可选:可以在添加好友后,更新好友列表
this.friends.push(
this.searchResults.find((user) => user.id === friendId)
);
}
},
// 开始与某个好友聊天
startChat(friend) {
this.currentChatUser = friend; // 设置当前聊天用户
this.messages = []; // 清空当前消息
// 连接 WebSocket(与服务器端的 WebSocket 实现保持一致)
if (!this.socket) {
const token = localStorage.getItem("token");
this.socket = new WebSocket(`ws://localhost:3000?token=${token}`);
// 监听 WebSocket 消息
this.socket.onmessage = (event) => {
if (event.data instanceof Blob) {
// 如果是 Blob 类型的数据,将其转换为文本
const reader = new FileReader();
reader.onload = (event) => {
const text = event.target.result;
const message = {
text: text,
sentByUser: false,
timestamp: new Date().toLocaleTimeString(), // 添加时间戳
};
this.messages.push(message); // 将消息添加到消息列表
};
reader.readAsText(event.data);
} else {
// 如果不是 Blob 数据,直接将消息显示
const message = {
text: event.data,
sentByUser: false,
timestamp: new Date().toLocaleTimeString(),
};
this.messages.push(message);
}
};
}
},
// 发送消息
sendMessage() {
if (this.newMessage.trim() !== "") {
const message = {
text: this.newMessage,
sentByUser: true,
timestamp: new Date().toLocaleTimeString(), // 获取当前时间
};
this.messages.push(message); // 添加到消息列表
this.socket.send(this.newMessage); // 发送消息
this.newMessage = ""; // 清空输入框
}
},
},
};
</script>
<style>
/* 样式调整 */
.chat-window {
border: 1px solid #ccc;
padding: 10px;
height: 300px;
overflow-y: scroll;
margin-bottom: 10px;
}
.sent {
text-align: right;
background-color: #d1f0d1; /* 发送的消息背景色 */
}
.received {
text-align: left;
background-color: #f0f0f0; /* 接收的消息背景色 */
}
.timestamp {
font-size: 0.8em;
color: #888; /* 时间戳颜色 */
}
</style>
import { createRouter, createWebHistory } from "vue-router";
const routes = [
{
path: "/",
name: "Home",
component: import("../views/Home.vue"),
},
{
path: "/login",
name: "Login",
component: import("../views/Login.vue"),
},
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});
export default router;
主体功能初步实现,后期可以优化方向,设计数据库 将添加过的好友存储在数据表中,添加好友要先通过才能添加