Vue3.0+Electron聊天室|electron跨平台仿QQ客户端|vue3.x聊天应用
基于vue3+electron11跨端仿制QQ桌面应用实战Vue3ElectronQchat。
使用vue3+electron+vuex4+ant-design-vue+v3scroll+v3layer等技术构建跨平台模仿QQ|TIM界面聊天应用。实现了发送富文本消息、图片/视频/链接预览、拖拽发送图片、调用dll截图、朋友圈等功能。支持多开窗口|父子modal窗口、换肤等操作。
一、技术栈
- 编码工具:vscode
- 框架技术:vue3.0+electron11.2.3+vuex4+vue-router@4
- 组件库:ant-design-vue^2.0.0 (蚂蚁金服pc端vue3组件库)
- 打包工具:vue-cli-plugin-electron-builder
- 按需引入:babel-plugin-import^1.13.3
- 弹窗组件:v3layer(基于vue3封装的自定义对话框组件)
- 滚动条组件:v3scroll(基于vue3封装的美化系统滚动条)
- 矢量图标:阿里iconfont字体图标库
二、目录结构
◆ 一睹效果
◆ electron实现新开窗口/多窗体并存
项目支持新开多个窗口,只需调用公共createWin方法并传入配置参数即可快速新开一个窗口。
// 关于窗口 const handleAboutWin = () => { createWin({ title: '关于', route: '/about', width: 380, height: 280, resize: false, parent: winCfg.window.id, modal: true, }) } // 换肤窗口 const handleSkinWin = () => { createWin({ title: '换肤', route: '/skin', width: 720, height: 475, resize: false, }) } // 朋友圈窗口 const handleFZoneWin = () => { createWin({ title: '朋友圈', route: '/fzone', width: 550, height: 700, resize: false, }) } // 界面管理器窗口 const handleUIManager = () => { createWin({ title: '界面管理器', route: '/uimanager', width: 400, height: 475, resize: false, parent: winCfg.window.id, modal: true, }) }
支持如下参数配置:
// 窗口参数配置 export const winConfig = { id: null, // 窗口唯一id background: '#fff', // 背景色 route: '', // 路由地址url title: '', // 标题 data: null, // 传入数据参数 width: '', // 窗口宽度 height: '', // 窗口高度 minWidth: '', // 窗口最小宽度 minHeight: '', // 窗口最小高度 x: '', // 窗口相对于屏幕左侧坐标 y: '', // 窗口相对于屏幕顶端坐标 resize: true, // 是否支持缩放 maximize: false, // 最大化窗口 isMultiWin: false, // 是否支持多开窗口(为true则会支持创建多个窗口) isMainWin: false, // 是否主窗口(为true则会替代之前主窗口) parent: '', // 父窗口(传入父窗口id) modal: false, // 模态窗口(需设置parent和modal选项) alwaysOnTop: false, // 是否置顶窗口 }
至于如何使用vue3+electron搭建项目和创建多窗口,之前有过一篇分享文章,大家可以去看看。
https://www.cnblogs.com/xiaoyan2017/p/14403820.html
◆ electron自定义无边框窗体拖拽/禁用系统右键菜单
项目采用无边框模式 frame: false 如上图所示区域是自定义拖拽导航栏。支持传入标题、标题颜色/背景色、是否透明背景等功能。
但有一个问题,设置 -webkit-app-region: drag 之后,点击鼠标右键会弹出系统菜单,总感觉是一种伪拖拽效果,可通过如下方法在创建窗体的时候暂时给屏蔽掉。
// 屏蔽系统右键菜单 win.hookWindowMessage(278, () => { win.setEnabled(false) setTimeout(() => { win.setEnabled(true) }, 100) return true })
至于如何自定义导航条及最大化/最小化/关闭按钮,之前也有过一篇分享文章,大家感兴趣可以去看一看。
https://www.cnblogs.com/xiaoyan2017/p/14449570.html
◆ electron实现QQ托盘图标/闪烁
关闭主窗体的时候,会提示直接关闭软件还是最小化到系统托盘。
// 关闭窗口 const handleWinClose = () => { if(winCfg.window.isMainWin) { let $el = v3layer({ type: 'android', content: '是否最小化至托盘,不退出程序?', btns: [ { text: '残忍退出', style: 'color:#ff5438', click: () => { $el.close() store.commit('LOGOUT') setWin('close') } }, { text: '最小化至托盘', style: 'color:#00d2ff', click: () => { $el.close() win.hide() } } ] }) }else { setWin('close', winCfg.window.id) } }
由于项目支持打开多个窗口,在关闭的时候需要判断是否是主窗口,如果不是则直接传入该窗口id进行关闭,如果是主窗口,则会出现弹窗提示。
// 创建系统托盘图标 let tray = null let flashTimer = null let trayIco1 = path.join(__dirname, '../static/tray.ico') let trayIco2 = path.join(__dirname, '../static/tray-empty.ico') createTray() { const trayMenu = Menu.buildFromTemplate([ { label: '我在线上', icon: path.join(__dirname, '../static/icon-online.png'), click: () => {...} }, { label: '忙碌', icon: path.join(__dirname, '../static/icon-busy.png'), click: () => {...} }, { label: '隐身', icon: path.join(__dirname, '../static/icon-invisible.png'), click: () => {...} }, { label: '离开', icon: path.join(__dirname, '../static/icon-offline.png'), click: () => {...} }, {type: 'separator'}, { label: '关闭所有声音', click: () => {...}, }, { label: '关闭头像闪动', click: () => { this.flashTray(false) } }, {type: 'separator'}, { label: '打开主窗口', click: () => { try { for(let i in this.winLs) { let win = this.getWin(i) if(!win) return if(win.isMinimized()) win.restore() win.show() } } catch (error) { console.log(error) } } }, { label: '退出', click: () => { try { for(let i in this.winLs) { let win = this.getWin(i) if(win) win.webContents.send('win-logout') } app.quit() } catch (error) { console.log(error) } } }, ]) this.tray = new Tray(this.trayIco1) this.tray.setContextMenu(trayMenu) this.tray.setToolTip(app.name) this.tray.on('double-click', () => { // ... }) } // 托盘图标闪烁 flashTray(flash) { let hasIco = false if(flash) { if(this.flashTimer) return this.flashTimer = setInterval(() => { this.tray.setImage(hasIco ? this.trayIco1 : this.trayIco2) hasIco = !hasIco }, 500) }else { if(this.flashTimer) { clearInterval(this.flashTimer) this.flashTimer = null } this.tray.setImage(this.trayIco1) } } // 销毁托盘图标 destoryTray() { this.flashTray(false) this.tray.destroy() this.tray = null }
大家需要准备两个大小一样的ico图标,其中一个透明即可。通过定时器控制 setImage() 函数来显示ico图标,达到闪烁效果。
通过调用 flashTray(true|false) 方法来开启/停止托盘闪烁。
注意:图标路径如果不正确,则无法显示托盘图标,大家可以 console.log(__dirname) 来查看路径。默认是输出dist_electron目录。
◆ vue3.0全局对话框/虚拟滚动条组件
大家看到的项目中有一些弹窗是使用vue3自定义组件来实现的。另外项目中的滚动条也是使用vue3自定义封装替代系统滚动条。
之前也有过两篇相关的技术分享,这里就不详细介绍了,大家感兴趣也可以去看看哈。
https://www.cnblogs.com/xiaoyan2017/p/14221729.html
https://www.cnblogs.com/xiaoyan2017/p/14242983.html
◆ vue3.x+electron项目配置/打包配置
项目搭建之后,根目录会有一个vue.config.js项目配置文件。可进行一些简单的环境/插件配置,另外electron-builder打包参数也可以在里面进行配置。
/** * @Desc vue3项目配置文件 * @Time andy by 2021-02 * @About Q:282310962 wx:xy190310 */ const path = require('path') module.exports = { // 基本路径 // publicPath: '/', // 输出文件目录 // outputDir: 'dist', // assetsDir: '', // 环境配置 devServer: { // host: 'localhost', // port: 8080, // 是否开启https https: false, // 编译完是否打开网页 open: false, // 代理配置 // proxy: { // '^/api': { // target: '<url>', // ws: true, // changeOrigin: true // }, // '^/foo': { // target: '<other_url>' // } // } }, // webpack配置 chainWebpack: config => { // 配置路径别名 config.resolve.alias .set('@', path.join(__dirname, 'src')) .set('@assets', path.join(__dirname, 'src/assets')) .set('@components', path.join(__dirname, 'src/components')) .set('@module', path.join(__dirname, 'src/module')) .set('@plugins', path.join(__dirname, 'src/plugins')) .set('@layouts', path.join(__dirname, 'src/layouts')) .set('@views', path.join(__dirname, 'src/views')) }, // 插件配置 pluginOptions: { electronBuilder: { // 配置后可以在渲染进程使用ipcRenderer nodeIntegration: true, // 项目打包参数配置 builderOptions: { "productName": "electron-qchat", //项目名称 打包生成exe的前缀名 "appId": "com.example.electronqchat", //包名 "compression": "maximum", //store|normal|maximum 打包压缩情况(store速度较快) "artifactName": "${productName}-${version}-${platform}-${arch}.${ext}", //打包后安装包名称 // "directories": { // "output": "build", //输出文件夹(默认dist_electron) // }, "asar": false, //asar打包 // 拷贝静态资源目录到指定位置 "extraResources": [ { "from": "./static", "to": "static" }, ], "nsis": { "oneClick": false, //一键安装 "allowToChangeInstallationDirectory": true, //允许修改安装目录 "perMachine": true, //是否开启安装时权限设置(此电脑或当前用户) "artifactName": "${productName}-${version}-${platform}-${arch}-setup.${ext}", //打包后安装包名称 "deleteAppDataOnUninstall": true, //卸载时删除数据 "createDesktopShortcut": true, //创建桌面图标 "createStartMenuShortcut": true, //创建开始菜单图标 "shortcutName": "ElectronQChat", //桌面快捷键图标名称 }, "win": { "icon": "./static/shortcut.ico", //图标路径 } } } } }
◆ ant-design-vue组件库按需引入
有时候项目开发,需要用到一些组件库里面的个别功能,这时按需引入最适合,会减少打包后的大小。
安装按需引入插件 npm i babel-plugin-import -D 并在babel.config.js里面进行相关引入配置。
module.exports = { presets: [ '@vue/cli-plugin-babel/preset' ], // 按需引入第三方插件 plugins: [ [ 'import', { 'libraryName': 'ant-design-vue', 'libraryDirectory': 'es', 'style': 'css', } ] ] }
这时候就可以按需引入ant-design-vue组件了,样式文件会自动引入的。
// 按需引入ant-design-vue组件库 import { Button, message, Tabs, Checkbox, Image, Upload } from 'ant-design-vue' // ... const Plugins = (app) => { app.use(Button) app.use(Tabs) app.use(Checkbox) app.use(Image) app.use(Upload) // ... app.config.globalProperties.$message = message } export default Plugins
◆ electron实现截图功能
项目中的截图功能,本想着自己捣鼓一个,后来时间有限,就使用了一个微信截图dll来实现。可实现基本功能,并且打包后也可以截图。
// 屏幕截图 ipcMain.on('win-capture', () => { console.log('调用微信dll截图...') let printScr = execFile(path.join(__dirname, '../static/screenShot/PrintScr.exe')) printScr.on('exit', (code) => { if(code) { console.log(code) } }) })
如果出现打包后,截图功能失效,需要在vue.config.js的打包配置中添加 extraResources 字段配置。
// 拷贝静态资源目录到指定位置 "extraResources": [ { "from": "./static", "to": "static" }, ],
from表示文件原路径,to表示打包后资源放置的路径。打包之后会在resources目录下有个static目录。
另外还需注意:
1、项目路径不能含有中文,否则打包会出错!
2、尽量不要使用 getCurrentInstance 函数来操作store或router,打包也会出错!
3、在渲染进程,也就是.vue页面,使用ipcRenderer或remote出现如下错误
Uncaught TypeError: fs.existsSync is not a function
则需要配置 nodeIntegration: true 来开启Node环境支持。
OK,基于Vue3+Electron开发桌面端仿Q应用就分享到这里。希望对大家有所帮助哈~~ 👽😎
往期跨平台项目
flutter3-winchat桌面端聊天实例|Flutter3+Dart3+Getx仿微信Exe程序
uniapp+vue3聊天室|uni-app+vite4+uv-ui跨端仿微信app聊天语音/朋友圈
Vite5+Electron聊天室|electron31跨平台仿微信EXE客户端|vue3聊天程序