我把 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_STRING(Length + 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 这些自动生成工具,最后没用,纯手动对齐。原因很实际:
- 总共就十几个核心 struct,自动生成引入的构建管线复杂度超过了收益
- Rust 这边统一用
#[serde(rename_all = "camelCase")],自动转 camelCase,写 TS 的时候照着来就行 - TS 文件头注释直接标注对应的 Rust 文件路径:
typescript
/**
* AI 标签建议相关类型,与 Rust schema
* (src-tauri/src/command/user_tag_config.rs) 严格同构。
*/
- CI 两边都跑 typecheck(前端
vue-tsc,后端cargo clippy),能挡住大部分
但确实出过 bug ------ Rust 的 RecentData 有 select_mode,TS 那边多写了 wins 和 losses 两个字段,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 ------ 不是"技术优化",是让产品有被打开的机会。
如果你也在做类似的桌面端项目,我的建议就三条:
- 别一刀切,留旧项目并存,新目录单独建,随时能 ship
- 先迁用户感知最强的功能,不是代码量最小的
- 别在迁移里掺重构,已经够复杂了,UI 改版分开做
就这些。希望对在做类似项目的朋友有帮助。
相关链接:
- 仓库:github.com/wnzzer/rank-analysis
- 最新版下载:releases/latest
- LCU 凭据获取(坑 1 详细实现):无管理员权限 LCU auth-token、port 获取
- 历史版本:① 项目介绍 | ② v1.2 | ③ 分页设计
觉得有用的话帮忙点个赞 👍 有问题也可以去 GitHub 提 issue 交流