基于Tauri的跨平台游戏登录器实践

4,136 阅读6分钟

需求背景

近期接到需求,需要做一个游戏登录器,即类似于wegame的一个桌面应用,可以通过登录器来实现游戏的管理。对比了目前流行的跨平台桌面应用框架 electronnwjstauri,决定尝试一下tauri,毕竟是真的轻量,还能借机学习一下rust,很完美。

Tauri简介

Tauri是基于rust开发的跨平台应用框架,使用系统自带的webview渲染ui。Tauri 在 Windows 上使用 Microsoft Edge WebView2,在 macOS 上使用 WKWebView,在 Linux 上使用 webkitgtk。目前新的alpha版本已经支持了移动端。

Tauri的优点:

  1. 轻量,打包快且体积小
  2. 高性能,内存占用少
  3. 安全

Tauri的缺点:

  1. 依赖系统的webview版本,可能存在兼容问题(目前还没遇到)
    1. 调试页面时,mac上是Safari的界面,个人不是很习惯..
  2. 跨平台编译流程繁琐,目前只提供了github action,代码不在github上就很...只有win打win的包,mac打mac的包
  3. 学习成本微高,需要学习新语言rust,rust的学习曲线又是出了名的陡峭。(但是可以学学rust,毕竟在工程化领域很吃香)
  4. 中文资料较少,遇到问题要花更长的时间解决(如果有chatgpt辅助会好些)

!!!请不要盲目使用Tauri,要根据项目的实际情况选择,重要紧急的项目不推荐使用

窗口设置

tauri的配置文件是/src-tauri/tauri.conf.json,配置文件的详细说明参见官网tauri.app/zh-cn/v1/ap…。窗口相关的配置在tauri.window中,如宽、高、是否可拖动、是否全屏等。
image.png
decorations设置为false时,窗口不会展示最大化、最小化、关闭等工具栏按钮,如果需要这些功能,需要手动调用@tauri-apps/api/window中的setMinSize``setMaxSize方法。
!!注意

  1. decorations为false时
    1. 窗口会自动偏移,需要设置center为true
    2. 窗口会无法拖动,如果需要能够拖动,需要在外层的dom节点上添加data-tauri-drag-region属性
  2. setMinSize``setMaxSize所有js api在使用时,需要先在tauri.conf.json``tauri.allowlist中进行声明。

通信

前端调用rust

tauri提供了command系统,用于从 web 应用程序调用 Rust 函数。 command可以接受参数并返回结果, 也可以返回错误。如何使用command?

  1. 在rust的函数上添加#[tauri::command]注释,函数名即为command的名称
  2. main函数中使用invoke_handler注入command,多个command传递数组
  3. 在js代码中,从@tauri-apps/api/tauri中导入invoke函数,invoke('command_name')调用

!!注意

  1. command传递参数时,如果变量使用了驼峰命名,在rust中接收到的变量会被转化为snake形势,如图,invokeMessage被转化为了invoke_message。image.png
  2. invoke方法是异步的,可以使用await方式调用

事件

可以通过触发和监听事件,从rust向js或者反过来传递消息。事件可以是全局事件,也可以指定窗口,没什么特殊用法,参见官网tauri.app/zh-cn/v1/gu…

文件下载

游戏相关的资源都存放在oss上,下载或更新时需要使用http库拉取文件,这里使用了reqwestcrates.io/就像是npm一样,我们可以在上面搜索需要使用的第三方包,然后通过cargo安装。下载功能的实现参考了FishLauncherdouyin-downloader。文件下载主要有两个点需要考虑:

  1. 下载的进度条
  2. 断点续传

进度条

当前进度 = 当前已下载的大小 / 总文件大小
构造一个client实例后,很容就能得到当前下载文件的总大小和当前已下载的大小,触发progress事件,然后在js中监听,获取事件参数,计算进度并展示,部分代码如下:

let client = reqwest::Client::new();

let response = client
    .get(&url)
    .headers(headers)
    .send()
    .await
    .or(Err(format!("Failed to GET from '{}'", &url)))?;
  // 文件总大小
  let source_size = response
    .content_length()
    .ok_or(format!("Failed to get content length from '{}'", &url))?;

// 已经下载的大小
let mut download_size: u64 = 0;

// 下载流
let mut stream = response.bytes_stream();

  while let Some(item) = stream.next().await {
    let chunk = item.or(Err(format!("Error while downloading file")))?;
    download_size += chunk.len() as u64;

    // 触发事件
    window
      .emit(
        "progress",
        Payload {
          total: source_size,
          done: download_size,
        },
      )
      .unwrap();
    file
      .write(&chunk)
      .or(Err(format!("Error while writing to file")))?;
  }


断点续传

游戏文件一般都超过500M,需要考虑中断下载后继续下载的情况,即断点续传。阿里云oss支持这个功能,所以使用起来很简单,在http请求中添加header字段range,服务器就知道该返回哪一块的数据回来。具体操作如下:

  1. 保存已经下载的文件大小 download_bytes
  2. 每次开始下载前,检查download_bytes的值是否大于0
    1. 大于0,进入断点续传逻辑
      1. 打开原来的文件,而不是创建新文件,要使用追加的模式
      2. 在header中添加range字段
    2. 否则正常下载
  3. 下载完成后,重置download_bytes的值

部分代码如下:

// 根据download_bytes判断创建文件还是打开原有文件
let mut file = if download_bytes > 0 {
  OpenOptions::new().append(true).open(&folder).or(Err(format!("Failed to open file '{}'", folder)))?
} else {
  File::create(&folder).or(Err(format!("Failed to create file '{}'", folder)))?
};

let mut headers = reqwest::header::HeaderMap::new();
headers.append(USER_AGENT, user_agent.parse().unwrap());

// 添加RANGE
if download_bytes > 0 {
  headers.append(RANGE, format!("bytes={}-", bytes).parse().unwrap());
}

// 要保证进度条的准确
let source_size = response
  .content_length()
  .ok_or(format!("Failed to get content length from '{}'", &url))? + download_bytes;

let mut download_size: u64 = download_bytes;

!!注意:

  1. 多文件同时下载时,要注意进度条的变化,最好先获取多个文件的总大小,计算总的进度。否则会0-100%乱闪。
  2. 断点续传时,由于使用了range,reqwest获取到的文件总大小并不是实际的文件总大小。

自更新

打包

参见文档 tauri.app/zh-cn/v1/gu…
如果我们需要校验更新是否安全,需要在tauri.conf.json中设置_Public-key,_ 打包时会生成对应包的签名,需要配置在更新文件中_。参见 tauri.app/zh-cn/v1/gu…
运行下方的命令,项目根目录下,会生成_Public-key和Private key

tauri signer generate -w ~/.tauri/myapp.key
需要注意的是,配置_Public-key_后,需要设置一个环境变量,TAURI_PRIVATE_KEY ,值为 _Private-key,_否则打包会失败。另外,在window上,最好在cmd命令行中运行打包命令。

更新json配置

tauri更新的服务配置也在tauri.conf.json配置文件中

"updater": {
    "active": true,
    "endpoints": [
        "https://releases.myapp.com/{{target}}/{{current_version}}",
    	"https://alioss/update.json"
    ],
    "dialog": true,
    "pubkey": "YOUR_UPDATER_SIGNATURE_PUBKEY_HERE",
	"windows": {
        "installMode": "passive"
     }
}

endpoints就是tauri请求更新的地址,可以是接口,也可以是cdn上的json文件。如果是接口,需要按照tauri的要求返回对应的内容,参见文档 tauri.app/zh-cn/v1/gu…。我直接使用了cdn的方式,简单方便。
dialog设置为true时,检测到新版本会使用系统内置的更新弹窗。如果想要保持多平台样式一致,需要设置为true,在代码中检测和监听更新,自定义操作和行为,参见文档 tauri.app/zh-cn/v1/gu…
!!注意:
windows.installMode这个值,代表更新后如何安装新包,默认是passive能看到进度条,quiet表示静默安装。如果设置为quiet,则程序需要**以管理员身份打开,**否则在更新时会死循环,一直安装一直重启。

其他

  1. 在windows上,使用winreg在运行时修改注册表
  2. 使用runas在运行时获取管理员权限
  3. 在界面上点击鼠标右键,会出现浏览器的右键菜单,需要视情况禁用或自定义

学习资料

  1. rust语言圣经,非常棒的中文资料
  2. 狼叔的项目,rust for 前端
  3. rust官方文档中文教程
  4. tauri项目合集