尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Vue3 + Electron桌面聊天应用:从IPC通信到虚拟滚动与打包实践

发布时间:2026/9/16 21:16:19

资讯中心
01
ARTICLE

Vue3 + Electron桌面聊天应用:从IPC通信到虚拟滚动与打包实践

Vue3 + Electron桌面聊天应用:从IPC通信到虚拟滚动与打包实践
简介基于最新Vue3与Electron开发的桌面聊天示例工程仿微信/QQ交互形态适合正在自学Vue3生态、Electron跨端打包或桌面应用搭建的中级前端开发者。项目综合使用Vue3.x、Electron、Ant Design Vueantdv、v3Layer与v3Scroll覆盖渲染进程、主进程与原生窗口交互的常见写法。压缩包共26个文件包含10个JavaScript逻辑文件、4个PNG与4个ICO图标资源、3个JSON配置以及exe/dll等构建产物整体仅622KB便于快速下载和对照学习工程配置与静态资源也较为完整。已有430人学习适合作为短小精悍的实战范例。资源内实现了图文表情消息、图片视频弹窗预览、链接预览、拖拽粘贴截图发送图片、红包与朋友圈等主要功能并保留components、views、store、mock、plugins、layouts、router等清晰目录读者可借此拆解Vue3单页应用与Electron的结合方式、消息流设计与弹层组件封装亦可作为二次开发的起点。1. 仿微信 QQ 聊天实例Vue3 Electron 桌面端项目的真实复杂度很多人以为 Electron 聊天工具就是套一个 Web 页面难点全在 UI 还原上。实际拆过这类项目会发现真正花时间的不是气泡样式而是主进程和渲染进程之间的职责划分。这个基于最新 Vue3 Electron 开发的仿微信 QQ 桌面端实例把 vue3.x、Electron、antdv、v3Layer、v3Scroll 串在了一条链路上覆盖了图文表情消息、图片视频弹窗预览、链接预览、拖拽粘贴截图发送、红包和朋友圈这些高频功能。对正在做桌面端聊天、IM 架构选型或者想了解 Electron 工程化落地的人来说它最有价值的地方不是界面像不像而是消息流、预览层、截图链路和打包配置这些模块之间如何协作。2. 工程结构与主进程、渲染进程的 IPC 边界设计2.1 dist_electron 与 src 分层Electron 项目的目录规划拿到这个实例先看目录dist_electron是打包输出目录src下分了components、mock、plugins、store、router、layouts、views等模块。这种分层和纯 Vue 项目最大的差异在于Electron 应用有两个进程上下文src里的代码最终会有一部分跑在 Node 环境主进程一部分跑在 Chromium 环境渲染进程。实际开发中我一般这样划分职责主进程main窗口创建、系统托盘、截图工具、文件对话框、IPC 通信枢纽渲染进程rendererVue 组件、状态管理、消息渲染、交互逻辑preload 脚本通过 contextBridge 暴露白名单 API避免直接开 nodeIntegration这个项目里static目录下的tray.ico、tray-empty.ico、icon-online.png、icon-busy.png等图标文件说明它实现了完整的在线状态切换和托盘菜单逻辑。托盘是桌面端聊天工具一个容易被忽略但很重要的入口关闭窗口时应用不能退出得收缩到托盘继续收消息。2.2 contextBridge 与 ipcRenderer安全暴露 API 的推荐实践Electron 高版本默认nodeIntegration: false渲染进程拿不到 Node API必须通过 preload 脚本桥接。这个项目如果用了较新的 Electron 版本通信链路一般是这样的// preload.js const { contextBridge, ipcRenderer } require(electron) contextBridge.exposeInMainWorld(desktopAPI, { sendMessage: (payload) ipcRenderer.invoke(chat:send, payload), onMessage: (callback) { const handler (_event, data) callback(data) ipcRenderer.on(chat:receive, handler) return () ipcRenderer.removeListener(chat:receive, handler) }, openExternal: (url) ipcRenderer.invoke(shell:openExternal, url) })渲染进程里这样调用// 渲染进程中 const api window.desktopAPI // 发送消息 api.sendMessage({ type: text, content: hello, timestamp: Date.now() }) // 订阅消息注意返回的清理函数要绑定到 onUnmounted const unsubscribe api.onMessage((data) { messageList.value.push(data) })关键点说明ipcRenderer.invoke与ipcMain.handle配对使用返回 Promise适合「渲染进程发起主进程处理返结果」这种请求响应模式onMessage返回的清理函数必须在组件卸载时执行否则消息订阅会泄漏切页面后旧的 handler 还在收消息很常见的内存泄漏源头不要在 preload 里暴露整个 ipcRenderer只暴露白名单方法攻击面会小很多2.3 主进程窗口管理与系统托盘实现窗口创建逻辑里有一个细节很多人会忽略聊天工具窗口关闭时需要阻止默认退出行为改成隐藏窗口。主进程里一般这样处理// main.js const { app, BrowserWindow, Tray, Menu, nativeImage } require(electron) let mainWindow null let tray null function createWindow() { mainWindow new BrowserWindow({ width: 1100, height: 760, minWidth: 940, minHeight: 600, frame: true, webPreferences: { preload: path.join(__dirname, preload.js), contextIsolation: true, nodeIntegration: false } }) mainWindow.loadFile(path.join(__dirname, dist_electron/index.html)) } app.whenReady().then(() { createWindow() // 图标直接决定托盘菜单渲染效果16x16 的 icon 在高分屏会糊 tray new Tray(nativeImage.createFromPath(path.join(__dirname, static/tray.ico))) const contextMenu Menu.buildFromTemplate([ { label: 显示主界面, click: () mainWindow.show() }, { label: 退出, click: () { app.quit() } } ]) tray.setToolTip(Vue3 Electron Chat) tray.setContextMenu(contextMenu) }) mainWindow.on(close, (e) { if (!app.isQuiting) { e.preventDefault() mainWindow.hide() } })参数说明contextIsolation: true必须开启它把 preload 和页面脚本隔离在各自上下文避免页面脚本直接操作 Node 环境。frame: false可以做出微信那种无边框窗口但需要自己实现拖拽区域用 CSS-webkit-app-region: drag指定可拖拽区域。这个项目保留了系统边框更稳重适合作为学习基线。3. 消息列表与 v3Scroll 滚动加载的工程实现3.1 消息数据结构与 store 里怎么组织会话聊天应用的核心不是 UI 组件而是消息的数据结构设计。这个项目用store管理会话状态合理的消息对象至少要包含这些字段// store/modules/chat.js export const useChatStore defineStore(chat, { state: () ({ conversations: [], currentConversationId: null, messages: [], pageSize: 20, hasMore: true }), actions: { async loadHistory(conversationId, beforeTimestamp) { // mock 数据可以模拟分页 const res await fetch(/api/messages?convId${conversationId}before${beforeTimestamp}limit20) const list await res.json() return list }, appendMessage(msg) { this.messages.push(msg) }, prependMessages(olderMsgs) { this.messages.unshift(...olderMsgs) } } })值得注意的一个设计点是向上翻历史消息时用unshift往数组头部插入但滚动位置要保持住。很多新手直接 append 进数组导致列表从头渲染翻历史消息时滚轮位置会跳动体验很差。正确的做法是先记录当前列表的总高度和滚动容器高度插入旧消息后补偿滚动偏移。3.2 v3Scroll 在长列表场景下的关键作用这个项目引入 v3Scroll 组件就是为了解决长消息列表的性能问题。v3Scroll 是 Vue3 的虚拟滚动/增强滚动组件它替代的是vue-virtual-scroller在 Vue2 生态里的位置。我做过的聊天项目里直接渲染 500 条以上 DOM 节点时输入框已经开始感觉到明显卡顿滚轮事件处理也会掉帧。虚拟滚动的核心思路是只渲染可视区域内的消息节点及其上下缓冲区配合itemHeight预估实现template v3-scroll refscrollRef :datamessages :heightscrollHeight :item-height60 load-start loadloadMoreHistory template #default{ item } MessageItem :dataitem / /template /v3-scroll /template关键参数我一般这样调参数建议值说明item-height60-80消息项预估高度头像 40px 上下 padding 20px实际按内容类型拆分子组件buffer-size5-10可视区上下各预渲染多少条设置 0 会出现快速滚动白屏load-starttrue初始化时先加载第一屏避免空列表容器高度为 0如果消息里有图片固定高度会失效。需要拿到图片宽高后动态计算该项高度或者用懒加载占位图撑住高度。v3Scroll 支持高度异动的列表但代价是滚动时要做高度重算性能比固定高度差不少。我一般按文本、图片、混合消息拆行给图片类消息单独估高度。3.3 拖拽、粘贴、截图的图片上传链路图片发送是这个实例里功能覆盖最全的一条链路包含三条数据来源第一条是拖拽上传。渲染进程监听dragover和drop事件从DataTransfer对象里取文件过滤掉非图片后缀// components/ChatInput/hooks/useDragDrop.js export function useDragDrop(onImageFile) { const handleDrop (e) { e.preventDefault() const files Array.from(e.dataTransfer.files) const imageFiles files.filter((f) f.type.startsWith(image/)) if (imageFiles.length 0) return // 这里注意如果拖进来的是文件系统路径主进程需要做安全校验 imageFiles.forEach((file) { const url URL.createObjectURL(file) onImageFile({ url, file, preview: url }) }) } return { handleDrop } }第二条是粘贴上传。从剪贴板读到的图片通常不是 File 对象而是 DataURL 格式的 base64 字符串document.addEventListener(paste, async (e) { const items Array.from(e.clipboardData?.items || []) for (const item of items) { if (item.type.startsWith(image/)) { const file item.getAsFile() // 直接塞进消息列表时用 FileReader 转 dataURL 会造成主线程阻塞 // 更好的方式用 createObjectURL 生成短地址 const previewUrl URL.createObjectURL(file) } } })第三条是截图上传。这个功能是纯 Electron 特性需要主进程调用desktopCapturer抓取屏幕再渲染一个全屏遮罩层让用户框选区域。核心流程是主进程监听截图快捷键或托盘菜单调用desktopCapturer.getSources({ types: [screen] })获取屏幕缩略图把截图画面传给渲染进程的全屏组件用 canvas 划定选区选区确认后生成 base64 图片直接作为消息内容发送// main/screenshot.js const { desktopCapturer, ipcMain, BrowserWindow } require(electron) ipcMain.handle(screenshot:capture, async () { const sources await desktopCapturer.getSources({ types: [screen], thumbnailSize: { width: 1920, height: 1080 } }) // 多显示器场景遍历 sources把 thumbnail.toDataURL() 传给渲染进程 return sources.map((src) ({ id: src.id, name: src.name, thumbnail: src.thumbnail.toDataURL() })) })主进程拿到屏幕图像传回渲染进程后渲染进程用 canvas 叠加半透明遮罩、绘制选框鼠标拖动时实时更新选区坐标。这个模块最耗时的是框选交互不是截图本身。注意thumbnailSize如果设太小高分屏下截图会发虚设太大传输耗时和内存开销都会上涨。3.4 消息类型拆分与渲染策略图文表情消息、图片、视频、红包、朋友圈在同一个消息流里渲染组件不能只靠v-if堆条件。这个项目可以按 type 字段拆分独立组件规则如下// components/MessageItem.vue 的渲染片段 template div classmessage-item :classmessage.self ? self : other Avatar :srcmessage.avatar :statusmessage.userStatus / div classbubble template v-ifmessage.type text TextMessage :contentmessage.content :emojismessage.emojis / /template template v-else-ifmessage.type image ImageMessage :urlmessage.thumb :originmessage.origin / /template template v-else-ifmessage.type video VideoMessage :postermessage.cover :srcmessage.videoUrl / /template template v-else-ifmessage.type redpacket RedPacket :amountmessage.amount :greetingmessage.greeting / /template /div /div /template组件拆分后video类型要关注一个细节消息列表里的视频不能直接用video标签加载全量视频要有封面图poster属性点击后再加载真实视频地址。否则长列表滚动时所有视频同时请求加载网络和内存都会爆炸。4. 预览层与 antdv、v3Layer 的弹窗体系设计4.1 v3Layer 在图片视频预览中的角色v3Layer 是一个类 layui layer 风格的弹窗组件库封装了 layer 的 API 形式到 Vue3 组件化开发里。图片和视频的弹窗预览在这个项目里通过 v3Layer 打开一个新的弹出层而不是跳路由。路由跳转的问题在于聊天应用的预览是瞬态操作用户期望 ESC 关闭后还停留在原来的聊天记录滚动位置。v3Layer 的调用方式类似这样import { layer } from v3layer const previewImage (images, index) { layer.open({ type: 1, title: false, closeBtn: 1, shadeClose: true, area: [80vw, 80vh], content: img src${images[index].origin} stylemax-width:100%;height:auto;object-fit:contain; / }) }但实际开发里直接拼 HTML 字符串不够灵活我用的是 v3Layer 的组件渲染模式content 可以传一个 Vue 组件让预览层内部去处理图片轮播、缩放比例、视频播放和关闭事件。贴一个组件结合方案!-- components/PreviewLayer.vue -- script setup import { layer } from v3layer import { h } from vue const props defineProps({ list: Array, initialIndex: Number }) const open () { layer.open({ type: 1, title: 预览, area: [860px, 600px], content: () h(MediaPreview, { list: props.list, index: props.initialIndex, // 关闭前清理播放状态 onBeforeClose: () stopPlay() }) }) } /scriptv3Layer 的area参数支持百分比或像素聊天场景里预览层宽度设min(90vw, 1000px)更合适太宽在大屏上会失去焦点感太小又看不清图片细节。4.2 链接预览的实现服务端抽 meta 还是客户端本地解析链接预览是聊天应用中一个隐蔽但很吃细节的功能。用户发出来一个淘宝链接、掘金文章链接或 bilibili 视频链接消息里要展示一个小卡片标题 描述 缩略图。纯前端实现这个功能跨域会卡死因为目标站点的 HTML 没有 CORS 头。可选方案有三个第一个方案是项目后端做一个 proxy 接口从服务端抓取 URL 内容解析 meta 标签// 服务端伪代码 app.get(/api/link-preview, async (req, res) { const target req.query.url const html await fetch(target, { headers: { user-agent: UA } }) const $ cheerio.load(html) res.json({ title: $(meta[propertyog:title]).attr(content) || $(title).text(), description: $(meta[propertyog:description]).attr(content) || , image: $(meta[propertyog:image]).attr(content), url: target }) })第二个方案是 Electron 主进程直接 fetch不走渲染进程的跨域限制。因为主进程在 Node 环境没有跨域概念。这个项目如果用 Electron 的 ipcMain 去做 fetch可以省掉后端依赖但要注意 URL 的协议过滤防止 SSRF。第三个方案是直接用现成服务比如unfurl.js、microlink之类的库但要注意外部服务有请求频率限制。og:image有个很隐蔽的问题有些站点返回的是相对路径必须要拼上目标页面的 origin 才能用否则预览图会 404。4.3 红包与朋友圈的组件化思路红包组件看起来是一个弹窗实际上它是「消息流内联组件 弹窗浮层」的组合。消息流里渲染一个红色气泡点击后打开一个领取弹窗领取后气泡变成已领取状态消息列表里不能直接改 prop 的状态需要触发 store 的updateMessageaction。红包的 data 结构可以这样设计{ type: redpacket, content: { greeting: 恭喜发财, amount: 0.88, total: 1, received: 0, status: pending // pending | received | expired } }朋友圈呢在这个项目里更像一个独立的页面模块不在 chat 主流程里。Electron 这里可以用多窗口方案主窗口里点朋友圈入口通过 IPC 让主进程新建一个 BrowserWindow加载朋友圈的 URL。多窗口的好处是朋友圈的滚动、图片预览和聊天框互不影响坏处是内存占用多一个渲染进程。我见过团队最初做单窗口内嵌朋友圈后来发现聊天列表和朋友圈的滚动互斥问题太麻烦又改回多窗口。朋友圈图片九宫格在前端有一个常用技巧三张图时左边大图占 2 列宽右边一张竖图其余以 3xN 的网格排列。这个样式用 CSS Grid 实现最省力.album-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; } .album-grid .is-single { max-width: 200px; }5. Electron 打包配置与桌面端实践的坑位盘点5.1 vue.config.js 与 electron-builder 的联动package.json里能打dist_electron目录说明这个项目用了 electron-builder 作为打包方案。Vue3 Electron 项目里有一个容易出问题的点vue.config.js的publicPath必须设成./否则打包出来的 HTML 引用 JS 资源是绝对路径Electron 的loadFile加载本地文件时找不到资源白屏没商量。// vue.config.js module.exports { publicPath: ./, outputDir: dist_electron, productionSourceMap: false, devServer: { port: 8080, hot: true } }outputDir直接指定dist_electron在这里是有意为之让 electron-builder 的files配置直接包含它不需要二次拷贝。electron-builder 的配置文件appId: com.example.chat productName: Vue3Chat directories: output: release buildResources: build files: - dist_electron/** - package.json win: target: - nsis nsis: oneClick: false allowToChangeInstallationDirectory: true shortcutName: Vue3Chat5.2 pnpm 配置 electron 打包的坑热词里频繁出现 pnpm 配置 electron 打包这里值得单列。pnpm 的符号链接机制和 Electron 天然有冲突Electron 打包时需要把node_modules里的依赖物理拷贝进 asar 包pnpm 默认用 symlink 指向全局 storeelectron-builder 处理不了符号链接经常报node_modules not found之类的错误。我一般用.npmrc做兼容shamefully-hoisttrue node-linkerhoistednode-linkerhoisted让 pnpm 像 npm 一样把所有依赖平铺在 node_modules 下等于是牺牲 pnpm 的部分空间优势换兼容性。如果不想改全局配置也可以在.npmrc里加electron_mirrorhttps://npmmirror.com/mirrors/electron/加速 Electron 二进制下载国内网络拉 electron 的二进制经常卡在 postinstall 阶段。5.3 主进程白屏与内存泄漏排查Electron 桌面聊天端最常见的故障是白屏。白屏分三种各自的排查路径完全不同第一种是加载路径错loadFile路径写错开发环境正常打包后白屏。在 main 进程里打印path.join(__dirname, dist_electron/index.html)是否存在即可确认。第二种是 publicPath 错HTML 能打开但 JS 404。打开 devtools 看 network 标签如果 script src 是file:///js/app.js少了./调整 vue.config.js 的 publicPath。第三种是渲染进程 JS 报错比如在 Vue3 里用了浏览器不支持的 API。Electron 内置 Chromium 版本通常较新这个坑不多但若遇到 SSRF 或蓝牙等 API 需要主进程配合。内存泄漏方面聊天场景有两个高频泄漏源我几乎每次都遇到。一个是有setInterval的长轮询没有被清理窗口隐藏后定时器仍在执行Electron 应用用一段时间后内存涨到 1G 很常见。另一个是消息订阅没有退订前面 preload 里写的onMessage要在组件onUnmounted时调用清理函数。5.4 编译链路验证从源码到桌面应用最后梳理一遍可复现的流程# 1. 安装依赖Electron 二进制下载慢时换 npmmirror npm install --registryhttps://registry.npmmirror.com # 2. 构建渲染进程资源 npm run build # 3. 启动 electron 加载 dist_electron 目录 npx electron .如果要调试主进程代码推荐用electron --inspect5858配合 Chrome devtools 的 node 调试协议。单独调试渲染进程时在mainWindow.webContents.openDevTools({ mode: detach })可以打开独立的调试窗口不会污染业务界面。截图功能涉及desktopCapturer时Electron 的getSources会弹系统权限询问框。在 macOS 上如果用户拒绝过一次屏幕录制权限必须去系统设置里手动打开这是 Electron 截图工具常见的「现象是截图为黑屏」的根因。红包和朋友圈这类业务如果流量大了之后要接入真实后端可以保留 mock 目录里的数据格式把fetch路径替换成真实接口mock 数据的价值在于联调阶段不需要起后端服务。到生产阶段mock 模块和 store 之间的边界应该做到切换数据源只改一个配置变量模块内部不需要感知请求来源。这个项目把mock独立成目录就是这个用意它是「数据层可插拔」的信号也是评论 React 和 Vue 项目架构设计水平时一个值得关注的细节。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。