dev-manager-tauri/docs/tauri-updater-guide.md
lanrtop 0cdaa3678c feat: add Blueprint feature to ProjectCard and commands
- Introduced BlueprintModal in ProjectCard for project blueprint visualization.
- Added state management for displaying the BlueprintModal.
- Implemented Blueprint-related interfaces and functions in commands.ts for handling blueprint data.
2026-04-02 19:12:53 +09:00

409 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Tauri v2 客户端自动更新指南
> **方案版本**: v1.02026-04-02
> **适用范围**: Tauri v2 + 任意更新源(阿里云 OSS / GitHub Releases / 自建服务器)
> **验证状态**: 已在 dev-manager-tauri 项目实际跑通
>
> 本文档聚焦客户端侧的自动更新功能实现,涵盖从插件安装到 UI 交互的全链路。
> 服务端构建与发布流程请参考 [tauri-cicd-guide.md](./tauri-cicd-guide.md)。
---
## 整体流程
```
客户端启动 / 用户点击「检查更新」
请求 endpoint 上的 latest.json
比较 latest.json.version vs 本地版本
┌────┴────┐
│ 无更新 │ 有新版本
│ 结束 │
│ ▼
│ 展示版本号 + 更新说明
│ 用户点击「立即更新」
│ │
│ ▼
│ 下载 .exe带进度
│ │
│ ▼
│ 验证签名 → 安装 → relaunch
└─────────┘
```
---
## 一、安装依赖
### 1.1 Rust 端src-tauri/Cargo.toml
```toml
[dependencies]
tauri-plugin-updater = "2"
tauri-plugin-process = "2"
```
### 1.2 前端package.json
```bash
pnpm add @tauri-apps/plugin-updater @tauri-apps/plugin-process
```
---
## 二、注册插件
`src-tauri/src/lib.rs` 中注册两个插件:
```rust
tauri::Builder::default()
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init())
// ... 其他插件和配置
```
- `tauri_plugin_updater` — 检查更新、下载、验证签名、安装
- `tauri_plugin_process` — 安装后调用 `relaunch()` 重启应用
---
## 三、配置 tauri.conf.json
```jsonc
{
"bundle": {
"createUpdaterArtifacts": true // ⚠️ 必须!否则构建不生成 .exe.sig 签名文件
},
"plugins": {
"updater": {
"pubkey": "<公钥 base64>",
"endpoints": [
"https://<bucket>.<endpoint>/latest.json"
],
"dialog": false // false = 用代码控制更新 UItrue = 系统原生弹窗
}
}
}
```
### 关键说明
| 字段 | 作用 | 注意事项 |
|------|------|---------|
| `createUpdaterArtifacts` | 构建时生成 `.exe.sig` 签名文件 | Tauri v2 默认 false不开启则签名静默跳过 |
| `pubkey` | 客户端用来验证下载文件签名 | 是**公钥文件**的 base64不是私钥 |
| `endpoints` | `latest.json` 的 URL 列表 | 可配多个,按顺序尝试 |
| `dialog` | 是否使用原生弹窗 | 推荐 false自定义 UI 体验更好 |
### pubkey 生成方法
```powershell
# 生成密钥对
pnpm tauri signer generate -w "$HOME\.tauri\your-app.key"
# 获取公钥 base64填入 pubkey 字段)
$content = Get-Content "$HOME\.tauri\your-app.key.pub" -Raw
$content = $content -replace "`r`n", "`n"
$bytes = [System.Text.Encoding]::UTF8.GetBytes($content)
[Convert]::ToBase64String($bytes)
```
---
## 四、配置 Capabilities 权限
`src-tauri/capabilities/default.json` 中必须声明权限,否则前端 API 调用会被拦截:
```jsonc
{
"identifier": "default",
"windows": ["main"],
"permissions": [
"core:default",
// ... 其他权限 ...
{
"identifier": "http:default",
"allow": [
{ "url": "https://*.aliyuncs.com/**" } // ⚠️ 放行更新源域名
]
},
"updater:default", // ⚠️ 允许 check()、downloadAndInstall()
"process:default" // ⚠️ 允许 relaunch()
]
}
```
**三项缺一不可**
- `updater:default` — 否则报 `updater.check not allowed`
- `process:default` — 否则 `relaunch()` 无法调用
- http allow 列表包含更新源域名 — 否则报 `Could not fetch a valid release JSON`
---
## 五、latest.json 格式
更新源需要提供一个 `latest.json`,格式如下:
```json
{
"version": "0.1.11",
"pub_date": "2026-04-02T05:29:44Z",
"notes": "版本 v0.1.11 已发布",
"platforms": {
"windows-x86_64": {
"signature": "<.exe.sig 文件内容>",
"url": "https://<bucket>.<endpoint>/releases/v0.1.11/<app>_0.1.11_x64-setup.exe"
}
}
}
```
| 字段 | 说明 |
|------|------|
| `version` | 新版本号updater 用它和本地版本比较 |
| `pub_date` | ISO 8601 格式,必须有 |
| `notes` | 更新说明,前端通过 `update.body` 获取 |
| `platforms` | 按平台提供下载链接和签名 |
| `signature` | `.exe.sig` 文件内容base64客户端用 pubkey 验证 |
| `url` | 安装包下载地址,必须可公开访问 |
### 多平台支持
```json
{
"platforms": {
"windows-x86_64": { "signature": "...", "url": "...setup.exe" },
"darwin-x86_64": { "signature": "...", "url": "...app.tar.gz" },
"darwin-aarch64": { "signature": "...", "url": "...app.tar.gz" },
"linux-x86_64": { "signature": "...", "url": "...AppImage.tar.gz" }
}
}
```
---
## 六、前端实现React + TypeScript
### 6.1 状态机设计
更新流程有明确的状态流转,用联合类型建模最清晰:
```typescript
type UpdateState =
| { status: "idle" } // 初始状态
| { status: "checking" } // 正在检查
| { status: "latest" } // 已是最新
| { status: "available"; version: string; notes: string; downloadAndInstall: () => Promise<void> }
| { status: "downloading"; progress: number } // 下载中0~100
| { status: "error"; message: string } // 出错
```
### 6.2 完整组件参考
```typescript
import { useState, useEffect } from "react";
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";
import { getVersion } from "@tauri-apps/api/app";
function UpdateSection() {
const [appVersion, setAppVersion] = useState("");
const [state, setState] = useState<UpdateState>({ status: "idle" });
useEffect(() => {
getVersion().then(setAppVersion).catch(() => {});
}, []);
const handleCheck = async () => {
setState({ status: "checking" });
try {
const update = await checkUpdate();
if (!update?.available) {
setState({ status: "latest" });
return;
}
setState({
status: "available",
version: update.version,
notes: update.body ?? "",
downloadAndInstall: async () => {
let downloaded = 0;
let total = 0;
await update.downloadAndInstall((event) => {
if (event.event === "Started") {
total = event.data.contentLength ?? 0;
setState({ status: "downloading", progress: 0 });
} else if (event.event === "Progress") {
downloaded += event.data.chunkLength;
const progress = total > 0 ? Math.round((downloaded / total) * 100) : 0;
setState({ status: "downloading", progress });
} else if (event.event === "Finished") {
setState({ status: "idle" });
}
});
await relaunch();
},
});
} catch (e) {
setState({ status: "error", message: String(e) });
}
};
return (
<div>
<p>当前版本 v{appVersion}</p>
{state.status === "latest" && <span>已是最新版本</span>}
{state.status === "available" && (
<>
<span>v{state.version} 可用</span>
<button onClick={state.downloadAndInstall}>立即更新</button>
</>
)}
{state.status === "downloading" && (
<span>下载中 {state.progress}%</span>
)}
{state.status === "error" && (
<div>
<p>{state.message}</p>
<button onClick={() => navigator.clipboard.writeText(state.message)}>
复制错误
</button>
</div>
)}
{state.status !== "available" && (
<button
onClick={handleCheck}
disabled={state.status === "checking" || state.status === "downloading"}
>
{state.status === "checking" ? "检查中…" : "检查更新"}
</button>
)}
</div>
);
}
```
### 6.3 下载事件说明
`downloadAndInstall` 的回调事件顺序:
| 事件 | 数据 | 说明 |
|------|------|------|
| `Started` | `{ contentLength?: number }` | 开始下载contentLength 可能为 0 |
| `Progress` | `{ chunkLength: number }` | 每个数据块,累加计算进度 |
| `Finished` | 无 | 下载完成,随后自动安装 |
> `downloadAndInstall` 完成后立即调用 `relaunch()` 重启应用,新版本生效。
---
## 七、版本号管理
Tauri v2 项目有**三处版本号**必须保持一致:
| 文件 | 字段 | 作用 |
|------|------|------|
| `src-tauri/tauri.conf.json` | `"version"` | updater 用来和 latest.json 比较 |
| `src-tauri/Cargo.toml` | `version` | Rust 构建元数据 |
| `package.json` | `"version"` | 前端构建元数据 |
### 发版步骤
```bash
# 1. 三处改版本号(如 0.2.0
# 2. 提交
git add -A
git commit -m "release: v0.2.0"
# 3. 打 tag + pushPowerShell 不支持 &&,分开执行)
git tag v0.2.0
git push
git push --tags
```
> **版本号不一致的后果**`latest.json` 中的 version 高于本地版本时才会提示更新。
> 如果 `tauri.conf.json` 的版本没改,每次启动都会提示"有新版本"即使已经更新过。
---
## 八、更新源选择
| 更新源 | 优点 | 缺点 | 适用场景 |
|--------|------|------|---------|
| **阿里云 OSS** | 国内快CDN 加速 | 需要额外配置 OSS | 面向国内用户 |
| **GitHub Releases** | 零配置tauri-action 自动生成 | 国内访问慢/不稳定 | 面向海外用户 |
| **自建服务器** | 完全可控 | 需要运维 | 企业内网分发 |
Tauri v2 的 `endpoints` 支持多个 URL可以配置主备
```json
"endpoints": [
"https://<oss-bucket>.oss-cn-hangzhou.aliyuncs.com/latest.json",
"https://github.com/<user>/<repo>/releases/latest/download/latest.json"
]
```
---
## 九、调试技巧
### 9.1 验证 latest.json 可访问
```powershell
curl https://<your-endpoint>/latest.json
```
- 返回 JSON → 正常
- 返回 `AccessDenied` → OSS Bucket 未设为公共读
- 返回 404 → CI/CD 未上传或路径不对
### 9.2 开发模式测试
`pnpm tauri dev` 模式下也能调用 `check()`,但注意:
- 开发模式的版本号来自 `tauri.conf.json`
- 如果本地版本号和 latest.json 相同或更高,不会触发更新
### 9.3 强制触发更新(调试用)
临时把 `tauri.conf.json` 的版本号改低(如 `0.0.1`),再运行 `pnpm tauri dev` 点检查更新即可触发。
---
## 十、踩坑清单
| # | 问题 | 现象 | 解决方案 |
|---|------|------|---------|
| 1 | 缺少 `updater:default` | `updater.check not allowed` | `capabilities/default.json` 添加 `updater:default` |
| 2 | 缺少 `process:default` | `relaunch()` 无效 | `capabilities/default.json` 添加 `process:default` |
| 3 | http 未放行更新源域名 | `Could not fetch a valid release JSON` | capabilities http allow 加更新源域名 |
| 4 | OSS Bucket 为私有 | 同上curl 返回 `AccessDenied` | Bucket ACL 改为**公共读** |
| 5 | `createUpdaterArtifacts` 未开启 | 构建成功但无 `.exe.sig` | `tauri.conf.json``bundle.createUpdaterArtifacts: true` |
| 6 | pubkey 填了私钥 | `failed to decode pubkey` | 用公钥文件的 base64 |
| 7 | 版本号三处不一致 | 更新后仍提示有新版本 | `tauri.conf.json`、`Cargo.toml`、`package.json` 必须同步 |
| 8 | latest.json 缺少 `pub_date` | updater 解析失败 | 必须包含 ISO 8601 格式的日期 |
| 9 | signature 字段为空 | 签名验证失败 | 检查 CI/CD 是否正确读取了 `.exe.sig` 文件内容 |
| 10 | `dialog: true` 时无法自定义 UI | 弹出系统原生对话框 | 设为 `false`,用代码实现自定义更新界面 |
---
## 配置文件速查
新项目最小配置清单:
```
src-tauri/Cargo.toml → 添加 tauri-plugin-updater、tauri-plugin-process 依赖
package.json → pnpm add @tauri-apps/plugin-updater @tauri-apps/plugin-process
src-tauri/src/lib.rs → 注册两个插件
src-tauri/tauri.conf.json → bundle.createUpdaterArtifacts + plugins.updater 配置
src-tauri/capabilities/*.json → updater:default + process:default + http allow
前端组件 → 调用 check() + downloadAndInstall() + relaunch()
```