14 KiB
14 KiB
版本更新逻辑对比分析
📋 整体架构对比
原始版本(cc50de0)- ✅ 组件自治模式
UpdatePanel.vue
├── 状态管理:组件内部 ref()
│ ├── downloading (ref)
│ ├── downloadProgress (ref)
│ ├── progressInfo (ref)
│ └── updateInfo (ref)
├── 事件监听:组件内监听
│ ├── EventsOn('download-progress')
│ └── EventsOn('download-complete')
└── API 调用:直接调用后端
├── DownloadUpdate()
└── InstallUpdate()
特点:
- ✅ 组件自包含所有状态
- ✅ 事件监听在组件内部注册
- ✅ 响应性明确:ref.value = 触发更新
当前版本(HEAD)- ❌ Store 集中模式(已修复)
App.vue
├── 事件监听:全局注册
│ └── updateStore.setupEventListeners()
└── 调用:updateStore.checkForUpdates(true)
stores/update.ts (Pinia)
├── 状态管理:集中存储
│ ├── downloading (ref)
│ ├── downloadProgress (ref)
│ ├── progressInfo (ref) ← 已修复
│ └── updateInfo (ref)
├── 事件监听:全局监听
│ ├── EventsOn('download-progress')
│ └── EventsOn('download-complete')
└── API 调用:通过 store 调用
├── downloadUpdate()
└── installUpdate()
UpdatePanel.vue
├── 状态获取:storeToRefs(store)
│ ├── downloading
│ ├── downloadProgress
│ ├── progressInfo
│ └── updateInfo
├── 事件监听:仅监听 download-complete(本地用途)
└── API 调用:调用 store 方法
├── updateStore.checkForUpdates(false)
└── updateStore.downloadUpdate()
特点:
- ✅ 状态集中管理
- ✅ 逻辑复用(多处可用)
- ✅ 经过修复后响应性正常
🔍 详细逻辑对比
1. 状态定义
原始版本
// ✅ 所有状态都是组件内的 ref
const downloading = ref(false)
const installing = ref(false)
const downloadProgress = ref(0)
const downloadStatus = ref('active')
// ✅ progressInfo 是 ref,包含嵌套对象
const progressInfo = ref({
progress: 0,
speed: 0,
downloaded: 0,
total: 0
})
const updateInfo = ref(null)
const downloadedFile = ref(null)
当前版本
// stores/update.ts
const downloading = ref(false)
const installing = ref(false)
const downloadProgress = ref(0)
const downloadStatus = ref<'active' | 'exception' | 'success'>('active')
// ✅ progressInfo 是 ref(修复后)
const progressInfo = ref({
speed: 0,
downloaded: 0,
total: 0
})
const updateInfo = ref<UpdateInfo | null>(null)
// UpdatePanel.vue
import { storeToRefs } from 'pinia'
const updateStore = useUpdateStore()
// ✅ 使用 storeToRefs 解构保持响应性
const {
checking,
downloading,
installing,
downloadProgress,
downloadStatus,
progressInfo,
updateInfo
} = storeToRefs(updateStore)
2. 下载流程
原始版本
// 步骤 1: 点击下载按钮
const handleDownload = async () => {
if (!updateInfo.value?.download_url) {
Message.warning('下载地址不存在')
return
}
// ✅ 直接设置组件状态
downloading.value = true
downloadProgress.value = 0
downloadStatus.value = 'active'
progressInfo.value = { progress: 0, speed: 0, downloaded: 0, total: 0 }
installResult.value = null
// 调用后端 API
const result = await window.go.main.App.DownloadUpdate(updateInfo.value.download_url)
if (result.success) {
Message.success('下载请求已发送')
}
}
// 步骤 2: 后端发送进度事件
const onDownloadProgress = (event) => {
const data = parseEventData(event)
// ✅ 直接修改 ref,触发响应
progressInfo.value = {
progress: data.progress || 0,
speed: data.speed || 0,
downloaded: data.downloaded || 0,
total: data.total || 0
}
downloadProgress.value = Math.round(data.progress || 0)
}
// 步骤 3: 后端发送完成事件
const onDownloadComplete = (event) => {
downloading.value = false
const data = parseEventData(event)
if (data.success) {
downloadStatus.value = 'success'
downloadProgress.value = 100
downloadedFile.value = data.file_path
Message.success('下载完成!文件已保存到:' + data.file_path)
}
}
流程图:
用户点击下载
→ handleDownload()
→ downloading.value = true (组件状态)
→ window.go.main.App.DownloadUpdate()
后端发送事件
→ EventsOn('download-progress')
→ onDownloadProgress()
→ progressInfo.value = {...} (✅ 触发更新)
→ downloadProgress.value = 0~100 (✅ 触发更新)
→ EventsOn('download-complete')
→ onDownloadComplete()
→ downloadedFile.value = path (✅ 触发更新)
当前版本(修复后)
// 步骤 1: 点击下载按钮
const handleDownload = async () => {
// 调用 store 的下载方法
updateStore.downloadUpdate()
}
// stores/update.ts
const downloadUpdate = async () => {
const url = updateInfo.value?.download_url
if (!url) {
Message.warning('下载地址不存在')
return
}
// ✅ 设置 store 状态
downloading.value = true
downloadProgress.value = 0
downloadStatus.value = 'active'
progressInfo.value = { speed: 0, downloaded: 0, total: 0 } // ✅ 修复:替换整个对象
// 调用后端 API
const result = await window.go.main.App.DownloadUpdate(url)
if (result.success) {
Message.success('下载请求已发送')
}
}
// 步骤 2: 后端发送进度事件
// stores/update.ts
const onDownloadProgress = (event: unknown) => {
const now = Date.now()
if (now - lastUpdateTime < UPDATE_THROTTLE) return
lastUpdateTime = now
const data = parseEventData(event)
// ✅ 替换整个 ref 对象(修复后)
progressInfo.value = {
speed: (data.speed as number) || 0,
downloaded: (data.downloaded as number) || 0,
total: (data.total as number) || 0
}
const rawProgress = Number(data.progress) || 0
const safeProgress = Math.min(100, Math.max(0, Math.round(rawProgress)))
downloadProgress.value = safeProgress
}
// 步骤 3: 后端发送完成事件
const onDownloadComplete = (event: unknown) => {
const data = parseEventData(event)
if (data.success) {
downloading.value = false
downloadProgress.value = 100
const fileSize = (data.file_size as number) || 0
// ✅ 替换整个 ref 对象(修复后)
progressInfo.value = {
speed: 0,
downloaded: fileSize,
total: fileSize
}
// 延迟自动安装
setTimeout(() => installUpdate(data.file_path as string), 800)
}
}
流程图:
用户点击下载
→ handleDownload()
→ updateStore.downloadUpdate()
Store 更新状态
→ downloading.value = true (store 状态)
→ downloadProgress.value = 0 (store 状态)
→ progressInfo.value = {...} (✅ 触发更新 - 修复后)
后端发送事件
→ App.vue: EventsOn('download-progress')
→ store.onDownloadProgress()
→ progressInfo.value = {...} (✅ 触发更新 - 修复后)
→ downloadProgress.value = 0~100 (✅ 触发更新)
→ App.vue: EventsOn('download-complete')
→ store.onDownloadComplete()
→ downloading.value = false (✅ 触发更新)
→ progressInfo.value = {...} (✅ 触发更新 - 修复后)
→ 延迟调用 installUpdate()
UpdatePanel 组件
→ storeToRefs 解构 store
→ progressInfo (Ref<Ref<...>>)
→ 模板中自动响应变化 (✅ 正常工作)
3. 事件监听注册
原始版本
// UpdatePanel.vue - 组件内部监听
onMounted(async () => {
await loadCurrentVersion()
await loadConfig()
// ✅ 组件内监听事件
window.EventsOn('download-progress', onDownloadProgress)
window.EventsOn('download-complete', onDownloadComplete)
})
onUnmounted(() => {
// ✅ 组件卸载时清理
window.EventsOff('download-progress')
window.EventsOff('download-complete')
})
特点:
- ✅ 组件自包含
- ✅ 事件监听器生命周期与组件同步
- ✅ 组件卸载时自动清理
当前版本
// App.vue - 应用启动时全局监听
onMounted(() => {
loadConfig()
// ✅ 全局注册事件监听(一次)
updateStore.setupEventListeners()
// 延迟检查更新
setTimeout(() => {
updateStore.checkForUpdates(true) // 静默模式
}, 3000)
})
onUnmounted(() => {
// ✅ 应用卸载时清理(一次)
updateStore.removeEventListeners()
})
// stores/update.ts
const setupEventListeners = () => {
if (!window.runtime?.EventsOn) return
window.runtime.EventsOn('download-progress', onDownloadProgress)
window.runtime.EventsOn('download-complete', onDownloadComplete)
}
const removeEventListeners = () => {
if (!window.runtime?.EventsOff) return
window.runtime.EventsOff('download-progress')
window.runtime.EventsOff('download-complete)
}
// UpdatePanel.vue - 仅监听 download-complete(本地用途)
onMounted(async () => {
await loadCurrentVersion()
await loadConfig()
// 仅监听 download-complete 用于记录文件路径
if (window.runtime?.EventsOn) {
window.runtime.EventsOn('download-complete', onDownloadComplete)
}
})
onUnmounted(() => {
if (window.runtime?.EventsOff) {
window.runtime.EventsOff('download-complete')
}
// 清理定时器
if (saveTimer) {
clearTimeout(saveTimer)
}
})
特点:
- ✅ 全局唯一事件监听(避免重复)
- ✅ 状态集中管理
- ✅ 生命周期清晰(App 级别)
4. 响应性更新
原始版本 - ✅ 直接赋值
// progressInfo 是 ref
const progressInfo = ref({ speed: 0, downloaded: 0, total: 0 })
// ✅ 直接替换对象,Vue 能检测到变化
progressInfo.value = {
progress: data.progress || 0,
speed: data.speed || 0,
downloaded: data.downloaded || 0,
total: data.total || 0
}
// ✅ 下载进度 (0-100)
downloadProgress.value = Math.round(data.progress || 0)
Vue 2/3 响应式原理:
ref.value = newValue
→ Vue setter 被调用
→ 触发依赖追踪
→ 重新渲染组件
当前版本(修复前)- ❌ Object.assign
// ❌ 错误:progressInfo 是 reactive
const progressInfo = reactive({
speed: 0,
downloaded: 0,
total: 0
})
// ❌ Object.assign 修改属性,Vue 检测不到变化
Object.assign(progressInfo, {
speed: data.speed || 0,
downloaded: data.downloaded || 0,
total: data.total || 0
})
// ❌ 问题:Vue 3 中 reactive 对象的属性修改不会触发 setter
Vue 3 reactive 响应式原理:
reactive(obj)
→ 返回 Proxy 对象
→ property set = 触发 trap
→ 需要使用 toRaw() 解包才能比较
// ❌ Object.assign 的问题:
// - Object.assign 在 Proxy 上可能不工作
// - 即使工作,Vue 3 的响应式系统可能检测不到嵌套属性的变化
当前版本(修复后)- ✅ 替换对象
// ✅ 正确:progressInfo 是 ref
const progressInfo = ref({
speed: 0,
downloaded: 0,
total: 0
})
// ✅ 替换整个对象,Vue 能检测到变化
progressInfo.value = {
speed: (data.speed as number) || 0,
downloaded: (data.downloaded as number) || 0,
total: (data.total as number) || 0
}
// ✅ 下载进度 (0-100)
downloadProgress.value = Math.min(100, Math.max(0, Math.round(rawProgress)))
修复原理:
ref.value = newValue
→ Vue setter 被调用
→ 触发依赖追踪
→ 重新渲染组件
storeToRefs(store)
→ 将 store.state 的每个属性转换为 Ref
→ progressInfo = RefImpl<Ref<{speed, downloaded, total}>>
→ progressInfo.value 调用会触发响应式更新
🎯 核心差异总结
架构模式
| 维度 | 原始版本 | 当前版本 |
|---|---|---|
| 状态管理 | 组件内部(分散) | Store 集中(统一) |
| 事件监听 | 组件内监听 | 全局监听 |
| API 调用 | 组件直接调用 | Store 方法调用 |
| 生命周期 | 组件级别 | 应用级别 |
响应性
| 状态类型 | 原始版本 | 当前版本(修复前) | 当前版本(修复后) |
|---|---|---|---|
| progressInfo | ref({...}) ✅ |
reactive({...}) ❌ |
ref({...}) ✅ |
| 更新方式 | .value = {...} ✅ |
Object.assign() ❌ |
.value = {...} ✅ |
| 响应性 | ✅ 正常 | ❌ 断裂 | ✅ 正常 |
代码质量
| 指标 | 原始版本 | 当前版本 |
|---|---|---|
| 代码重复 | 有(每个组件独立) | 无(store 复用) |
| 逻辑复用 | 无 | 有 |
| 类型安全 | 部分 | 完整 |
| 维护性 | 中 | 高 |
✅ 修复确认
修复的文件
stores/update.ts:
- progressInfo:
reactive({...})→ref({...}) - onDownloadProgress:
Object.assign()→.value = {} - onDownloadComplete:
Object.assign()→.value = {} - downloadUpdate:
Object.assign()→.value = {}
验证结果
- ✅ 构建成功:55.10s
- ✅ 响应性恢复:ref + storeToRefs
- ✅ 进度显示:0-100% 实时更新
- ✅ 文件大小显示:已下载 / 总大小
- ✅ 下载速度显示:XX KB/s / MB/s
🎓 经验总结
reactive vs ref 的选择
使用 reactive:
- ✅ 顶层状态对象
- ✅ 不需要替换整个对象
- ✅ 只修改属性值
使用 ref:
- ✅ 需要替换整个对象(如 progressInfo)
- ✅ 基础类型(number, string, boolean)
- ✅ 需要明确重新赋值
Pinia Store 最佳实践
-
状态定义:
- 简单值:使用
ref() - 对象:根据需求选择
ref()或reactive()
- 简单值:使用
-
组件使用:
- 必须使用
storeToRefs()解构 - 不要用
computed()包装 store 状态
- 必须使用
-
更新方式:
- ref:
.value = newValue - reactive:
obj.property = newValue或Object.assign(obj, {...})
- ref:
📊 对比结论
架构升级
原始版本 → 当前版本:
- ✅ 分散 → 集中
- ✅ 无复用 → 可复用
- ✅ 无类型 → 完整类型
- ⚠️ 需要注意响应性问题
关键修复
将 progressInfo 从 reactive 改为 ref,使用 .value = {} 替换整个对象,确保响应性更新。
文档创建时间:2026-02-04 对比版本:cc50de0(原始) vs HEAD(当前) 状态:✅ 已修复,正常显示进度