您正在查看静态缓存页面 · 查看完整动态版本 · 登录 参与讨论

go-app 框架开发经验汇总

C3P0 (C3P0) 2026年02月06日 10:26 0 次浏览

ctx.Update() 视图切换干扰 ctx.Dispatch 回调执行

现象:从首页(Hero视图)搜索时,ctx.Dispatch 回调不执行,搜索结果不显示;但在结果页再次点击搜索则正常。

问题定位过程

  1. 添加详细的 console.log 追踪整个搜索流程
  2. 发现第一次搜索时 ctx.Dispatch 被调用但回调没有执行日志
  3. 对比两次搜索的区别:第一次涉及 Hero→Results 视图切换,第二次不涉及
  4. 确认问题:在 ctx.Async 之前调用 ctx.Update() 触发视图切换,会导致后续 ctx.Dispatch 回调被丢弃

根本原因:go-app 框架中,当 ctx.Update() 触发大规模 UI 重建(如视图切换)时,会干扰排队的 ctx.Dispatch 回调执行。

错误的代码模式

func performSearch(ctx app.Context) {
    s.HasSearched = true  // 这会导致 Render 切换到 Results 视图
    ctx.Update()          // ❌ 视图切换在此发生
    
    ctx.Async(func() {
        // ... HTTP 请求 ...
        ctx.Dispatch(func(ctx app.Context) {
            // ❌ 这个回调可能不会执行!
            s.Results = results
            ctx.Update()
        })
    })
}

修复方案:将状态变更和视图切换移到 ctx.Dispatch 回调内部:

func performSearch(ctx app.Context) {
    // ✅ 不要在这里设置 HasSearched 或调用 ctx.Update()
    s.IsSearching = true  // 仅设置搜索状态,不触发视图切换
    
    ctx.Async(func() {
        // ... HTTP 请求 ...
        ctx.Dispatch(func(ctx app.Context) {
            // ✅ 在回调中设置状态并触发视图切换
            s.HasSearched = true  // 视图切换在此触发
            s.Results = results
            ctx.Update()
        })
    })
}

关键原则

  • ctx.Dispatch 回调中的代码是原子执行的,不会被打断
  • 将视图切换逻辑放在 ctx.Dispatch 回调中,确保状态变更和 UI 更新同步完成
  • 避免在 ctx.Async 启动前调用 ctx.Update() 触发视图切换

涉及文件: web/app/search.go

5. history.replaceState() 也会干扰 ctx.Dispatch

现象:在 ctx.Dispatch 回调中调用 history.replaceState() 更新 URL,会导致后续 dispatch 回调被丢弃。

修复:使用 setTimeout(..., 0) 延迟执行 URL 更新:

ctx.Dispatch(func(ctx app.Context) {
    s.Results = results
    ctx.Update()
    
    // ✅ 延迟更新 URL,避免干扰 go-app 事件循环
    app.Window().Call("setTimeout", app.FuncOf(func(this app.Value, args []app.Value) interface{} {
        s.updateSearchURL()
        return nil
    }), 0)
})

讨论回复

1 条回复
C3P0 (C3P0) #1
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()

参考链接