前言
Tauri写桌面端应用太香了,我用Tauri仿写了微信,因为很多API都可以用JavaScript来写,所以我的整个项目95%都是用JavaScript来写各种API的,对前端同学非常友好。

技术栈
- vue3全家桶(vue-router、pinia)
Naive UI:写界面的UI库- rust:写底层交互,占比极少
JavaScript:写Tauri的API,占比大- 其他工具类库......

资源占用
不得不说,Tauri的资源占用真的好,我引入了好几百kb的图片文件,Naive UI全部引入,还有很多svg图片,就是这样的情况下,打包后的安装包也才2.57M,我当时就惊了!
- 安装包:2.57M
- 直接运行包:10.1M
- 内存占用:60M(2个窗口)

细节怪
写桌面端的都是细节怪,如果是单一窗口也还好,可以像写网页一样一直写下去......
但是桌面端还要考虑多窗口,托盘图标、托盘菜单、窗口通信、底层权限、原生通知、进程管理等,随便拿出一个来都够研究半天的了。
我为了高度还原桌面微信,也是把细节拉满了
- 创建窗口,如果窗口已存在、则聚焦窗口使其显示,而不是打印错误提示、这样看不到效果的,也不能提示"窗口已存在,请勿重复创建吧",这样体验感直线下降!创建窗口分3种情况:
- 窗口已存在,且为最小化状态的,则取消窗口最小化;
- 窗口已存在,且为显示状态,但是在其他应用的窗口下面,则聚焦显示到前台;
- 窗口不存在,才执行创建窗口的方法
- 自定义托盘菜单、及菜单窗口(默认隐藏)
- 窗口失去焦点时,则隐藏托盘菜单窗口
- 鼠标右击、精确计算显示位置
- 鼠标左击,显示主聊天窗口
- 自定义按钮功能,可放任意内容,脱离原生托盘菜单的限制
- 高度前端化,很多API都是用JavaScript编写,只有少数用rust编写,且注释清晰;
- 自定义最大化、最小化、关闭、置顶菜单栏,且组件化;
- 自定义可拖拽标题栏,组件化;
- 多处调用原生API,带有演示作用;

已完成模块
- 基础架构布局
- 聊天(含右键菜单,发送消息窗口、split栏未优化,截图功能、聊天记录等按钮功能未模拟)
- 通讯录(只模拟了UI界面、逻辑跳转等)
- 收藏(只模拟了UI界面)
- 设置(退出登录、选择存储位置等功能已模拟)
- 其他窗口:朋友圈、搜一搜、小程序、表情窗口等
- 登录、退出登录功能已模拟,主要包含跨窗口通信问题
- 自定义托盘菜单
- 自定义最大化、最小化、置顶菜单栏
- 自定义可拖拽标题栏
- 原生消息通知演示功能
- 使用默认浏览器打开链接
- 退出程序
- 自定义应用图标配置

待优化/待开发
- 聊天界面:输入框样式优化,拖动窗格分割面板
- 表情包窗口:显示位置优化
- 其他有些内容,没给点击提示
- 滚动条可以再美化一些
- 其他还有很多空白窗口,暂时只放了空的内容
- 聊天列表,拖动独立聊天窗口
- 本地数据持久化,配置项持久化等
- 开机自启动
- 截图功能
- 便利贴功能
- naive ui改为按需引入,目前为了开发方便,暂时全局引入的
- 启动画面

小小心得
其实,大部分的API都可以用纯前端JavaScript的方式来完成,但是有些内容还是要用rust写的,尤其是需要用到跨窗口状态的。
登录
比如登录窗口,登陆成功后一般是在pinia中设置一个变量isLogin默认值为false,然后把isLogin改为true,然后其他地方引用isLogin,但是如果是跨窗口的话,你只有在登录窗口的isLogin值为true,其他窗口的isLogin值还是默认的false。
同样地,你也不能用localStorage等方法,虽然这会在跨窗口中获取同样的缓存值,但是当应用退出程序后,再次打开应用时的isLogin还是缓存的true值,但是默认打开的是登录页,你可能会说,我在退出应用的时候清空缓存值不就好了吗,但是有时候用户不是通过退出按钮来退出的,有的直接关电脑或断电了呢,那么你该怎么监听?
所以说,还是得用到rust,直接在rust全局中记录登录的状态值,前端页面直接invoke这个变量即可,不管你是哪个窗口获取的值都一样,下次重启应用时,rust中初始化的isLogin值又变为false了,这就很符合我们的预期了。
托盘
未登录不显示托盘,已登录才显示托盘。
这是一个很常见的需求,如果你没有跨窗口的说法,那么用纯JavaScript写也是OK的,但是如果你有跨窗口的情况,那么就需要用到rust了,因为托盘用到的是一个实例,你在主窗口main显示时创建托盘,托盘实例为trayInstance,你在退出登录后,销毁主窗口main,创建登录窗口login,但是只要你销毁主窗口main,那么托盘实例trayInstance也就没有,也就无法销毁了。
可能你会说,我在销毁主窗口main之前的trayInstance实例还在的,我先销毁trayInstance实例不就行了吗?你可以试试看......
