tauri v2 开源项目学习(二)

前言:

tauri2编程,前端部分和electron差不多,框架部分差别大,资料少,官网乱,AI又骗我
所以在gitee上,寻找tauri v2开源项目,
通过记录框架部分与rust部分的写法,对照确定编程方式
tarui2插件,可以查看:https://github.com/tauri-apps/plugins-workspace


1. EcoPaste

https://gitee.com/ayangweb/EcoPaste
剪切板管理工具

1.1 tauri.conf.json

  • acceptFirstMouse 作用:

控制窗口首次显示时是否接受第一个鼠标点击事件(仅 macOS)

  • skipTaskbar 作用:

是否在任务栏(Windows/Linux)或 Dock(macOS)中显示窗口图标

  • visibleOnAllWorkspaces 作用:

窗口是否在所有虚拟桌面(工作区)中可见(跨工作区显示)。
true:窗口会出现在所有工作区(如全局悬浮工具栏)。
false:仅当前工作区可见(默认)。

  • titleBarStyle 作用:

作用:标题栏样式(仅 macOS)

  • windowEffects requireLiteralLeadingDot

窗口视觉效果(仅 macOS)。
effects:可选值如 “sidebar”(侧边栏模糊效果)、“menu”(菜单栏效果)。
state:效果状态,如 “active”(激活时显示)、“inactive”(非激活时显示)

  • requireLiteralLeadingDot c

文件路径访问是否要求以 . 开头的字面量(防止路径遍历攻击)。
false:允许无 . 的路径(如 …/ 相对路径)。
true:严格限制路径必须以 . 开头(更安全,但灵活性低)。
影响范围:
assetProtocol.scope:控制前端通过 asset: 协议加载文件的路径规则。
fs 插件:控制文件系统 API 的路径检查。

"app": {
		"macOSPrivateApi": true,
		"windows": [
			{
				"label": "main",
				"title": "EcoPaste",
				"url": "index.html/#/",
				...
				"alwaysOnTop": true,
				"acceptFirstMouse": true,
				"skipTaskbar": true,
				"visibleOnAllWorkspaces": true
			},
			{
				"label": "preference",
				"url": "index.html/#/preference",
				...
				"maximizable": false,
				"hiddenTitle": true,
				"skipTaskbar": true,
				"titleBarStyle": "Overlay",
				"dragDropEnabled": false,
				"windowEffects": {
					"effects": ["sidebar"],
					"state": "active"
				}
			}
		],
		"security": {
			"csp": null,
			"dangerousDisableAssetCspModification": true,
			"assetProtocol": {
				"enable": true,
				"scope": {
					"allow": ["**/*"],
					"requireLiteralLeadingDot": false
				}
			}
		}
	},
	...
"plugins": {
		"updater": {
			"pubkey": "***",
			"endpoints": ["https://api.ecopaste.cn/update"]
		},
		"fs": {
			"requireLiteralLeadingDot": false
		}
	}

1.2 cargo.toml

安装的插件

[dependencies]
tauri = { workspace = true, features = ["tray-icon", "protocol-asset", "macos-private-api", "image-ico"] }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
tauri-plugin-shell.workspace = true
tauri-plugin-single-instance = "2"
tauri-plugin-autostart = "2"
tauri-plugin-sql = { version = "2", features = ["sqlite"] }
tauri-plugin-log = "2"
tauri-plugin-global-shortcut = "2"
tauri-plugin-os = "2"
tauri-plugin-dialog = "2"   
tauri-plugin-fs = "2"
tauri-plugin-updater = "2"
tauri-plugin-process = "2"
tauri-plugin-drag = "2"
tauri-plugin-macos-permissions = "2"
tauri-plugin-locale = "2"
tauri-plugin-opener = "2"
tauri-plugin-prevent-default = "1"
tauri-plugin-fs-pro.workspace = true
tauri-plugin-eco-window.workspace = true
tauri-plugin-eco-clipboard.workspace = true
tauri-plugin-eco-ocr.workspace = true
tauri-plugin-eco-paste.workspace = true
tauri-plugin-eco-autostart.workspace = true

1.3 App.tsx

获得本地窗口

import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
....
// 监听显示窗口的事件
useTauriListen(LISTEN_KEY.SHOW_WINDOW, ({ payload }) => {
	const appWindow = getCurrentWebviewWindow();

	if (appWindow.label !== payload) return;

	showWindow();
});

1.4 src\plugins\window.ts

通过window插件,对webviewWindow

import {
	LogicalPosition,
	LogicalSize,
	currentMonitor,
} from "@tauri-apps/api/window";
...
	await appWindow.setSize(new LogicalSize(width, windowHeight));
	await appWindow.setPosition(new LogicalPosition(x, y));

此项目前端是react编写,有很多关于插件的应用


2. Redis-Gui

https://gitee.com/hepengju/redis-gui
关于redis的rust实现

关于redis的做法参考,框架上没有什么可参考处

#[cfg(test)]
mod tests {
    use redis::cluster::ClusterClient;
    use redis::cluster_routing::RoutingInfo::MultiNode;
    use redis::cluster_routing::{MultipleNodeRoutingInfo, ResponsePolicy};
    use redis::{cluster, Commands, RedisResult, ScanOptions};
    use MultipleNodeRoutingInfo::AllMasters;
    use ResponsePolicy::AllSucceeded;

    // 获取连接
    fn get_conn() -> RedisResult<redis::Connection> {
        let client = redis::Client::open("redis://:[email protected]:6379")?;
        client.get_connection()
    }

    #[test]
    fn info() -> RedisResult<()> {
        let mut conn = get_conn()?;
        let info: String = redis::cmd("info").query(&mut conn)?;
        println!("Redis Info: {}", info);
        Ok(())
    }

    #[test]
    fn get_set() -> RedisResult<()> {
        let mut conn = get_conn()?;
        // 低级别api
        let ack: String = redis::cmd("set").arg("rust:low:api").arg("低级别api").query(&mut conn)?;
        println!("Redis Ack: {}", ack);
        let ack: String = redis::cmd("get").arg("rust:low:api").query(&mut conn)?;
        println!("Redis Ack: {}", ack);

        // 高级别api
        let ack: String = conn.set("rust:high:api", "高级别api")?;
        println!("Redis Ack: {}", ack);
        let ack: String = conn.get("rust:high:api")?;
        println!("Redis Ack: {}", ack);
        Ok(())
    }

    #[test]
    fn scan() -> RedisResult<()> {
        let mut conn = get_conn()?;
        let keys: Vec<String> = conn.scan()?.collect();
        println!("Keys: {:?}", keys);

        let opts = ScanOptions::default().with_count(500).with_pattern("*rust*");
        let keys: Vec<String> = conn.scan_options(opts)?.collect();
        println!("Keys: {:?}", keys);
        Ok(())
    }

    // 获取集群连接
    fn get_cluster_conn() -> RedisResult<cluster::ClusterConnection> {
        // 集群连接默认只传入1个节点即可
        let nodes = vec!["redis://:[email protected]:7001"];
        let client = ClusterClient::new(nodes)?;
        client.get_connection()
    }

    #[test]
    fn get_set_cluster() -> RedisResult<()> {
        let mut conn = get_cluster_conn()?;
        let ack: String = conn.set("rust:cluster:api", "集群连接")?;
        println!("Redis Ack: {}", ack);
        let ack: String = conn.get("rust:cluster:api")?;
        println!("Redis Ack: {}", ack);
        Ok(())
    }

    #[test]
    fn scan_cluster() -> RedisResult<()> {
        let routing_info = MultiNode((AllMasters, Some(AllSucceeded)));
        Ok(())
    }

}

3. tauri_python

https://gitee.com/cai-xinpenge/tauri_python
用tauri运行python

3.1 tauir.conf.json

一个开屏界面的做法

 "app": {
    "withGlobalTauri": true,
    "windows": [
      {
        "label": "splash",
        "url": "splash/splash.html",
        "width": 400,
        "height": 200,
        "center": true,
        "decorations": false,
        "resizable": false,
        "alwaysOnTop": true,
        "transparent": true
      },
      {
        "label": "main",
        "url": "index.html",
        "visible": false,
        "title": "tauri_python",
        "width": 800,
        "height": 600,
        "center": true,
        "decorations": true,
        "resizable": true
      }
    ],
    "security": {
      "csp": null
    }
  },

3.2 lib.rs

setup里有开屏软件的做法,也有线程的做法可以参考

pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .setup(move |app| {
            let queue = GLOBAL_QUEUE.clone();
            let app_handle = app.handle().clone();
            let config = create_message_queue_server_config(app_handle); // 4 个线程
                                                                         // 创建服务端
            let server = Arc::new(Mutex::new(Server::new(Arc::clone(&queue), config)));
            // 启动服务端线程
            let _server_thread = Arc::new(Mutex::new(Some(thread::spawn({
                let server = Arc::clone(&server);
                move || {
                    let server = server.lock().unwrap();
                    server.start();
                }
            }))));
            let splash_window = app.get_webview_window("splash").unwrap();
            let main_window = app.get_webview_window("main").unwrap();
            std::thread::spawn(move || {
                std::thread::sleep(std::time::Duration::from_secs(3)); // 模拟 3 秒加载时间
                splash_window.close().unwrap(); // 关闭启动窗口
                main_window.show().unwrap(); // 显示主窗口
            });

            Ok(())
        })
        .invoke_handler(tauri::generate_handler![run_playwright])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

4. tauri2+vue3读取串口通讯

https://gitee.com/chun22222222/tauri2ser
tauir连接串口

4.1 cargo.toml

[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"   
serialport = "4.7.0"  
regex = "1"
log = "0.4"
env_logger = "0.10"
fern = "0.6"
chrono = { version = "0.4", features = ["serde"] }

4.2 lib.rs

mod serial;
mod log_app;
use std::panic;
 

fn custom_panic_handler(info: &panic::PanicInfo) { 
    // 打印 panic 信息
    log::error!("{}", info); 
}
 
pub fn run() {
     // 设置自定义的所有错误 或者 其他的 panic 信息的处理函数
    panic::set_hook(Box::new(custom_panic_handler));

    
    log_app::log_init(); 
    tauri::Builder::default()
        .manage(serial::SerialPortState::new())
        .invoke_handler(tauri::generate_handler![
            serial::connect_serial_port,
            serial::disconnect_serial_port,
            serial::send_msg,
        ])  
        //设置窗口的标题 
        .run(tauri::generate_context!())
        .expect("error running tauri application");
}

4.3 serial.rs

通过app.emit(“serial_data”, serial_data).unwrap();发送串口信息到前端

// 查找换行符
while let Some(pos) = find_newline(&temp_buffer) {
    let line = temp_buffer.drain(..pos + 1).collect::<Vec<u8>>(); // 提取一行数据,包括换行符
    let serial_data = if is_hex {
        line.iter().map(|b| format!("{:02X}", b)).collect::<Vec<String>>().join(" ")
    } else {
        match str::from_utf8(&line) {
            Ok(valid_str) => valid_str.trim_end_matches(&['\r', '\n'][..]).to_string(),
            Err(e) => {
                eprintln!("Failed to decode UTF-8: {}", e);
                continue;
            }
        }
    };
    //判断是否为空
    if serial_data.is_empty() {
        continue;
    }
    // 发送数据
    app.emit("serial_data", serial_data).unwrap();
}

4.4 App.vue

前端通过listen,接收到串口信息

import { invoke,Channel  } from "@tauri-apps/api/core";
import { listen } from '@tauri-apps/api/event';
const showMsg = ref([]);
const  msg_value = ref('');
const com_port = ref("COM3");

//获取当前时间,格式化到毫秒HH:mm:ss:SSS
const  get_time = () => {
  let date = new Date(); 
  return date.toTimeString().split(' ')[0] + '.' + date.getMilliseconds();
}
listen('serial_data', (event) => { 
  //获取当前时间 
  showMsg.value.unshift({
    code:0,
    msg:event.payload,
    time : get_time()
  });
});

5. HuLa

https://gitee.com/HuLaSpark/HuLa
即时通讯客户端软件

5.1 tauri.conf.json

此demo支持5个平台(win,mac,linux,android,ios)
tauri v2 开源项目学习(二)_第1张图片

  • macOSPrivateApi 作用:

在 Tauri 的 tauri.conf.json 配置中,macOSPrivateApi 是一个布尔值选项,用于控制是否启用 macOS 的私有 API(非公开的、未正式支持的 API)。
默认值:false(禁用)
启用后 (true):
允许 Tauri 使用一些 macOS 特有的私有 API(如访问更深层的系统功能)。
⚠️ 风险:
这些 API 可能不稳定,未来 macOS 版本可能移除或更改它们。
可能导致 App Store 审核被拒(如果目标是上架 Mac App Store)。
仅推荐在需要特定 macOS 功能(如特殊窗口管理、系统集成)时使用。

  • capabilities 的作用

capabilities 是 Tauri 安全配置中的一个数组,用于定义应用程序的 权限能力集(类似权限白名单)。
默认值
default-capability:基础能力(如文件系统访问、网络请求等)。
mobile-capability:移动端特有功能(如传感器、摄像头,适用于 Tauri 移动端开发)。
desktop-capability:桌面端特有功能(如系统托盘、全局快捷键)。
关键点
权限控制:Tauri 默认采用最小权限原则,capabilities 明确声明应用需要哪些权限。
自定义能力:你可以通过插件(如 tauri-plugin-name)扩展能力

"app": {
    "withGlobalTauri": true,
    "windows": [],
    "security": {
      "csp": null,
      "capabilities": [
        "default-capability",
        "mobile-capability",
        "desktop-capability"
      ]
    },
    "macOSPrivateApi": true
  },

5.2 tauri.windows.conf.json

这几个窗口大小,作用,显示模式都有不同

"windows": [
      {
        "title": "登录",
        "label": "login",
        "url": "/login",
        "fullscreen": false,
        "resizable": false,
        "center": true,
        "width": 320,
        "height": 448,
        "skipTaskbar": false,
        "transparent": true,
        "visible": false,
        "decorations": false
      },
      {
        "label": "tray",
        "url": "/tray",
        "resizable": false,
        "center": false,
        "visible": false,
        "width": 130,
        "height": 44,
        "alwaysOnTop": true,
        "skipTaskbar": true,
        "decorations": false,
        "transparent": true
      },
      {
        "label": "notify",
        "url": "/notify",
        "resizable": false,
        "center": false,
        "visible": false,
        "width": 280,
        "height": 140,
        "alwaysOnTop": true,
        "skipTaskbar": true,
        "decorations": false,
        "transparent": true
      },
      {
        "label": "capture",
        "url": "/capture",
        "fullscreen": false,
        "transparent": true,
        "resizable": false,
        "skipTaskbar": false,
        "decorations": false,
        "visible": false
      },
      {
        "label": "checkupdate",
        "url": "/checkupdate",
        "resizable": false,
        "width": 500,
        "height": 150,
        "alwaysOnTop": true,
        "focus": true,
        "skipTaskbar": true,
        "visible": false,
        "decorations": false,
        "hiddenTitle": true
      }
    ],

5.3 cargo.toml

安装的组件

[dependencies]
tauri = { version = "2.5.1", features = [
    "macos-private-api",
    "tray-icon",
    "image-png",
    "rustls-tls",
] }
tauri-plugin-os = "2.2.1"
tauri-plugin-shell = "2.2.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri-plugin-http = { version = "2.4.3", features = [
    "unsafe-headers",
    "rustls-tls",
] }
tauri-plugin-process = "2.2.1"
tauri-plugin-fs = "2.2.1"
tauri-plugin-dialog = "2.2.1"
tauri-plugin-upload = "2.2.0"
tauri-plugin-global-shortcut = "2.2.0"
tauri-plugin-clipboard-manager = "2.2.2"
tauri-plugin-updater = "2.7.1"
tauri-plugin-sql = { version = "2.2.0", features = ["sqlite"] }
tauri-plugin-single-instance = "2.2.3"
tauri-plugin-notification = "2.2.2"
tungstenite = { version = "0.26.2", features = ["rustls-tls-webpki-roots"] }
tokio = { version = "1", features = ["full"] }

[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
tauri-plugin-autostart = "2.3.0"
lazy_static = "1.4"
screenshots = "0.5.4"
base64 = "0.22.1"
rodio = "0.17.3"
reqwest = { version = "0.11", features = [
    "json",
    "socks",
    "rustls-tls",
    "blocking",
] }
surge-ping = "0.8.0"
rand = "0.8.5"
[target."cfg(target_os =\"macos\")".dependencies]
cocoa = "0.26.0"
objc = "0.2.7"

5.4 src-tauri\capabilities\default.json

有很多权限配置,看下几个权限的配置

    "core:webview:allow-create-webview",
    "core:webview:allow-create-webview-window",
    "core:webview:allow-internal-toggle-devtools",
    "core:webview:allow-set-webview-size",
    "core:webview:allow-webview-size",
	...
	"http:allow-fetch",
    "http:allow-fetch-cancel",
    "http:allow-fetch-read-body",
    "http:allow-fetch-send",

5.5 lib.rs

看到不同平台的启动不同setup的写法
#[cfg(desktop)]
mod desktops;
引入了init_plugin等函数的写法,把plugin的注册放到其他文件中

pub fn run() {
    #[cfg(desktop)]
    {
        setup_desktop();
    }
    #[cfg(mobile)]
    {
        setup_mobile();
    }
}

#[cfg(desktop)]
fn setup_desktop() {
    tauri::Builder::default()
        .init_plugin()
        .init_webwindow_event()
        .init_window_event()
        .setup(move |app| {
            tray::create_tray(app.handle())?;
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![
            default_window_icon,
            screenshot,
            audio,
            set_height,
            set_badge_count,
            test_api_proxy,
            test_ws_proxy,
            #[cfg(target_os = "macos")]
            hide_title_bar_buttons
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

5.6 src\App.vue

前端通过WebviewWindow 获取当前窗口和其他窗口,进行显示或关闭

import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
...
const appWindow = WebviewWindow.getCurrent()
...
const checkUpdateWindow = await WebviewWindow.getByLabel('checkupdate')
await checkUpdateWindow?.show()
...
const closeWindow = await WebviewWindow.getByLabel(event.close)
closeWindow?.close()

5.7 src\hooks\useWindow.ts

可以创建窗口,并可以指定父层

  • parent 作用:
  1. 窗口层级关系(Z-Order)
    子窗口(Child Window) 会始终显示在 父窗口 的上方。
    当父窗口移动时,子窗口会跟随移动(模态窗口的常见行为)。
    父窗口最小化时,子窗口也会被隐藏。
  2. 模态窗口行为(Modal Window)
    如果 parent 被指定,子窗口会默认成为 模态窗口(阻塞父窗口的交互,直到子窗口关闭)。
    用户必须先处理子窗口,才能回到父窗口操作(类似对话框)。
  3. 生命周期关联
    如果父窗口关闭,子窗口也会自动关闭(除非单独设置了 closeOnParentClose: false)。
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { EventEnum } from '@/enums'
import { LogicalSize } from '@tauri-apps/api/dpi'
import { type } from '@tauri-apps/plugin-os'
import { UserAttentionType } from '@tauri-apps/api/window'

/** 判断是兼容的系统 */
const isCompatibility = computed(() => type() === 'windows' || type() === 'linux')
export const useWindow = () => {
  /**
   * 创建窗口
   * @param title 窗口标题
   * @param label 窗口名称
   * @param wantCloseWindow 创建后需要关闭的窗口
   * @param width 窗口宽度
   * @param height 窗口高度
   * @param resizable 调整窗口大小
   * @param minW 窗口最小宽度
   * @param minH 窗口最小高度
   * @param transparent 是否透明
   * @param visible 是否显示
   * */
  const createWebviewWindow = async (
    title: string,
    label: string,
    width: number,
    height: number,
    wantCloseWindow?: string,
    resizable = false,
    minW = 330,
    minH = 495,
    transparent?: boolean,
    visible = false
  ) => {
    const checkLabel = computed(() => {
      /** 如果是打开独立窗口就截取label中的固定label名称 */
      if (label.includes(EventEnum.ALONE)) {
        return label.replace(/\d/g, '')
      } else {
        return label
      }
    })
    const webview = new WebviewWindow(label, {
      title: title,
      url: `/${checkLabel.value}`,
      fullscreen: false,
      resizable: resizable,
      center: true,
      width: width,
      height: height,
      minHeight: minH,
      minWidth: minW,
      skipTaskbar: false,
      decorations: !isCompatibility.value,
      transparent: transparent || isCompatibility.value,
      titleBarStyle: 'overlay', // mac覆盖标签栏
      hiddenTitle: true, // mac隐藏标题栏
      visible: visible
    })

    await webview.once('tauri://created', async () => {
      if (wantCloseWindow) {
        const win = await WebviewWindow.getByLabel(wantCloseWindow)
        win?.close()
      }
    })

    await webview.once('tauri://error', async () => {
      // TODO 这里利用错误处理的方式来查询是否是已经创建了窗口,如果一开始就使用WebviewWindow.getByLabel来查询在刷新的时候就会出现问题 (nyh -> 2024-03-06 23:54:17)
      await checkWinExist(label)
    })

    return webview
  }

  /**
   * 创建模态子窗口
   * @param title 窗口标题
   * @param label 窗口标识
   * @param width 窗口宽度
   * @param height 窗口高度
   * @param parent 父窗口
   * @returns 创建的窗口实例或已存在的窗口实例
   */
  const createModalWindow = async (title: string, label: string, width: number, height: number, parent: string) => {
    // 检查窗口是否已存在
    const existingWindow = await WebviewWindow.getByLabel(label)
    const parentWindow = parent ? await WebviewWindow.getByLabel(parent) : null

    if (existingWindow) {
      // 如果窗口已存在,则聚焦到现有窗口并使其闪烁
      existingWindow.requestUserAttention(UserAttentionType.Critical)
      return existingWindow
    }

    // 创建新窗口
    const modalWindow = new WebviewWindow(label, {
      url: `/${label}`,
      title: title,
      width: width,
      height: height,
      resizable: false,
      center: true,
      minWidth: 500,
      minHeight: 500,
      focus: true,
      parent: parentWindow ? parentWindow : parent,
      decorations: !isCompatibility.value,
      transparent: isCompatibility.value,
      titleBarStyle: 'overlay', // mac覆盖标签栏
      hiddenTitle: true, // mac隐藏标题栏
      visible: false
    })

    // 监听窗口创建完成事件
    modalWindow.once('tauri://created', async () => {
      if (type() === 'windows') {
        // 禁用父窗口,模拟模态窗口效果
        await parentWindow?.setEnabled(false)
      }

      // 设置窗口为焦点
      await modalWindow.setFocus()
    })

    // 监听错误事件
    modalWindow.once('tauri://error', async (e) => {
      console.error(`${title}窗口创建失败:`, e)
      window.$message?.error(`创建${title}窗口失败`)
      await parentWindow?.setEnabled(true)
    })

    return modalWindow
  }

  /**
   * 调整窗口大小
   * @param label 窗口名称
   * @param width 窗口宽度
   * @param height 窗口高度
   * */
  const resizeWindow = async (label: string, width: number, height: number) => {
    const webview = await WebviewWindow.getByLabel(label)
    // 创建一个新的尺寸对象
    const newSize = new LogicalSize(width, height)
    // 调用窗口的 setSize 方法进行尺寸调整
    await webview?.setSize(newSize).catch((error) => {
      console.error('无法调整窗口大小:', error)
    })
  }

  /**
   * 检查窗口是否存在
   * @param L 窗口标签
   */
  const checkWinExist = async (L: string) => {
    const isExistsWinds = await WebviewWindow.getByLabel(L)
    if (isExistsWinds) {
      nextTick().then(async () => {
        // 如果窗口已存在,首先检查是否最小化了
        const minimized = await isExistsWinds.isMinimized()
        // 检查是否是隐藏
        const hidden = await isExistsWinds.isVisible()
        if (!hidden) {
          await isExistsWinds.show()
        }
        if (minimized) {
          // 如果已最小化,恢复窗口
          await isExistsWinds.unminimize()
        }
        // 如果窗口已存在,则给它焦点,使其在最前面显示
        await isExistsWinds.setFocus()
      })
    }
  }

  /**
   * 设置窗口是否可调整大小
   * @param label 窗口名称
   * @param resizable 是否可调整大小
   */
  const setResizable = async (label: string, resizable: boolean) => {
    const webview = await WebviewWindow.getByLabel(label)
    if (webview) {
      await webview.setResizable(resizable).catch((error) => {
        console.error('设置窗口可调整大小失败:', error)
      })
    }
  }

  return {
    createWebviewWindow,
    createModalWindow,
    resizeWindow,
    checkWinExist,
    setResizable
  }
}

5.8 其他窗口操作

    const label = await WebviewWindow.getCurrent().label
    if (route.name !== '/message' && label === 'home') {
      router.push('/message')
    }
    ...
    const appWindow = WebviewWindow.getCurrent()
    await pushListeners([
      appWindow.listen(EventEnum.ALONE, () => {
        emit(EventEnum.ALONE + itemRef.value?.roomId, itemRef.value)
        if (aloneWin.value.has(EventEnum.ALONE + itemRef.value?.roomId)) return
        aloneWin.value.add(EventEnum.ALONE + itemRef.value?.roomId)
      }),
      appWindow.listen(EventEnum.WIN_CLOSE, (e) => {
        aloneWin.value.delete(e.payload)
      })
    ])

5.9 src\services\http.ts

http请求函数

import { fetch } from '@tauri-apps/plugin-http'
import { AppException, ErrorType } from '@/common/exception'
import { RequestQueue } from '@/utils/RequestQueue'
import urls from './urls'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { URLEnum } from '@/enums'

// 错误信息常量
const ERROR_MESSAGES = {
  NETWORK: '网络连接异常,请检查网络设置',
  TIMEOUT: '请求超时,请稍后重试',
  OFFLINE: '当前网络已断开,请检查网络连接',
  ABORTED: '请求已取消',
  UNKNOWN: '请求失败,请稍后重试'
} as const

/**
 * @description 请求参数
 * @property {"GET"|"POST"|"PUT"|"DELETE"} method 请求方法
 * @property {Record} [headers] 请求头
 * @property {Record} [query] 请求参数
 * @property {any} [body] 请求体
 * @property {boolean} [isBlob] 是否为Blob
 * @property {RetryOptions} [retry] 重试选项
 * @property {boolean} [noRetry] 是否禁用重试
 * @return HttpParams
 */
export type HttpParams = {
  method: 'GET' | 'POST' | 'PUT' | 'DELETE'
  headers?: Record<string, string>
  query?: Record<string, any>
  body?: any
  isBlob?: boolean
  retry?: RetryOptions // 重试选项
  noRetry?: boolean // 禁用重试
}

/**
 * @description 重试选项
 */
export type RetryOptions = {
  retries?: number
  retryDelay?: (attempt: number) => number
  retryOn?: number[]
}

/**
 * @description 等待指定的毫秒数
 * @param {number} ms 毫秒数
 * @returns {Promise}
 */
function wait(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms))
}

/**
 * @description 判断是否应进行下一次重试
 * @returns {boolean} 是否继续重试
 */
function shouldRetry(attempt: number, maxRetries: number, abort?: AbortController): boolean {
  return attempt + 1 < maxRetries && !abort?.signal.aborted
}

/**
 * TODO: 防止当有请求的时候突然退出登录,导致在登录窗口发生请求错误
 * 检查是否需要阻止请求
 * @param url 请求地址
 * @returns 是否需要阻止请求
 */
const shouldBlockRequest = async (url: string) => {
  try {
    const currentWindow = WebviewWindow.getCurrent()
    // TODO: 这里如果后续不需要token就可以发送请求还有在没有登录下的窗口都不需要阻止
    const isLoginWindow = currentWindow.label === 'login' || 'register' || 'forgetPassword' || 'tray'

    // 如果不是登录窗口,不阻止请求
    if (!isLoginWindow) return false

    // 登录相关的接口永远不阻止
    if (url.includes(URLEnum.TOKEN) || url.includes(URLEnum.CAPTCHA)) return false

    // 检查是否已登录成功(有双token)
    const hasToken = localStorage.getItem('TOKEN')
    const hasRefreshToken = localStorage.getItem('REFRESH_TOKEN')
    const isLoggedIn = hasToken && hasRefreshToken

    // 在登录窗口但已登录成功的情况下不阻止请求
    return !isLoggedIn
  } catch (error) {
    console.error('检查请求状态失败:', error)
    return false
  }
}

/**
 * @description HTTP 请求实现
 * @template T
 * @param {string} url 请求地址
 * @param {HttpParams} options 请求参数
 * @param {boolean} [fullResponse=false] 是否返回完整响应
 * @param {AbortController} abort 中断器
 * @returns {Promise} 请求结果
 */
async function Http<T = any>(
  url: string,
  options: HttpParams,
  fullResponse: boolean = false,
  abort?: AbortController
): Promise<{ data: T; resp: Response } | T> {
  // 检查是否需要阻止请求
  const shouldBlock = await shouldBlockRequest(url)
  if (shouldBlock) {
    throw new AppException('在登录窗口中,取消非登录相关请求', {
      type: ErrorType.Network,
      showError: false
    })
  }

  // 打印请求信息
  console.log(` 发起请求 → ${options.method} ${url}`, {
    body: options.body,
    query: options.query
  })

  // 默认重试配置,在登录窗口时禁用重试
  const defaultRetryOptions: RetryOptions = {
    retries: 3,
    retryDelay: (attempt) => Math.pow(2, attempt) * 1000,
    retryOn: [] // 状态码意味着已经连接到服务器
  }

  // 合并默认重试配置与用户传入的重试配置
  const retryOptions: RetryOptions = {
    ...defaultRetryOptions,
    ...options.retry
  }

  const { retries = 3, retryDelay } = retryOptions

  // 获取token和指纹
  const token = localStorage.getItem('TOKEN')
  //const fingerprint = await getEnhancedFingerprint()

  // 构建请求头
  const httpHeaders = new Headers(options.headers || {})

  // 设置Content-Type
  if (!httpHeaders.has('Content-Type') && !(options.body instanceof FormData)) {
    httpHeaders.set('Content-Type', 'application/json')
  }

  // 设置Authorization
  if (token) {
    httpHeaders.set('Authorization', `Bearer ${token}`)
  }

  // 设置浏览器指纹
  //if (fingerprint) {
  //httpHeaders.set('X-Device-Fingerprint', fingerprint)
  //}

  // 构建 fetch 请求选项
  const fetchOptions: RequestInit = {
    method: options.method,
    headers: httpHeaders,
    signal: abort?.signal
  }

  // 获取代理设置
  // const proxySettings = JSON.parse(localStorage.getItem('proxySettings') || '{}')
  // 如果设置了代理,添加代理配置 (BETA)
  // if (proxySettings.type && proxySettings.ip && proxySettings.port) {
  //   // 使用 Rust 后端的代理客户端
  //   fetchOptions.proxy = {
  //     url: `${proxySettings.type}://${proxySettings.ip}:${proxySettings.port}`
  //   }
  // }

  // 判断是否需要添加请求体
  if (options.body) {
    if (!(options.body instanceof FormData || options.body instanceof URLSearchParams)) {
      fetchOptions.body = JSON.stringify(options.body)
    } else {
      fetchOptions.body = options.body // 如果是 FormData 或 URLSearchParams 直接使用
    }
  }

  // 添加查询参数
  if (options.query) {
    const queryString = new URLSearchParams(options.query).toString()
    url += `?${queryString}`
  }

  // 定义重试函数
  let tokenRefreshCount = 0 // 在闭包中存储计数器
  async function attemptFetch(currentAttempt: number): Promise<{ data: T; resp: Response } | T> {
    try {
      const response = await fetch(url, fetchOptions)

      // 先判断是否连接到服务器,fetch请求是否成功,如果不成功那么就是本地客户端网络异常
      if (!response.ok) {
        throw new AppException(`HTTP error! status: ${response.status}`, {
          type: ErrorType.Network,
          code: response.status,
          details: { url, method: options.method }
        })
      }

      // 解析响应数据
      const responseData = options.isBlob ? await response.arrayBuffer() : await response.json()

      // 判断服务器返回的错误码进行操作
      switch (responseData.code) {
        case 401: {
          console.log(' Token无效,清除token并重新登录...')
          // 触发重新登录事件
          window.dispatchEvent(new Event('needReLogin'))
          break
        }
        case 403: {
          console.log(' 权限不足')
          break
        }
        case 422: {
          break
        }
        case 40004: {
          // 限制token刷新重试次数,最多重试一次
          if (tokenRefreshCount >= 1) {
            console.log(' Token刷新重试次数超过限制,退出重试')
            window.dispatchEvent(new Event('needReLogin'))
            throw new AppException('Token刷新失败', {
              type: ErrorType.TokenExpired,
              showError: true
            })
          }

          try {
            console.log(' 开始尝试刷新Token并重试请求')
            const newToken = await refreshTokenAndRetry()
            // 使用新token重试当前请求
            httpHeaders.set('Authorization', `Bearer ${newToken}`)
            console.log(' 使用新Token重试原请求')
            // 增加计数器
            tokenRefreshCount++
            return attemptFetch(currentAttempt)
          } catch (refreshError) {
            // 续签出错也触发重新登录
            window.dispatchEvent(new Event('needReLogin'))
            throw refreshError
          }
        }
      }

      // 如果fecth请求成功,但是服务器请求不成功并且返回了错误,那么就抛出错误
      if (responseData && !responseData.success) {
        throw new AppException(responseData.msg || '服务器返回错误', {
          type: ErrorType.Server,
          code: response.status,
          details: responseData,
          showError: true
        })
      }

      // 打印响应结果
      console.log(`✅ 请求成功 → ${options.method} ${url}`, {
        status: response.status,
        data: responseData
      })

      // 若请求成功且没有业务错误
      if (fullResponse) {
        return { data: responseData, resp: response }
      }
      return responseData
    } catch (error: any) {
      // 优化错误日志,仅在开发环境打印详细信息
      if (import.meta.env.DEV) {
        console.error(`尝试 ${currentAttempt + 1} 失败 →`, error)
      }

      // 处理网络相关错误
      if (
        error instanceof TypeError || // fetch 的网络错误会抛出 TypeError
        error.name === 'AbortError' || // 请求被中断
        !navigator.onLine // 浏览器离线
      ) {
        // 获取用户友好的错误信息
        const errorMessage = getNetworkErrorMessage(error)

        if (shouldRetry(currentAttempt, retries, abort)) {
          console.warn(`${errorMessage},准备重试 → 第 ${currentAttempt + 2} 次尝试`)
          const delayMs = retryDelay ? retryDelay(currentAttempt) : 1000
          await wait(delayMs)
          return attemptFetch(currentAttempt + 1)
        }

        // 重试次数用完,抛出友好的错误信息
        throw new AppException(errorMessage, {
          type: ErrorType.Network,
          details: { attempts: currentAttempt + 1 },
          showError: true
        })
      }

      // 未知错误,使用友好的错误提示
      throw new AppException(ERROR_MESSAGES.UNKNOWN, {
        type: error instanceof TypeError ? ErrorType.Network : ErrorType.Unknown,
        details: { attempts: currentAttempt + 1 },
        showError: true
      })
    }
  }

  // 添加获取网络错误信息的辅助函数
  function getNetworkErrorMessage(error: any): string {
    if (!navigator.onLine) {
      return ERROR_MESSAGES.OFFLINE
    }

    if (error.name === 'AbortError') {
      return ERROR_MESSAGES.ABORTED
    }

    // 检查是否包含超时关键词
    if (error.message?.toLowerCase().includes('timeout')) {
      return ERROR_MESSAGES.TIMEOUT
    }

    return ERROR_MESSAGES.NETWORK
  }

  // 第一次执行,attempt=0
  return attemptFetch(0)
}

// 添加一个标记,避免多个请求同时刷新token
let isRefreshing = false
// 使用队列实现
const requestQueue = new RequestQueue()
async function refreshTokenAndRetry(): Promise<string> {
  if (isRefreshing) {
    console.log(' 已有刷新请求在进行中,加入等待队列')
    return new Promise((resolve) => {
      // 可以根据请求类型设置优先级
      requestQueue.enqueue(resolve, 1)
    })
  }

  isRefreshing = true
  try {
    const refreshToken = localStorage.getItem('REFRESH_TOKEN')
    if (!refreshToken) {
      console.error('❌ 无刷新令牌')
      throw new Error('无刷新令牌')
    }

    console.log(' 正在使用refreshToken获取新的token')
    const response = await fetch(urls.refreshToken, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${refreshToken}`
      },
      body: JSON.stringify({ refreshToken })
    })

    const data = await response.json()

    if (!response.ok || !data.success) {
      // 重新登录
      window.dispatchEvent(new Event('needReLogin'))
      throw new Error('刷新令牌失败')
    }
    const { token, refreshToken: newRefreshToken } = data.data

    console.log(' Token刷新成功,更新存储', data)
    // 更新本地存储的token 知道
    localStorage.setItem('TOKEN', token)
    localStorage.setItem('REFRESH_TOKEN', newRefreshToken)

    // 使用队列处理方式
    await requestQueue.processQueue(token)

    return token
  } catch (error) {
    console.error('❌ 刷新Token过程出错:', error)
    requestQueue.clear() // 发生错误时清空队列
    window.dispatchEvent(new Event('needReLogin'))
    throw error
  } finally {
    isRefreshing = false
  }
}

export default Http

5.10 src\services\webSocket.ts

websocket函数

import { WsResponseMessageType, WsTokenExpire } from '@/services/wsType.ts'
import type {
  LoginSuccessResType,
  LoginInitResType,
  WsReqMsgContentType,
  OnStatusChangeType,
  UserStateType
} from '@/services/wsType.ts'
import type { MessageType, MarkItemType, RevokedMsgType } from '@/services/types'
import { OnlineEnum, ChangeTypeEnum, WorkerMsgEnum, ConnectionState } from '@/enums'
import { useMitt } from '@/hooks/useMitt.ts'
import { useUserStore } from '@/stores/user'
import { getEnhancedFingerprint } from '@/services/fingerprint.ts'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { useTauriListener } from '@/hooks/useTauriListener'
import { listen } from '@tauri-apps/api/event'
import { useDebounceFn } from '@vueuse/core'
// 使用类型导入避免直接执行代码
import type { useNetworkReconnect as UseNetworkReconnectType } from '@/hooks/useNetworkReconnect'

// 创建 webSocket worker
const worker: Worker = new Worker(new URL('../workers/webSocket.worker.ts', import.meta.url), {
  type: 'module'
})

// 创建 timer worker
const timerWorker: Worker = new Worker(new URL('../workers/timer.worker.ts', import.meta.url), {
  type: 'module'
})

// 添加一个标识是否是主窗口的变量
let isMainWindow = false

// LRU缓存实现
class LRUCache<K, V> {
  private maxSize: number
  private cache = new Map<K, V>()

  constructor(maxSize: number = 1000) {
    this.maxSize = maxSize
  }

  set(key: K, value: V) {
    if (this.cache.has(key)) {
      this.cache.delete(key)
    } else if (this.cache.size >= this.maxSize) {
      const firstKey = this.cache.keys().next().value
      if (firstKey) {
        this.cache.delete(firstKey)
      }
    }
    this.cache.set(key, value)
  }

  has(key: K): boolean {
    return this.cache.has(key)
  }

  clear() {
    this.cache.clear()
  }

  get size(): number {
    return this.cache.size
  }
}

class WS {
  // 添加消息队列大小限制
  readonly #MAX_QUEUE_SIZE = 50 // 减少队列大小
  #tasks: WsReqMsgContentType[] = []
  // 重连
  #connectReady = false
  // 使用LRU缓存替代简单的Set
  #processedMsgCache = new LRUCache<number, number>(1000) // 使用LRU缓存

  #tauriListener: ReturnType<typeof useTauriListener> | null = null

  // 存储连接健康状态信息
  #connectionHealth = {
    isHealthy: true,
    lastPongTime: null as number | null,
    timeSinceLastPong: null as number | null
  }

  // 网络重连工具,延迟初始化
  #networkReconnect: ReturnType<typeof UseNetworkReconnectType> | null = null

  // 存储watch清理函数
  #unwatchFunctions: (() => void)[] = []

  constructor() {
    this.initWindowType()
    if (isMainWindow) {
      this.initConnect()
      // 收到WebSocket worker消息
      worker.addEventListener('message', this.onWorkerMsg)
      // 收到Timer worker消息
      timerWorker.addEventListener('message', this.onTimerWorkerMsg)
      // 添加页面可见性监听
      this.initVisibilityListener()

      this.initNetworkReconnect()
    }
  }

  // 初始化网络重连工具
  private initNetworkReconnect() {
    // 动态导入以延迟执行
    import('@/hooks/useNetworkReconnect')
      .then(({ useNetworkReconnect }) => {
        this.#networkReconnect = useNetworkReconnect()
        console.log('[WebSocket] 网络重连工具初始化完成')

        // 监听网络在线状态变化
        if (this.#networkReconnect.isOnline) {
          const unwatch = watch(this.#networkReconnect.isOnline, (newValue, oldValue) => {
            // 只在网络从离线变为在线时执行重连
            if (newValue === true && oldValue === false) {
              console.log('[WebSocket] 网络恢复在线状态,主动重新初始化WebSocket连接')
              // 重置重连计数并重新初始化连接
              this.forceReconnect()
            }
          })

          // 存储清理函数
          this.#unwatchFunctions = this.#unwatchFunctions || []
          this.#unwatchFunctions.push(unwatch)
        }
      })
      .catch((err) => {
        console.error('[WebSocket] 网络重连工具初始化失败:', err)
      })
  }

  // 初始化页面可见性监听
  private async initVisibilityListener() {
    const handleVisibilityChange = (isVisible: boolean) => {
      worker.postMessage(
        JSON.stringify({
          type: 'visibilityChange',
          value: { isHidden: !isVisible }
        })
      )

      // 优化的可见性恢复检查
      if (isVisible && this.#networkReconnect?.isOnline?.value) {
        // 检查最后一次通信时间,如果太久没有通信,刷新数据
        const now = Date.now()
        const lastPongTime = this.#connectionHealth.lastPongTime
        const heartbeatTimeout = 90000 // 增加到90秒,减少误触发
        if (lastPongTime && now - lastPongTime > heartbeatTimeout) {
          console.log('[Network] 应用从后台恢复且长时间无心跳,刷新数据')
          this.#networkReconnect?.refreshAllData()
        }
      }
    }

    const debouncedVisibilityChange = useDebounceFn((isVisible: boolean) => {
      handleVisibilityChange(isVisible)
    }, 300)

    // 使用document.visibilitychange事件 兼容web
    document.addEventListener('visibilitychange', () => {
      const isVisible = !document.hidden
      console.log(`document visibility change: ${document.hidden ? '隐藏' : '可见'}`)
      debouncedVisibilityChange(isVisible)
    })

    // 跟踪当前窗口状态,避免无变化时重复触发
    let currentVisibilityState = true

    // 创建状态变更处理器
    const createStateChangeHandler = (newState: boolean) => {
      return () => {
        if (currentVisibilityState !== newState) {
          currentVisibilityState = newState
          debouncedVisibilityChange(newState)
        }
      }
    }

    try {
      // 设置各种Tauri窗口事件监听器
      // 窗口失去焦点 - 隐藏状态
      await listen('tauri://blur', createStateChangeHandler(false))

      // 窗口获得焦点 - 可见状态
      await listen('tauri://focus', createStateChangeHandler(true))

      // 窗口最小化 - 隐藏状态
      await listen('tauri://window-minimized', createStateChangeHandler(false))

      // 窗口恢复 - 可见状态
      await listen('tauri://window-restored', createStateChangeHandler(true))

      // 窗口隐藏 - 隐藏状态
      await listen('tauri://window-hidden', createStateChangeHandler(false))

      // 窗口显示 - 可见状态
      await listen('tauri://window-shown', createStateChangeHandler(true))
    } catch (error) {
      console.error('无法设置Tauri Window事件监听:', error)
    }
  }

  // 处理Timer worker消息
  onTimerWorkerMsg = (e: MessageEvent<any>) => {
    const data = e.data
    switch (data.type) {
      case 'timeout': {
        // 检查是否是心跳超时消息
        if (data.msgId && data.msgId.startsWith('heartbeat_timeout_')) {
          // 转发给WebSocket worker
          worker.postMessage(JSON.stringify({ type: 'heartbeatTimeout' }))
        }
        // 处理任务队列定时器超时
        else if (data.msgId === 'process_tasks_timer') {
          const userStore = useUserStore()
          if (userStore.isSign) {
            // 处理堆积的任务
            for (const task of this.#tasks) {
              this.send(task)
            }
            // 清空缓存的消息
            this.#tasks = []
          }
        }
        break
      }
      case 'periodicHeartbeat': {
        // 心跳触发,转发给WebSocket worker
        worker.postMessage(JSON.stringify({ type: 'heartbeatTimerTick' }))
        break
      }
      case 'reconnectTimeout': {
        // timer上报重连超时事件,转发给WebSocket worker
        console.log('重试次数: ', data.reconnectCount)
        worker.postMessage(
          JSON.stringify({
            type: 'reconnectTimeout',
            value: { reconnectCount: data.reconnectCount }
          })
        )
        break
      }
    }
  }

  // 初始化窗口类型
  private async initWindowType() {
    const currentWindow = WebviewWindow.getCurrent()
    isMainWindow = currentWindow.label === 'home'
  }

  initConnect = async () => {
    const token = localStorage.getItem('TOKEN')
    // 如果token 是 null, 而且 localStorage 的用户信息有值,需要清空用户信息
    if (token === null && localStorage.getItem('user')) {
      localStorage.removeItem('user')
    }
    const clientId = await getEnhancedFingerprint()
    const savedProxy = localStorage.getItem('proxySettings')
    let serverUrl = import.meta.env.VITE_WEBSOCKET_URL
    if (savedProxy) {
      const settings = JSON.parse(savedProxy)
      const suffix = settings.wsIp + ':' + settings.wsPort + '/' + settings.wsSuffix
      if (settings.wsType === 'ws' || settings.wsType === 'wss') {
        serverUrl = settings.wsType + '://' + suffix
      }
    }
    // 初始化 ws
    worker.postMessage(
      `{"type":"initWS","value":{"token":${token ? `"${token}"` : null},"clientId":${clientId ? `"${clientId}", "serverUrl":"${serverUrl}"` : null}}}`
    )
  }

  onWorkerMsg = async (e: MessageEvent<any>) => {
    const params: { type: string; value: unknown } = JSON.parse(e.data)
    switch (params.type) {
      case WorkerMsgEnum.MESSAGE: {
        await this.onMessage(params.value as string)
        break
      }
      case WorkerMsgEnum.OPEN: {
        this.#dealTasks()
        break
      }
      case WorkerMsgEnum.CLOSE:
      case WorkerMsgEnum.ERROR: {
        this.#onClose()
        break
      }
      case WorkerMsgEnum.WS_ERROR: {
        console.log('WebSocket错误:', (params.value as { msg: string }).msg)
        useMitt.emit(WsResponseMessageType.NO_INTERNET, params.value)
        // 如果是重连失败,可以提示用户刷新页面
        if ((params.value as { msg: string }).msg.includes('连接失败次数过多')) {
          // 可以触发UI提示,让用户刷新页面
          useMitt.emit('wsReconnectFailed', params.value)
        }
        break
      }
      case 'startReconnectTimer': {
        console.log('worker上报心跳超时事件', params.value)
        // 向timer发送startReconnectTimer事件
        timerWorker.postMessage({
          type: 'startReconnectTimer',
          reconnectCount: (params.value as any).reconnectCount as number,
          value: { delay: 1000 }
        })
        break
      }
      // 心跳定时器相关消息处理
      case 'startHeartbeatTimer': {
        // 启动心跳定时器
        const { interval } = params.value as { interval: number }
        timerWorker.postMessage({
          type: 'startPeriodicHeartbeat',
          interval
        })
        break
      }
      case 'stopHeartbeatTimer': {
        // 停止心跳定时器
        timerWorker.postMessage({
          type: 'stopPeriodicHeartbeat'
        })
        break
      }
      case 'startHeartbeatTimeoutTimer': {
        // 启动心跳超时定时器
        const { timerId, timeout } = params.value as { timerId: string; timeout: number }
        timerWorker.postMessage({
          type: 'startTimer',
          msgId: timerId,
          duration: timeout
        })
        break
      }
      case 'clearHeartbeatTimeoutTimer': {
        // 清除心跳超时定时器
        const { timerId } = params.value as { timerId: string }
        timerWorker.postMessage({
          type: 'clearTimer',
          msgId: timerId
        })
        break
      }
      case 'connectionStateChange': {
        const { state, isReconnection } = params.value as { state: ConnectionState; isReconnection: boolean }

        // 检测重连成功
        if (isReconnection && state === ConnectionState.CONNECTED) {
          console.log(' WebSocket 重连成功')
          // 网络重连成功后刷新数据
          if (isMainWindow && this.#networkReconnect) {
            console.log('开始刷新数据...')
            this.#networkReconnect.refreshAllData()
          } else if (isMainWindow) {
            // 如果还没初始化,延迟初始化后再刷新
            this.initNetworkReconnect()
          }
        } else if (!isReconnection && state === ConnectionState.CONNECTED) {
          console.log('✅ WebSocket 首次连接成功')
        }
        break
      }
      // 处理心跳响应
      case 'pongReceived': {
        const { timestamp } = params.value as { timestamp: number }
        this.#connectionHealth.lastPongTime = timestamp
        break
      }
      // 处理连接健康状态
      case 'connectionHealthStatus': {
        const { isHealthy, lastPongTime, timeSinceLastPong } = params.value as {
          isHealthy: boolean
          lastPongTime: number | null
          timeSinceLastPong: number | null
        }
        this.#connectionHealth = { isHealthy, lastPongTime, timeSinceLastPong }
        useMitt.emit('wsConnectionHealthChange', this.#connectionHealth)
        break
      }
    }
  }

  // 重置一些属性
  #onClose = () => {
    this.#connectReady = false
  }

  #dealTasks = () => {
    this.#connectReady = true
    // 先探测登录态
    // this.#detectionLoginStatus()

    timerWorker.postMessage({
      type: 'startTimer',
      msgId: 'process_tasks_timer',
      duration: 500
    })
  }

  #send(msg: WsReqMsgContentType) {
    worker.postMessage(`{"type":"message","value":${typeof msg === 'string' ? msg : JSON.stringify(msg)}}`)
  }

  send = (params: WsReqMsgContentType) => {
    if (isMainWindow) {
      // 主窗口直接发送消息
      if (this.#connectReady) {
        this.#send(params)
      } else {
        // 优化的队列管理
        if (this.#tasks.length >= this.#MAX_QUEUE_SIZE) {
          // 优先丢弃非关键消息
          const nonCriticalIndex = this.#tasks.findIndex(
            (task) => typeof task === 'object' && task.type !== 1 && task.type !== 2
          )
          if (nonCriticalIndex !== -1) {
            this.#tasks.splice(nonCriticalIndex, 1)
            console.warn('消息队列已满,丢弃非关键消息')
          } else {
            this.#tasks.shift()
            console.warn('消息队列已满,丢弃最旧消息')
          }
        }
        this.#tasks.push(params)
      }
    }
  }

  // 收到消息回调
  onMessage = async (value: string) => {
    try {
      const params: { type: WsResponseMessageType; data: unknown } = JSON.parse(value)
      switch (params.type) {
        // 获取登录二维码
        case WsResponseMessageType.LOGIN_QR_CODE: {
          console.log('获取二维码')
          useMitt.emit(WsResponseMessageType.LOGIN_QR_CODE, params.data as LoginInitResType)
          break
        }
        // 等待授权
        case WsResponseMessageType.WAITING_AUTHORIZE: {
          console.log('等待授权')
          useMitt.emit(WsResponseMessageType.WAITING_AUTHORIZE)
          break
        }
        // 登录成功
        case WsResponseMessageType.LOGIN_SUCCESS: {
          console.log('登录成功')
          useMitt.emit(WsResponseMessageType.LOGIN_SUCCESS, params.data as LoginSuccessResType)
          break
        }
        // 收到消息
        case WsResponseMessageType.RECEIVE_MESSAGE: {
          const message = params.data as MessageType
          useMitt.emit(WsResponseMessageType.RECEIVE_MESSAGE, message)
          break
        }
        // 用户状态改变
        case WsResponseMessageType.USER_STATE_CHANGE: {
          console.log('用户状态改变', params.data)
          useMitt.emit(WsResponseMessageType.USER_STATE_CHANGE, params.data as UserStateType)
          break
        }
        // 用户上线
        case WsResponseMessageType.ONLINE: {
          console.log('上线', params.data)
          useMitt.emit(WsResponseMessageType.ONLINE, params.data as OnStatusChangeType)
          break
        }
        // 用户下线
        case WsResponseMessageType.OFFLINE: {
          console.log('下线')
          useMitt.emit(WsResponseMessageType.OFFLINE)
          break
        }
        // 用户 token 过期
        case WsResponseMessageType.TOKEN_EXPIRED: {
          console.log('账号在其他设备登录')
          useMitt.emit(WsResponseMessageType.TOKEN_EXPIRED, params.data as WsTokenExpire)
          break
        }
        // 拉黑的用户的发言在禁用后,要删除他的发言
        case WsResponseMessageType.INVALID_USER: {
          console.log('无效用户')
          useMitt.emit(WsResponseMessageType.INVALID_USER, params.data as { uid: number })
          break
        }
        // 点赞、不满消息通知
        case WsResponseMessageType.MSG_MARK_ITEM: {
          useMitt.emit(WsResponseMessageType.MSG_MARK_ITEM, params.data as { markList: MarkItemType[] })
          break
        }
        // 消息撤回通知
        case WsResponseMessageType.MSG_RECALL: {
          console.log('撤回')
          useMitt.emit(WsResponseMessageType.MSG_RECALL, params.data as { data: RevokedMsgType })
          break
        }
        // 新好友申请
        case WsResponseMessageType.REQUEST_NEW_FRIEND: {
          console.log('好友申请')
          useMitt.emit(WsResponseMessageType.REQUEST_NEW_FRIEND, params.data as { uid: number; unreadCount: number })
          break
        }
        // 成员变动
        case WsResponseMessageType.NEW_FRIEND_SESSION: {
          console.log('成员变动')
          useMitt.emit(
            WsResponseMessageType.NEW_FRIEND_SESSION,
            params.data as {
              roomId: number
              uid: number
              changeType: ChangeTypeEnum
              activeStatus: OnlineEnum
              lastOptTime: number
            }
          )
          break
        }
        // 同意好友请求
        case WsResponseMessageType.REQUEST_APPROVAL_FRIEND: {
          console.log('同意好友申请', params.data)
          useMitt.emit(
            WsResponseMessageType.REQUEST_APPROVAL_FRIEND,
            params.data as {
              uid: number
            }
          )
          break
        }
        // 自己修改我在群里的信息
        case WsResponseMessageType.MY_ROOM_INFO_CHANGE: {
          console.log('自己修改我在群里的信息', params.data)
          useMitt.emit(
            WsResponseMessageType.MY_ROOM_INFO_CHANGE,
            params.data as {
              myName: string
              roomId: string
              uid: string
            }
          )
          break
        }
        case WsResponseMessageType.ROOM_INFO_CHANGE: {
          console.log('群主修改群聊信息', params.data)
          useMitt.emit(
            WsResponseMessageType.ROOM_INFO_CHANGE,
            params.data as {
              roomId: string
              name: string
              avatar: string
            }
          )
          break
        }
        case WsResponseMessageType.ROOM_GROUP_NOTICE_MSG: {
          console.log('发布群公告', params.data)
          useMitt.emit(
            WsResponseMessageType.ROOM_GROUP_NOTICE_MSG,
            params.data as {
              id: string
              content: string
              top: string
            }
          )
          break
        }
        case WsResponseMessageType.ROOM_EDIT_GROUP_NOTICE_MSG: {
          console.log('编辑群公告', params.data)
          useMitt.emit(
            WsResponseMessageType.ROOM_EDIT_GROUP_NOTICE_MSG,
            params.data as {
              id: string
              content: string
              top: string
            }
          )
          break
        }
        case WsResponseMessageType.ROOM_DISSOLUTION: {
          console.log('群解散', params.data)
          useMitt.emit(WsResponseMessageType.ROOM_DISSOLUTION, params.data)
          break
        }
        default: {
          console.log('接收到未处理类型的消息:', params)
          break
        }
      }
    } catch (error) {
      console.error('Failed to parse WebSocket message:', error)
      // 可以添加错误上报逻辑
      return
    }
  }

  // 检查连接健康状态
  checkConnectionHealth() {
    if (isMainWindow) {
      worker.postMessage(
        JSON.stringify({
          type: 'checkConnectionHealth'
        })
      )
      return this.#connectionHealth
    }
    return null
  }

  // 获取当前连接健康状态
  getConnectionHealth() {
    return this.#connectionHealth
  }

  // 强制重新连接WebSocket
  forceReconnect() {
    console.log('[WebSocket] 强制重新初始化WebSocket连接')
    // 停止当前的重连计时器
    worker.postMessage(JSON.stringify({ type: 'clearReconnectTimer' }))

    // 停止心跳
    worker.postMessage(JSON.stringify({ type: 'stopHeartbeat' }))

    // 重置重连计数并重新初始化
    worker.postMessage(JSON.stringify({ type: 'resetReconnectCount' }))

    // 重新初始化连接
    this.initConnect()
  }

  destroy() {
    try {
      // 优化的资源清理顺序
      worker.postMessage(JSON.stringify({ type: 'clearReconnectTimer' }))
      worker.postMessage(JSON.stringify({ type: 'stopHeartbeat' }))

      // 同时终止timer worker相关的心跳
      timerWorker.postMessage({
        type: 'stopPeriodicHeartbeat'
      })

      // 清理内存
      this.#tasks.length = 0 // 更高效的数组清空
      this.#processedMsgCache.clear()
      this.#connectReady = false

      // 重置连接健康状态
      this.#connectionHealth = {
        isHealthy: true,
        lastPongTime: null,
        timeSinceLastPong: null
      }

      // 清理 Tauri 事件监听器
      this.#tauriListener?.cleanup()
      this.#tauriListener = null

      // 清理所有watch
      this.#unwatchFunctions.forEach((unwatch) => {
        try {
          unwatch()
        } catch (error) {
          console.warn('清理watch函数时出错:', error)
        }
      })
      this.#unwatchFunctions.length = 0

      // 最后终止workers
      setTimeout(() => {
        worker.terminate()
        timerWorker.terminate()
      }, 100) // 给一点时间让消息处理完成
    } catch (error) {
      console.error('销毁WebSocket时出错:', error)
    }
  }
}

export default new WS()

此案例有各类实现方式,如更新,托盘图标等,值得好好研究学习

你可能感兴趣的:(tauri v2 开源项目学习(二))