vue3+socketio一个即时通讯的小demo

前端安装:npm install socket.io-client

服务端安装:npm i express socket.io

app.vue

vue 复制代码
<template>
  <div class="homes">
    <div>连接状态:{{ state.connected ? '✅ 已连接' : '❌ 未连接' }}</div>
    <button @click="connect">开启连接</button>
    <button @click="disconnect">关闭连接</button>
    <br><br>
    <button @click="generateRandomMessage" :disabled="!state.connected">
      生成随机消息
    </button>

    <div class="message-list">
      <div v-for="(msg, i) in state.messages" :key="i" class="message">
        <span>[{{ msg.socketID }}] </span>
        <span>[{{ msg.time }}] </span>
        <span>{{ msg.content }}</span>
      </div>
    </div>
  </div>
</template>

<script setup>
import { onMounted, onUnmounted } from "vue";
import { state, socket } from "../hooks/socket.js";

// 存储事件监听的标识(用于移除)
let messageListener = null;
let historyListener = null;

// 初始化监听(确保只绑定一次)
const initListeners = () => {
  // 先移除可能存在的旧监听(关键:防止重复绑定)
  if (messageListener) {
    socket.off("broadcast_message", messageListener);
  }
  if (historyListener) {
    socket.off("history_messages", historyListener);
  }

  // 绑定新监听,并保存引用
  messageListener = (msg) => {
    state.messages.push(msg);
  };
  socket.on("broadcast_message", messageListener);

  historyListener = (history) => {
    state.messages = history;
  };
  socket.on("history_messages", historyListener);
};

// 移除监听(组件卸载时)
const removeListeners = () => {
  socket.off("broadcast_message", messageListener);
  socket.off("history_messages", historyListener);
  messageListener = null;
  historyListener = null;
};

// 连接函数
const connect = () => {
  if (!state.connected) {
    initListeners(); // 连接前初始化监听
    socket.connect();
  }
};

// 断开函数
const disconnect = () => {
  if (state.connected) {
    removeListeners(); // 断开时移除监听
    socket.disconnect();
  }
};

// 生成消息(不变)
const generateRandomMessage = () => {
  if (!state.connected) return;
  const msg = Fn_text();
  socket.emit("new_message", msg);
};

// 随机消息生成函数(不变)
const Fn_text = (min = 5, max = 30) => {
  const shortWords = ["风", "云", "花", "草", "人", "心", "走", "看"];
  const midWords = ["春天", "夏天", "阳光", "月亮", "星星", "河流"];
  const connectWords = ["和", "与", "但", "而", "在", "的"];
  const punctuation = ["。", "?", "!"];

  let sentence = [];
  let currentLen = 0;
  const targetLen = Math.floor(Math.random() * (max - min + 1)) + min;

  while (currentLen < targetLen) {
    const pools = [
      { pool: shortWords, weight: 4 },
      { pool: midWords, weight: 3 },
      { pool: connectWords, weight: 2 }
    ];
    let totalWeight = pools.reduce((sum, p) => sum + p.weight, 0);
    let random = Math.random() * totalWeight;
    let chosenPool = shortWords;

    for (const p of pools) {
      random -= p.weight;
      if (random <= 0) {
        chosenPool = p.pool;
        break;
      }
    }

    const word = chosenPool[Math.floor(Math.random() * chosenPool.length)];
    if (currentLen + word.length > max) continue;
    sentence.push(word);
    currentLen += word.length;
  }

  return sentence.join("") + punctuation[Math.floor(Math.random() * punctuation.length)];
};

// 组件卸载时清理
onUnmounted(() => {
  removeListeners();
});
</script>

<style scoped>
/* 样式不变 */
.homes {
  padding: 20px;
}
button {
  margin: 5px;
  padding: 8px 16px;
  cursor: pointer;
}
.message-list {
  margin-top: 20px;
  border: 1px solid #ccc;
  padding: 10px;
  height: 400px;
  overflow-y: auto;
}
.message {
  margin: 5px 0;
  padding: 5px;
  background: #f0f0f0;
}
</style>

socket.js

js 复制代码
// hooks/socket.js
import { io } from "socket.io-client";
import { reactive } from "vue";

// 用reactive管理状态(确保响应式)
export const state = reactive({
  connected: false,
  messages: []
});

// 创建Socket实例(自动连接设为false,手动控制)
export const socket = io("http://localhost:3000", {
  autoConnect: false, // 关键:禁用自动连接,由按钮控制
  cors: {
    origin: "http://localhost:5179" // 必须和前端实际端口一致
  }
});

// 监听连接状态变化(自动更新state)
socket.on("connect", () => {
  state.connected = true;
  console.log("✅ 已连接到服务器");
});

socket.on("disconnect", () => {
  state.connected = false;
  console.log("❌ 已断开连接");
});

服务端的index.js

js 复制代码
import express from 'express';
import { createServer } from 'node:http';
import { Server } from 'socket.io';

const app = express();
const server = createServer(app);
const io = new Server(server, {
  cors: {
    origin: "http://localhost:5179", // 必须和前端端口一致
    methods: ["GET", "POST"]
  }
});

let messageHistory = [];

io.on("connection", (socket) => {
  console.log("新用户连接:", socket.id);
  
  // 发送历史消息
  socket.emit("history_messages", messageHistory);

  // 接收消息并广播
  socket.on("new_message", (content) => {
    const msg = {
      content,
      socketID:socket.id,
      time: new Date().toLocaleTimeString()
    };
    messageHistory.push(msg);
    io.emit("broadcast_message", msg); // 广播给所有用户
  });

  socket.on("disconnect", () => {
    console.log("用户断开:", socket.id);
  });
});

server.listen(3000, () => {
  console.log("服务端运行在 http://localhost:3000");
});
相关推荐
Stringzhua21 分钟前
Vue数据的变更操作与表单数据的收集【6】
前端·javascript·vue.js
乐~~~1 小时前
el-date-picker type=daterange 日期范围限制
javascript·vue.js·elementui
武昌库里写JAVA6 小时前
使用 Java 开发 Android 应用:Kotlin 与 Java 的混合编程
java·vue.js·spring boot·sql·学习
HANK6 小时前
ECharts高效实现复杂图表指南
前端·vue.js
Juchecar6 小时前
Vue3 响应式 ref 和 reactive 原理详解及选择建议
前端·vue.js
zuo-yiran6 小时前
element table 表格多选框选中高亮
vue.js·elementui
Aotman_6 小时前
el-input 重写带图标密码框(点击小眼睛显示、隐藏密码)
前端·javascript·vue.js
lineo_7 小时前
手写 pinia 持久化缓存插件,兼容indexDB
前端·vue.js
王林不想说话7 小时前
新的枚举使用方式enum-plus
前端·vue.js·typescript