我把 Electron+Go 的 LOL 战绩工具重写成 Tauri+Rust,安装包从 128 MB 砍到 5 MB

我把 Electron+Go 的 LOL 战绩工具重写成 Tauri+Rust,安装包从 128 MB 砍到 5 MB

大家好,我是只会写 bug 的靓仔。

文章目录

  • [我把 Electron+Go 的 LOL 战绩工具重写成 Tauri+Rust,安装包从 128 MB 砍到 5 MB](#我把 Electron+Go 的 LOL 战绩工具重写成 Tauri+Rust,安装包从 128 MB 砍到 5 MB)
    • [🧱 先说老架构为什么非换不可](#🧱 先说老架构为什么非换不可)
      • [1️⃣ 128 MB 劝退用户](#1️⃣ 128 MB 劝退用户)
      • [2️⃣ 两个进程,启动和调试都难受](#2️⃣ 两个进程,启动和调试都难受)
      • [3️⃣ localhost HTTP 是隐性税](#3️⃣ localhost HTTP 是隐性税)
    • [🦀 为什么是 Tauri 2 + Rust](#🦀 为什么是 Tauri 2 + Rust)
    • [🛣️ 迁移路径:不是一锅煮的](#🛣️ 迁移路径:不是一锅煮的)
      • [前端壳为什么 1 个月就切完了](#前端壳为什么 1 个月就切完了)
      • [后端为什么磨了 8 个月](#后端为什么磨了 8 个月)
    • [🕳️ 三个大坑](#🕳️ 三个大坑)
      • [坑 1:LCU 自签证书 ------ Rust 的 API 名把 "danger" 写脸上了](#坑 1:LCU 自签证书 —— Rust 的 API 名把 "danger" 写脸上了)
      • [坑 2:Tauri 2 的 Permission 模型 ------ 配对了跑不通,报错还不告诉你](#坑 2:Tauri 2 的 Permission 模型 —— 配对了跑不通,报错还不告诉你)
      • [坑 3:Rust struct 和 TS type 的同步 ------ 手动对齐的酸爽](#坑 3:Rust struct 和 TS type 的同步 —— 手动对齐的酸爽)
    • [🎁 顺手白捡的一个好处:自动更新](#🎁 顺手白捡的一个好处:自动更新)
    • [📦 效果对比(有图有真相)](#📦 效果对比(有图有真相))
    • [💬 几句废话](#💬 几句废话)

之前发过几篇 LOL 排位分析工具的帖子(项目介绍 | v1.2 更新),当时还是 Electron + Go 的架构。有老哥评论说"这么大?",说实话我自己也觉得 ------ 一个查战绩的小工具,安装包 128 MB,下它比打一把排位还久。

所以去年花了几个月,把整个技术栈从 Electron+Go 迁到了 Tauri 2 + Rust。结果:

  • 安装包:128 MB → 5 MB(缩小 96%,GitHub Release 截图为证)
  • 冷启动:~1.5s → ~500ms(没上 profiler 仔细测,就是录屏数帧 + 肉眼掐表的量级感受,这个具体数字别太较真)
  • 进程架构:Electron 壳 + Go server 两个程序 → 单个 Tauri 二进制(砍掉独立后端进程和 localhost HTTP 那一跳)
  • 内存:~306 MB → ~241 MB(WebView2 那部分是系统共享的,边际占用远小于这个数)

下面把迁移过程和踩的坑记录一下,如果你也在做类似的桌面端项目,或者 Electron / Tauri 之间纠结,这篇应该能帮你省点时间。


🧱 先说老架构为什么非换不可

最早的设计是这样的:

复制代码
┌─────────────────────────────────┐
│  Electron 31 主进程              │
│  ┌───────────────────────────┐  │
│  │ Vue 3 + TDesign UI        │  │  ← 用户看到的界面
│  └───────────┬───────────────┘  │
│              │ fetch / axios     │
└──────────────┼──────────────────┘
               │ localhost HTTP
               ▼
┌─────────────────────────────────┐
│  Go HTTP Server (localhost:xxx) │  ← 独立子进程
│  - LCU API client               │
│  - 自动化逻辑 / 战绩聚合         │
└─────────────────────────────────┘
               │ HTTPS (LCU 自签证书)
               ▼
┌─────────────────────────────────┐
│  LeagueClientUx (LOL 客户端)     │
└─────────────────────────────────┘

能跑。但三个绕不开的痛点:

1️⃣ 128 MB 劝退用户

Electron 自带 Chromium runtime,光这就 60~80 MB,加上 Go binary 30 MB+。v1.0 打出来 128 MB。后来挤了又挤,稳定在 85~93 MB。

一个查战绩的工具下 100 多 MB,用户等下载的时间够再开一把了。这是后来下定决心换 Tauri 最直接的原因。

💡 体积不是"技术债",是用户会不会用你的第一道门槛。

2️⃣ 两个进程,启动和调试都难受

启动流程:Electron 起 → 拉起 Go server → Go 监听端口 → Electron 前端轮询 Go 是否就绪 → 才能调 LCU。任何一环卡住都是"加载中"。

Go 那边 panic 了,Electron 这边只看到 fetch 超时。调试得在两个终端间来回切,日志打两份。这种痛谁用谁知道。

3️⃣ localhost HTTP 是隐性税

每次前端调后端:JS 对象 → JSON 序列化 → HTTP body → loopback 网络 → Go 反序列化 → 业务逻辑 → 再原路返回。虽然是 loopback,HTTP 头解析、TCP 握手、JSON 双向序列化的开销都是实实在在的。查战绩这种"一次拉 10 个召唤师 + 各自 20 场"的场景,延迟肉眼可见。


🦀 为什么是 Tauri 2 + Rust

Tauri 1 vs 2:Tauri 1 在 Windows 用 Edge HTML 兜底,2 全面切 WebView2,兼容性和稳定性好太多。插件系统也重做了(v2),autostart / single-instance / fs 都有一等公民支持。还有新的 Capability + Permission 模型,后面会说到踩坑。

为什么不是 Go 了 :Tauri 所有 #[tauri::command]、State、AppHandle、事件总线都是 Rust API。继续用 Go 等于再绕一层 cgo/HTTP,那迁了个寂寞。Rust 直接编译进 Tauri 主进程,单二进制分发,不需要子进程。

那为什么不是 Wails? 这个问题我猜 Go 老哥们第一个就要问 ------ Wails 不就是 Go + 系统 WebView 嘛,照理说我后端一行都不用重写,最省事。我也认真纠结过。最后没选它,原因挺主观的:一是当时 Wails 在多窗口、自动更新、权限这些一等公民支持上,体感不如 Tauri 2 厚实,遇到坑社区里能搜到的答案也少;二是说实话有点私心,就想趁这个项目顺手学一下 Rust。所以这不是"Wails 不行",而是"我想学 Rust + 赌 Tauri 生态"。如果你就是想保留 Go 又要小体积,Wails 完全值得先试一把,别被我带跑。

UI 库也顺便从 TDesign 换成了 Naive UI,暗色主题和表格组件更顺。这一步单独做了一周,没掺在迁移里。


🛣️ 迁移路径:不是一锅煮的

很多博客写"我用一个周末把 X 重写成 Y"。我没那么神。实际上是两条线分头推,节奏完全不一样:

迁移线 起点 终点 耗时
前端壳:Electron → Tauri 2025-03-30(v1.5.4 双轨试水) 2025-04-19(v1.5.6 纯 Tauri) ~1 个月
后端语言:Go → Rust 2025-03-31 2025-12-13(删 4274 行 Go) ~8 个月

前端壳为什么 1 个月就切完了

用户用脚投票。 v1.5.4 同时发了 Electron 86 MB 和 Tauri 10 MB 两个包:

同一天、同一版本、同一功能集,86.3 MB vs 10.2 MB。这还纠结啥?2 周后 v1.5.6 就只发 Tauri 了。

后端为什么磨了 8 个月

因为对用户来说,Go server 也好、Rust 也好,功能没差。没有压差就不急:

  • 2025-03-31:新建 Tauri 目录,跟旧 Go 目录并存
  • 2025-04 ~ 2025-12 :一个 endpoint 一个 endpoint 往 Rust 搬。期间没冻需求,新功能照加
  • 2025-12-13-4274 行 Go / 34 个文件,旧服务彻底删

为什么不一刀切?因为用户在线啊。项目每 1-2 周发一版,停下来做半年大重写不现实。旧 Go server 保留着继续吃 bug fix,新功能直接写 Rust,旧 endpoint 按频率从高到低搬过去,搬完一个前端就切流量。并存期间项目目录长这样(确实丑,但能持续发版):

复制代码
.
├── lol-record-analysis-app/         # 旧 Electron 前端(4 月废弃)
├── lol-record-client-golang/        # 旧 Go 后端(活到 12 月)
└── lol-record-analysis-tauri/       # 新 Tauri + Rust + Vue

🕳️ 三个大坑

每个都卡了我至少半天,写出来希望大家别再踩。

坑 1:LCU 自签证书 ------ Rust 的 API 名把 "danger" 写脸上了

LCU API 是 HTTPS,但用的是每次客户端启动时动态生成的自签证书。Go 里关掉 TLS 校验很简单:

go 复制代码
tr := &http.Transport{
    TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}

到了 Rust 的 reqwest

rust 复制代码
let client = reqwest::Client::builder()
    .danger_accept_invalid_certs(true)
    .build()?;

看到 danger_accept_invalid_certs 这个名字的时候我愣了一下 ------ 但 LCU 场景下这是唯一解,Riot 不可能给你 CA 证书。

更大的坑是 port 和 auth-token 怎么拿。 这俩是 LCU 每次启动随机生成的,写在自己进程的命令行参数里(--app-port=xxxxx --remoting-auth-token=yyy)。网上清一色用 wmic 去查,但那玩意要管理员权限 。我之前专门写过一篇 无管理员权限的获取方法(全网首发 Go),原理是调 Windows 的 NtQueryInformationProcess API:

rust 复制代码
use windows::Win32::System::Threading::*;
let h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid)?;
let mut buf = vec![0u8; 4096];
let mut ret_len = 0u32;
NtQueryInformationProcess(
    h, ProcessCommandLineInformation,
    buf.as_mut_ptr() as _, buf.len() as _, &mut ret_len
)?;
// 解析 UNICODE_STRING → 正则抠 --app-port / --remoting-auth-token

PROCESS_QUERY_LIMITED_INFORMATION 是低权限级别,拿不到敏感数据。但进程命令行不算敏感 ------ Windows 设计里被忽视的一个口子,刚好让我们绕过了管理员要求。

NtQueryInformationProcess 吐出来的那坨 buffer,怎么变成 port 和 token?谜底就在这块 buffer 的结构上 ------ 它开头是一个 UNICODE_STRINGLength + MaximumLength + 一个指向宽字符串的指针),真正的命令行数据就跟在后面。把宽字符串转成 String,剩下的就是正则的活了:

rust 复制代码
#[repr(C)]
struct UnicodeString { length: u16, maximum_length: u16, buffer: *mut u16 }

// buf 就是上面 NtQueryInformationProcess 填好的缓冲区
let us = unsafe { &*(buf.as_ptr() as *const UnicodeString) };
let wide = unsafe { std::slice::from_raw_parts(us.buffer, (us.length / 2) as usize) };
let cmdline = String::from_utf16_lossy(wide);

// 命令行里把这两个随机值抠出来
let re_port  = Regex::new(r"--app-port=(\d+)").unwrap();
let re_token = Regex::new(r"--remoting-auth-token=([\w-]+)").unwrap();
let port  = re_port.captures(&cmdline).and_then(|c| c.get(1)).map(|m| m.as_str().to_string());
let token = re_token.captures(&cmdline).and_then(|c| c.get(1)).map(|m| m.as_str().to_string());

(边界检查和错误处理我省了,能看懂思路就行;完整实现在上面那篇全网首发的文里。)

坑 2:Tauri 2 的 Permission 模型 ------ 配对了跑不通,报错还不告诉你

Electron 安全模型基本就是开/关 nodeIntegration 二选一。Tauri 2 是白名单粒度,每个能力都要显式声明:

json 复制代码
{
  "permissions": [
    "core:default",
    "shell:allow-open",
    "http:default",
    "core:window:allow-start-dragging"
  ]
}

第一次写很容易陷入**"代码明明对了为啥跑不通"**的死循环 ------ 因为权限不足的报错不指向配置文件,而是给你一个看起来像"函数不存在"的错误。比如我漏配 dialog 权限时,前端 invoke 收到的是这么一句:

text 复制代码
command plugin:dialog|open not allowed. Permissions associated with this command: dialog:allow-open

乍一看像命令名写错了,对着代码翻来覆去找不出毛病。其实谜底就在报错的后半句 ------ 它已经把你要加的权限名(dialog:allow-open)报出来了,只是第一次根本不会想到往那看。我在这上面浪费了大半天。

后来踩熟了,记了几个具体坑:

  • http:default 要写两遍:第一遍启用 fetch 命令,第二遍配 URL 白名单 scope。只写第一遍,命令存在但 fetch 调用静默失败,连个报错都没有
  • 动态窗口要通配符 :项目给每场对局弹详情窗口,label 是动态的(match-detail-{id}),capability 里得写 "windows": ["main", "match-detail-*"]。漏了就所有 command 调不通
  • core:window:allow-start-dragging:项目禁了原生标题栏,整个拖拽靠这个 API。漏配了窗口就拖不动,你猜我怎么发现的

📌 经验 :每加一个新 Tauri command 或新窗口,第一件事查 capabilities/default.json。权限先行,代码后行。

坑 3:Rust struct 和 TS type 的同步 ------ 手动对齐的酸爽

旧前端调 Go 是 fetch,新前端调 Rust 是 invoke

ts 复制代码
// 旧
const data = await fetch('/api/match-history?puuid=xxx').then(r => r.json())
// 新
const data = await invoke('get_match_history', { puuid: 'xxx' })

表面上是换了个调用方式。但真正头疼的是:Rust 那边 struct 改一个字段,TS 怎么知道?

我试过 ts-rs、specta 这些自动生成工具,最后没用,纯手动对齐。原因很实际:

  1. 总共就十几个核心 struct,自动生成引入的构建管线复杂度超过了收益
  2. Rust 这边统一用 #[serde(rename_all = "camelCase")],自动转 camelCase,写 TS 的时候照着来就行
  3. TS 文件头注释直接标注对应的 Rust 文件路径:
typescript 复制代码
/**
 * AI 标签建议相关类型,与 Rust schema
 * (src-tauri/src/command/user_tag_config.rs) 严格同构。
 */
  1. CI 两边都跑 typecheck(前端 vue-tsc,后端 cargo clippy),能挡住大部分

但确实出过 bug ------ Rust 的 RecentDataselect_mode,TS 那边多写了 winslosses 两个字段,Rust 根本不返回这俩。运行时永远是 undefined不报错但数据是错的。当时前端那块胜负数永远显示空,我对着前端代码看了半天没看出毛病,最后跑去 grep Rust 的 struct 才发现:人家压根没这俩字段。这种静默 bug 比崩了还可怕,你不专门盯根本发现不了。

📌 建议:struct 超过 30 个或者多人协作,老老实实用 ts-rs / specta 自动生成。手动对齐只在一个人写、量不大的情况下才省心。


🎁 顺手白捡的一个好处:自动更新

这个本来没在计划里,迁完才发现是白捡的。

Electron 时代我也想做自动更新,但 electron-updater 那套配下来挺烦:要么自己搭个更新服务器,要么拿 GitHub 当源,还要处理签名、增量包,配置一大坨。我当时嫌麻烦一直没正经做,全靠用户自己去 Release 页手动下新版。

Tauri 2 有个官方的 tauri-plugin-updater,配置就几行:

json 复制代码
// tauri.conf.json
{
  "plugins": {
    "updater": {
      "endpoints": ["https://github.com/wnzzer/rank-analysis/releases/latest/download/latest.json"],
      "pubkey": "你的公钥"
    }
  }
}

更新源就是挂在 GitHub Release 上的一个 latest.json(等下"效果对比"那节,v1.8.2 截图里第一行那个 10.8 KB 的 latest.json 就是它),长这样:

json 复制代码
{
  "version": "1.8.2",
  "pub_date": "2026-05-24T00:00:00Z",
  "platforms": {
    "windows-x86_64": {
      "signature": "更新包的签名串",
      "url": "https://github.com/wnzzer/rank-analysis/releases/download/v1.8.2/lol-record-analysis-app-1.8.2-setup.exe"
    }
  }
}

前端启动时调一下 check(),有新版就提示下载安装,三五行的事:

ts 复制代码
import { check } from '@tauri-apps/plugin-updater'

const update = await check()
if (update) {
  await update.downloadAndInstall()  // 下完自动重启
}

这里有个容易搞混的点得说清楚 :Tauri updater 要你用一对自己生成的密钥(tauri signer generate,免费)给「更新包」签名,公钥填进上面的配置 ------ 这个签名是给自动更新做校验用的,和 Windows 上那种要花钱买的「代码签名证书」完全是两码事。我穷,没买代码签名证书,所以首次安装时 Windows SmartScreen 还是会弹个"未知发布者",这个坑我到现在没填。但自动更新本身一分钱没花就跑通了,对一个免费小工具来说,够用了。


📦 效果对比(有图有真相)

安装包体积演变

每一行都是 GitHub Release 公开记录,可以去 Releases 页 核对:

版本 日期 大小 阶段
v1.0 2025-01-13 128 MB 🟥 Electron + Go 起点
1.1 → 1.5.3 01~03月 ~85-93 MB 🟥 Electron 时代
v1.5.4 2025-03-30 86.3 MB + 10.2 MB 🟨 双轨同框
v1.5.6 2025-04-19 10.3 MB 🟩 纯 Tauri
v1.6.0 2025-10-08 6.7 MB 🟩 升 Tauri 2
v1.8.2 2026-05-24 5.01 MB 🟩 当前

起点 v1.0 ------ 128 MB

转折点 v1.5.4 ------ Electron 和 Tauri 同框

当前 v1.8.2 ------ 5.01 MB

内存占用

旧版 Electron + Go(稳态 ~306 MB)

新版 Tauri + Rust(稳态 ~241 MB)

WebView2 那 ~200MB 看着大,但系统里只要装了 Edge 或者别的 Tauri 应用,这部分就是共享的,边际内存远小于这个数。

截图里 实用工具 (6) 的 6 个进程是 WebView2 的渲染 / GPU / 管理器等辅助进程 ------ 和当年 Electron 的多进程模型一个道理,这部分跑不掉。前面"进程架构"那条说的"单个二进制",指的是 app 自己不再额外起一个 Go server 子进程 + localhost HTTP,不是说 OS 层只剩 1 个进程。


💬 几句废话

说实话,一开始决定迁的时候心里也没底。毕竟 Electron + Go 虽然胖,但它跑着呢。万一迁到一半翻车了,那才叫社死。

后来想通了:128 MB 对一个查战绩的工具来说太离谱了。 用户不会关心你用的什么框架,他们只关心下完打开能不能用。Tauri 让安装包从 128 MB 变成 5 MB ------ 不是"技术优化",是让产品有被打开的机会

如果你也在做类似的桌面端项目,我的建议就三条:

  1. 别一刀切,留旧项目并存,新目录单独建,随时能 ship
  2. 先迁用户感知最强的功能,不是代码量最小的
  3. 别在迁移里掺重构,已经够复杂了,UI 改版分开做

就这些。希望对在做类似项目的朋友有帮助。


相关链接:

觉得有用的话帮忙点个赞 👍 有问题也可以去 GitHub 提 issue 交流

相关推荐
本地化文档1 小时前
sphinxcontrib-rust-docs-l10n
python·rust·github·gitcode·sphinx
韦胖漫谈IT1 小时前
选语言不是站队,是选适合问题的工具
java·python·ai·rust·go·技术落地
特立独行的猫a2 小时前
鸿蒙 PC 平台 Rust 语言第三方库与应用移植全景指南
华为·rust·harmonyos·三方库·鸿蒙pc
红尘散仙10 小时前
我把终端小说阅读器接上了 AI Agent:TRNovel 现在能用 skill 生成书源了
人工智能·后端·rust
会编程的土豆12 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
basketball61613 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang
peterfei15 小时前
给 AI Agent 装上"长期记忆":一个 200 行 Rust 库解决 LLM 的致命短板
rust
SOC罗三炮17 小时前
OpenHuman 源码深度解构:一个 Rust 驱动的本地优先 AI 个人助手
开发语言·人工智能·rust
Generalzy18 小时前
从本地 Demo 到生产级检索:Milvus 学习笔记(1)
golang·prompt·软件工程