当前位置: 首页 > news >正文

Vue 3 + Socket.io 实时聊天项目完整开发文档

Vue 3 + Socket.io 实时聊天项目完整开发文档

接上篇文档 别再用原生的 Socket 了!Nest.js 让你的实时聊天系统开发效率翻倍
,这篇将展示前端界面及功能

完整项目地址:v-chat-pro
先看目标:

  • 完成注册、登录、通讯录、好友申请、私聊会话、未读数徽章。
  • 与后端chat-server接口与事件完全对齐。
    注册及登录展示:

添加好友:

效果展示:


多用户效果


前言

本文档旨在提供和后端文档同等粒度的前端开发步骤。我们将从环境搭建开始,逐步实现认证、会话、通讯录与实时聊天。前端采用Vue 3 + Vite + TypeScript + Vant + Pinia + Socket.io-client

前端项目目录:v-chat
后端项目目录:chat-server


第一阶段:环境搭建与基础配置

1.1 安装并启动项目

cdv-chatpnpminstallpnpmdev

默认访问:http://localhost:5173

1.2 配置后端地址

v-chat下新建.env.development

VITE_API_BASE_URL=http://localhost:3000

说明:HTTP 请求与 Socket 连接都会读取该变量。

1.3 配置入口文件

文件路径src/main.ts

import'vant/lib/index.css'import'./assets/main.css'import{createApp}from'vue'import{createPinia}from'pinia'importAppfrom'./App.vue'importrouterfrom'./router'constapp=createApp(App)app.use(createPinia())app.use(router)app.mount('#app')

第二阶段:请求层与 Socket 层封装

2.1 封装 Axios 请求实例

文件路径src/utils/request.ts

importaxiosfrom'axios'importtype{AxiosInstance,AxiosRequestConfig,AxiosResponse}from'axios'import{showFailToast}from'vant'constservice:AxiosInstance=axios.create({baseURL:import.meta.env.VITE_API_BASE_URL||'http://localhost:3000',timeout:10000,headers:{'Content-Type':'application/json'}})service.interceptors.response.use((response:AxiosResponse)=>{const{code,message,data}=response.dataif(code&&code>=400){showFailToast(message||'业务逻辑错误')returnPromise.reject(newError(message||'Error'))}returndata},(error)=>{constmessage=error.response?.data?.message||error.message||'网络请求错误'showFailToast(message)returnPromise.reject(error)})constrequest=<T=any>(config:AxiosRequestConfig):Promise<T>=>{returnservice.request(config)}exportdefaultrequest

2.2 封装 Socket 单例

文件路径src/utils/socket.ts

import{io,Socket}from'socket.io-client'import{ref}from'vue'constsocket=ref<Socket|null>(null)letcurrentUserId:number|null=nullconsteventListeners=newMap<string,Set<(data:any)=>void>>()exportfunctionuseSocket(){constconnect=(userId:number)=>{if(socket.value?.connected&&currentUserId===userId)returnif(socket.value){socket.value.disconnect()}currentUserId=userIdconstapiBase=import.meta.env.VITE_API_BASE_URL||'http://localhost:3000'constnewSocket=io(apiBase,{query:{userId}})eventListeners.forEach((callbacks,event)=>{callbacks.forEach((callback)=>{newSocket.on(event,callback)})})socket.value=newSocket}constemit=(event:string,data:any)=>{socket.value?.emit(event,data)}conston=(event:string,callback:(data:any)=>void)=>{if(!eventListeners.has(event)){eventListeners.set(event,newSet())}constcallbacks=eventListeners.get(event)!if(!callbacks.has(callback)){callbacks.add(callback)socket.value?.on(event,callback)}}constoff=(event:string,callback:(data:any)=>void)=>{constcallbacks=eventListeners.get(event)if(callbacks){callbacks.delete(callback)socket.value?.off(event,callback)}}return{socket,connect,emit,on,off}}

第三阶段:状态管理与路由鉴权

3.1 配置用户全局状态

文件路径src/stores/user.ts

import{defineStore}from'pinia'import{ref,computed}from'vue'exportconstuseUserStore=defineStore('user',()=>{constuserInfo=ref<{id:number;username:string}|null>(null)consttoken=ref(localStorage.getItem('token')||'')constpendingFriendCount=ref(0)constsessions=ref<any[]>([])consttotalUnreadCount=computed(()=>{returnsessions.value.reduce((sum,s)=>sum+(s.unreadCount||0),0)})constisLoggedIn=computed(()=>!!userInfo.value?.id)functionsetUser(user:{id:number;username:string}){userInfo.value=user localStorage.setItem('currentUser',JSON.stringify(user))}functionsetToken(val:string){token.value=val localStorage.setItem('token',val)}functioninitFromStorage(){constsaved=localStorage.getItem('currentUser')if(saved)userInfo.value=JSON.parse(saved)}functionlogout(){userInfo.value=nulltoken.value=''pendingFriendCount.value=0localStorage.removeItem('currentUser')localStorage.removeItem('token')}return{userInfo,token,pendingFriendCount,sessions,totalUnreadCount,isLoggedIn,setUser,setToken,initFromStorage,logout}})

3.2 配置路由与守卫

文件路径src/router/index.ts

import{createRouter,createWebHistory}from'vue-router'import{useUserStore}from'@/stores/user'constrouter=createRouter({history:createWebHistory(import.meta.env.BASE_URL),routes:[{path:'/',name:'index',component:()=>import('@/views/IndexView.vue'),meta:{requiresAuth:true}},{path:'/auth/login',name:'login',component:()=>import('@/views/auth/LoginView.vue')},{path:'/auth/register',name:'register',component:()=>import('@/views/auth/RegisterView.vue')},{path:'/contact',name:'contact',component:()=>import('@/views/contact/ContactView.vue'),meta:{requiresAuth:true}},{path:'/contact/search',name:'search',component:()=>import('@/views/contact/SearchView.vue'),meta:{requiresAuth:true}},{path:'/contact/pending',name:'pending',component:()=>import('@/views/contact/PendingRequestsView.vue'),meta:{requiresAuth:true}},{path:'/chat/:id',name:'chat',component:()=>import('@/views/chat/ChatView.vue'),meta:{requiresAuth:true}},{path:'/me',name:'me',component:()=>import('@/views/me/MeView.vue'),meta:{requiresAuth:true}},]})router.beforeEach((to)=>{constuserStore=useUserStore()userStore.initFromStorage()if(to.meta.requiresAuth&&!userStore.isLoggedIn){return{name:'login'}}if(userStore.isLoggedIn&&(to.name==='login'||to.name==='register')){return{name:'index'}}})exportdefaultrouter

第四阶段:接口模块封装

4.1 用户接口

文件路径src/api/user.ts

importrequestfrom'@/utils/request'exportconstregisterApi=(data:any)=>request({url:'/user/register',method:'post',data})exportconstloginApi=(data:any)=>request({url:'/auth/login',method:'post',data})exportconstgetUserInfoApi=(id:number)=>request({url:'/user/info',method:'get',params:{id}})exportconstsearchUserApi=(username:string)=>request({url:'/user/search',method:'get',params:{username}})

4.2 好友接口

文件路径src/api/friend.ts

importrequestfrom'@/utils/request'exportconstsendFriendRequestApi=(data:{requesterId:number;addresseeId:number})=>request({url:'/user/friend/request',method:'post',data})exportconstacceptFriendRequestApi=(data:{userId:number;requestId:number})=>request({url:'/user/friend/accept',method:'post',data})exportconstrejectFriendRequestApi=(data:{userId:number;requestId:number})=>request({url:'/user/friend/reject',method:'post',data})exportconstgetFriendListApi=(userId:number)=>request({url:'/user/friend/list',method:'get',params:{userId}})exportconstgetPendingRequestsApi=(userId:number)=>request({url:'/user/friend/pending',method:'get',params:{userId}})

4.3 聊天接口

文件路径src/api/chat.ts

importrequestfrom'@/utils/request'exportconstgetSessionsApi=(userId:number)=>request({url:'/chat/sessions',method:'get',params:{userId}})exportconstgetChatHistoryApi=(user1Id:number,user2Id:number)=>request({url:'/chat/history',method:'get',params:{user1Id,user2Id}})exportconstmarkAsReadApi=(userId:number,friendId:number)=>request({url:'/chat/read',method:'post',data:{userId,friendId}})

第五阶段:全局监听与页面实现

5.1 全局监听入口(App.vue)

文件路径src/App.vue

核心职责:

  1. 恢复登录态后建立连接。
  2. 监听friendRequest/friendAccepted/friendRejected/message
  3. 在非当前聊天页收到消息时刷新会话并提醒。

5.2 登录页

文件路径src/views/auth/LoginView.vue

核心逻辑:

constres=awaitloginApi({username:username.value,password:password.value})userStore.setUser({id:res.user.id,username:res.user.username})userStore.setToken(res.access_token)connect(res.user.id)router.replace('/')

5.3 注册页

文件路径src/views/auth/RegisterView.vue

核心逻辑:

if(password.value!==confirmPassword.value)returnawaitregisterApi({username:username.value,password:password.value})router.push('/auth/login')

5.4 首页会话页

文件路径src/views/IndexView.vue

核心职责:

  1. 展示/chat/sessions返回的会话数据。
  2. 显示会话未读数。
  3. TabBar 显示总未读和待处理申请红点。

5.5 通讯录页

文件路径src/views/contact/ContactView.vue

核心职责:

  1. 展示好友列表。
  2. 进入“新的朋友”页处理申请。
  3. 点击好友跳转聊天页。

5.6 搜索好友页

文件路径src/views/contact/SearchView.vue

核心逻辑:

constres=awaitsearchUserApi(keyword.value)searchResult.value=res||[]awaitsendFriendRequestApi({requesterId:userStore.user.id,addresseeId:targetId})

5.7 新的朋友页

文件路径src/views/contact/PendingRequestsView.vue

核心逻辑:

awaitacceptFriendRequestApi({userId:userStore.user.id,requestId})emit('sendMessage',{senderId:userStore.user.id,receiverId:requesterId,content:'我通过了你的朋友验证请求,现在我们可以开始聊天了'})

5.8 聊天页

文件路径src/views/chat/ChatView.vue

核心职责:

  1. 拉历史消息。
  2. 发送sendMessage事件。
  3. 监听message并仅渲染当前会话数据。
  4. 进入页面立即markAsRead

核心代码:

awaitgetChatHistoryApi(userStore.user.id,targetId)emit('sendMessage',{senderId:userStore.user.id,receiverId:targetId,content})awaitmarkAsReadApi(userStore.user.id,targetId)userStore.fetchSessions()

第六阶段:联调测试步骤

  1. 启动后端:
cdchat-serverpnpminstallpnpmrun start:dev
  1. 启动前端:
cdv-chatpnpminstallpnpmdev
  1. 准备两个账号 A/B,分别登录。
  2. A 搜索 B 并发送好友申请。
  3. B 在“新的朋友”点击通过。
  4. 验证:
    • A 收到通过提示。
    • 会话出现首条打招呼消息。
    • 双方实时收发正常。
    • 非聊天页收消息时会话红点增加。
    • 进入聊天页后红点清零。

第七阶段:常见问题排查

7.1 登录后收不到消息

排查:

  1. 登录成功后是否调用connect(userId)
  2. query.userId是否传给后端。
  3. 后端网关是否记录用户连接日志。

7.2 红点不准

排查:

  1. /chat/sessionsunreadCount是否正确。
  2. 收到新消息和标记已读后是否调用fetchSessions
  3. 总未读是否来自computed,而不是手工加减。

7.3 好友通过后发不出消息

排查:

  1. 好友关系是否已accepted
  2. senderId/receiverId是否传错。
  3. 是否监听后端返回的error事件。

http://www.cnnetsun.cn/news/2059277.html

相关文章:

  • Phi-3.5-mini-instruct入门必看:网页封装+参数详解+中文场景调优指南
  • Flux2-Klein-9B-True-V2多场景落地:政府宣传海报/公益广告图生成实践
  • Zotero AI插件完整指南:5分钟打造你的智能文献助手
  • 别再手动配环境了!用CMake+VS2022一键搞定PCL点云库(附完整项目模板)
  • ISG Index™显示:人工智能需求提振,亚太地区科技服务市场第一季度实现两位数增长
  • Real-Anime-Z在软件测试中的应用:自动生成UI测试用例配图
  • 民俗文化特色乡村文旅设计案例:行业运营者借鉴方向解析
  • Hyperf 成熟方案的PHP数据清洗、ETL工具链最好的库
  • 低成本改造工业表计接入LoRaWAN:EdgeBus与ThinkLink构建完整物联网闭环
  • 【阿里云/字节/SRE团队内部流出】:Docker 27资源监控9大反模式+3套压测验证脚本(限免72小时)
  • C语言:判断奇偶数
  • 【2026年华为留学生暑期实习-非AI方向(通软嵌软测试算法数据科学)-4月23日-第三题- 奖品采购】(题目+思路+JavaC++Python解析+在线测试)
  • LXC 容器网络无法正常连接问题总结与解决方案
  • HunyuanVideo-Foley问题解决:显存不足、长视频处理等实战技巧分享
  • Taiji架构:轻量级DPU虚拟化与内存管理优化
  • Windows任务栏美化终极指南:用TaskbarX打造Mac风格桌面体验
  • Phi-3.5-mini-instruct惊艳效果:中文技术术语与英文缩写双向精准映射
  • nli-MiniLM2-L6-H768一文详解:如何用该模型替代BERT进行低成本NLI任务迁移
  • RWKV7-1.5B-world双语对话教程:支持中英文社交媒体风格(emoji/缩写/口语化)
  • 从nmtui失败到命令搞定:我的Linux网络排错实战笔记(附虚拟机环境避坑点)
  • LFM2.5-1.2B-Instruct实际作品:8语言词汇表(65536)覆盖度实测报告
  • Java修饰符通关private/protected/public/default+static静态变量方法
  • VnPy量化交易框架实战指南
  • 沃尔玛为何要向200万员工推广AI技能培训
  • 高效城通网盘解析工具:3步获取直连地址,突破下载限制
  • 抖音视频批量下载神器:3分钟学会无痕保存你喜欢的作品
  • 大功率H桥电机驱动板电路设计方案
  • SPSS里没有Dunn‘s test选项?别慌,手把手教你用R插件搞定非参数多重比较
  • 【iOS安全】从Info.plist逆向解析App的URL Scheme与潜在攻击面
  • YOLOv26道路坑洼检测系统:3043张标注图像+PyQt5界面+模型权重+远程环境部署