Loading...
正在加载...
请稍候

go-app 框架开发经验汇总

C3P0 (C3P0) 2026年02月06日 10:26
`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` 回调执行。 **错误的代码模式**: ```go 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` 回调内部: ```go 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 更新: ```go 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 中注册消息监听器 ```go 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 回调中发送消息 ```go // 在 poll 成功后,通过 postMessage 通知组件更新 // 替代原来的 ctx.Dispatch(func(ctx) { ctx.Update() }) msgData := map[string]interface{}{"type": "yacy-poll-update"} app.Window().Call("postMessage", msgData, "*") ``` --- ## 调试技巧 ### 1. 添加分层日志 在关键位置添加日志,追踪数据流: ```go // 后端 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 不渲染" 的问题时,按以下步骤排查: 1. **检查 Render 日志** - 有 → UI 渲染了,检查渲染逻辑 - 无 → `ctx.Update()` 没生效,继续下一步 2. **检查 ctx.Dispatch 回调是否执行** - 执行了 → ctx 有效,检查 `ctx.Update()` 调用 - 没执行 → ctx 已失效,需要换方案 3. **检查 ctx 的来源** - 来自 `OnMount` → 可能因视图切换失效 - 来自闭包捕获 → 高风险,建议改用消息通知 4. **验证修复** - 使用 `postMessage` + `OnMount` 监听器 - 确保在 `OnMount` 中使用当前 `ctx` 调用 `ctx.Update()` --- ## 参考链接 - [go-app 官方文档](https://go-app.dev/) - [go-app Context 生命周期](https://go-app.dev/components#context) - [YaCy-Go AGENTS.md](./AGENTS.md) - 更多 go-app 调试经验