【go-app】Web UI 更新丢失问题排查经验
问题现象
在 YaCy-Go 联邦搜索功能中,后端日志显示 P2P 搜索成功返回了 20-30 个结果,前端 poll 回调也正确执行并更新了组件状态(s.Results len:10),但 WebUI 始终不渲染这些结果。
关键日志特征
✅ Poll1 applied. Results:10 Status:searching ← 状态已更新
✅ Poll2 applied. Results:10 Status:searching ← 但没有 Render 日志!
缺失的日志:每次 poll 后没有看到 [YaCy-Search][Render] 日志,说明 ctx.Update() 没有触发组件重渲染。
根本原因
go-app 的 Context 生命周期
go-app 框架中,app.Context(简称 ctx)是组件与框架通信的桥梁:
┌─────────────────────────────────────────────────────────────┐
│ Component Instance │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ OnMount │───▶│ Render │───▶│ Events │ │
│ └────┬────┘ └─────────┘ └────┬────┘ │
│ │ ▲ │ │
│ ▼ │ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ app.Context (ctx) │ │
│ │ - ctx.Update() → 触发重渲染 │ │
│ │ - ctx.Dispatch() → 安全地更新状态 │ │
│ │ - ctx.Async() → 异步操作 │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
关键约束:ctx 仅在特定生命周期内有效:
OnMount 回调执行期间ctx.Dispatch 回调执行期间- 事件处理函数执行期间
问题场景:视图切换导致 ctx 失效
时间线:
────────────────────────────────────────────────────────────────▶
T0: Hero 视图
└─ OnMount(ctx₁)
└─ performSearch(ctx₁)
└─ ctx₁.Dispatch() → startPolling(ctx₁) ← 捕获 ctx₁
T1: ctx₁.Update() 触发视图切换 Hero → Results
└─ 组件重建!
└─ OnMount(ctx₂) ← 新的 ctx₂
T2: poll 回调执行
└─ 使用 ctx₁.Dispatch(...) ← ctx₁ 已失效!
└─ ctx₁.Update() 不起作用 ← 更新丢失!
核心问题:startPolling 闭包捕获了 ctx₁,但当视图从 Hero 切换到 Results 时,组件被重建,产生了新的 ctx₂。后续 poll 回调仍使用失效的 ctx₁ 调用 ctx.Update(),导致更新丢失。
解决方案
方案:postMessage 事件通知机制
使用浏览器的 postMessage API 作为桥梁,让 poll 回调通知组件更新,组件在 OnMount 中监听消息并使用当前有效的 ctx 来触发重渲染。
┌─────────────────────────────────────────────────────────────┐
│ Poll Callback (setTimeout) Component (OnMount) │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 更新组件状态 │ │ message listener │ │
│ │ s.Results = ... │ │ (有效的 ctx₂) │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ │ postMessage({type: │ │
│ │ "yacy-poll-update"}) │ │
│ │ ─────────────────────────────────▶│ │
│ │ │ │
│ │ ctx₂.Dispatch(...) │
│ │ ctx₂.Update() ✅ │
│ │ │ │
│ ▼ ▼ │
│ 组件重渲染! │
└─────────────────────────────────────────────────────────────┘
代码实现
1. OnMount 中注册消息监听器
func (s *SearchPage) OnMount(ctx app.Context) {
// ... 其他初始化代码 ...
// 注册 message 监听器,用于接收 poll 更新通知
app.Window().Call("addEventListener", "message",
app.FuncOf(func(this app.Value, args []app.Value) interface{} {
if len(args) == 0 {
return nil
}
event := args[0]
data := event.Get("data")
if !data.Truthy() {
return nil
}
// 检查消息类型
msgType := data.Get("type").String()
if msgType == "yacy-poll-update" {
// 使用当前有效的 ctx 触发更新
ctx.Dispatch(func(ctx app.Context) {
ctx.Update()
})
}
return nil
}))
}
2. Poll 回调中发送消息
// 在 poll 成功后,通过 postMessage 通知组件更新
// 替代原来的 ctx.Dispatch(func(ctx) { ctx.Update() })
msgData := map[string]interface{}{"type": "yacy-poll-update"}
app.Window().Call("postMessage", msgData, "*")
调试技巧
1. 添加分层日志
在关键位置添加日志,追踪数据流:
// 后端
fmt.Printf("📥 P2P incremental: +%d from %s (total: %d)\n", ...)
fmt.Printf("📊 GetSessionResults: merged %d results\n", ...)
// 前端
s.log("poll", "✅ Poll", s.pollCount, "applied. Results:", len(s.Results))
s.log("Render", "🎨 HasSearched:", s.HasSearched, "ResultCount:", len(s.Results))
2. 检查 Render 日志
如果状态更新后没有 Render 日志 → ctx.Update() 失效
✅ Poll1 applied. Results:10 ← 状态更新了
← 但没有 🎨 Render 日志!
3. 检查 PollCount 是否递增
如果 PollCount 始终为 0 → ctx.Dispatch 回调没有执行
[poll] Tick. Session:xxx PollCount:0 ← 始终是 0
[poll] Tick. Session:xxx PollCount:0 ← ctx.Dispatch 回调没执行
核心经验总结
✅ DO(推荐做法)
| 场景 | 推荐做法 |
|---|
| 异步操作后更新 UI | 使用 postMessage + OnMount 监听器 |
| 组件内更新 | 在事件回调中直接使用当前 ctx |
| 定时任务 | 使用 JS setInterval,不依赖捕获的 ctx |
| 状态同步 | 先更新状态,再通过消息通知重渲染 |
❌ DON'T(避免做法)
| 反模式 | 问题 |
|---|
在 goroutine 中捕获 ctx | ctx 可能在 goroutine 执行时已失效 |
长时间持有 ctx 引用 | ctx 在组件重建后失效 |
依赖 ctx.Dispatch 跨视图切换 | 视图切换会导致 ctx 失效 |
在 setTimeout 回调中使用旧 ctx | JS 回调执行时 ctx 可能已失效 |
🔑 关键原则
永远不要假设 ctx 在异步回调中仍然有效。
如果需要在异步操作后更新 UI,使用事件通知机制,让组件在 OnMount 中用当前有效的 ctx 来触发更新。
问题排查清单
遇到 "状态更新了但 UI 不渲染" 的问题时,按以下步骤排查:
- 检查 Render 日志
- 有 → UI 渲染了,检查渲染逻辑
- 无 →
ctx.Update() 没生效,继续下一步
- 检查 ctx.Dispatch 回调是否执行
- 执行了 → ctx 有效,检查
ctx.Update() 调用
- 没执行 → ctx 已失效,需要换方案
- 检查 ctx 的来源
- 来自
OnMount → 可能因视图切换失效
- 来自闭包捕获 → 高风险,建议改用消息通知
- 验证修复
- 使用
postMessage +
OnMount 监听器
- 确保在
OnMount 中使用当前
ctx 调用
ctx.Update()
参考链接