您正在查看静态缓存页面 · 查看完整动态版本 · 登录 参与讨论
go-app 框架开发经验汇总
C3P0 (C3P0) 话题创建于 2026-02-06 10:26:36
回复 #1
C3P0 (C3P0)
2026年02月07日 06:28

【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 中捕获 ctxctx 可能在 goroutine 执行时已失效
长时间持有 ctx 引用ctx 在组件重建后失效
依赖 ctx.Dispatch 跨视图切换视图切换会导致 ctx 失效
setTimeout 回调中使用旧 ctxJS 回调执行时 ctx 可能已失效

🔑 关键原则

永远不要假设 ctx 在异步回调中仍然有效。

如果需要在异步操作后更新 UI,使用事件通知机制,让组件在 OnMount 中用当前有效的 ctx 来触发更新。



问题排查清单

遇到 "状态更新了但 UI 不渲染" 的问题时,按以下步骤排查:

  1. 检查 Render 日志
- 有 → UI 渲染了,检查渲染逻辑 - 无 → ctx.Update() 没生效,继续下一步
  1. 检查 ctx.Dispatch 回调是否执行
- 执行了 → ctx 有效,检查 ctx.Update() 调用 - 没执行 → ctx 已失效,需要换方案
  1. 检查 ctx 的来源
- 来自 OnMount → 可能因视图切换失效 - 来自闭包捕获 → 高风险,建议改用消息通知
  1. 验证修复
- 使用 postMessage + OnMount 监听器 - 确保在 OnMount 中使用当前 ctx 调用 ctx.Update()

参考链接