返回博客

微信小程序页面跳转后 WebSocket 还没重连,消息就已经丢了

某个 IM 聊天页,用户点击"选择订单"跳到订单列表,挑好之后 navigateBack 回来。理论上应该在聊天框发出一张订单卡片,但实际上:什么都没发出,没有任何错误提示,数据也已经丢失。这个 bug 的诡异之处在于,代码里该有的判断都有——只是全部在错误的时机执行了。

现象

用户从聊天页跳走再返回后,订单卡片没有发送。日志里看不到报错,sendOrderCard 方法被调用了,但进入函数第一行就因连接状态检查而返回。整个过程无声无息。

根因

问题出在 onShow 里三件事的执行顺序上:

  1. onShow 调用 connectWebSocket()——这是异步的,需要走握手、鉴权才能变成 connected
  2. onShow 同步读取 currentPage.selectedOrder,并立即 delete 掉(消费一次性数据的惯例)
  3. sendOrderCard() 检查 connectionStatus !== 'connected'此时还是 disconnected → 静默返回

数据在第 2 步被删,连接在第 3 步还没就绪,两者错开,订单永久丢失。

坑:onShow 里同步消费了数据,却等不到异步连接就绪

navigateBack 触发 onShow 时,connectWebSocket() 是异步的——它只是发起重连请求,不会阻塞后续代码。紧跟其后读取并删除 selectedOrder,再调用带连接守卫的发送函数,结果必然是:数据已消费,连接未就绪,发送静默失败。

正解

核心思路:在 onShow 里只做"检查 + 存队列",把真正的发送挂到 onAuthSuccess 回调里执行。

Page({
  // 实例属性,不放进 data——不需要响应式,不触发重渲染
  _pendingOrder: null,
 
  onShow() {
    // 1. 按需重连
    const status = chatWs.getStatus()
    if (!status.isConnected && !status.isConnecting && this.data.imToken) {
      this.connectWebSocket()
    }
 
    // 2. 读取并立即消费子页面传回的数据
    const pages = getCurrentPages()
    const currentPage = pages[pages.length - 1]
    if (currentPage.selectedOrder) {
      const order = currentPage.selectedOrder
      delete currentPage.selectedOrder  // 只消费一次
 
      // 3. 连接就绪直接发;否则入队等待
      if (this.data.connectionStatus === 'connected') {
        this.sendOrderCard(order)
      } else {
        this._pendingOrder = order
      }
    }
  },
 
  onAuthSuccess(data) {
    // 注意:用 setData 的回调,确保状态写入完成后再读队列
    this.setData({ connectionStatus: 'connected' }, () => {
      if (this._pendingOrder) {
        const order = this._pendingOrder
        this._pendingOrder = null
        this.sendOrderCard(order)
      }
    })
  },
 
  onAuthFailed() {
    this.setData({ connectionStatus: 'failed' })
    if (this._pendingOrder) {
      this._pendingOrder = null
      wx.showToast({ title: '连接失败,请重新操作', icon: 'none' })
    }
  },
})

几个细节值得注意:

  • _pendingOrder 放实例属性而非 data:它不需要驱动视图,放进 data 只会徒增 setData 开销。
  • setData 回调里发送:小程序的 setData 是异步的,setData({ connectionStatus: 'connected' }) 执行后立刻读 this.data.connectionStatus 可能还是旧值。回调里才是安全窗口。
  • 失败路径必须清队列onAuthFailedonReconnectFailed 都要清掉 _pendingOrder,否则下次连接成功时会把这次的旧数据发出去。
  • 页面销毁也要清理:在 onUnload 里清掉 _pendingOrder,防止内存泄漏。
正解:onShow 只存队列,onAuthSuccess 回调里投递

把"等连接就绪后发送"的逻辑从 onShow 移到 onAuthSuccess 的 setData 回调里,异步重连完成后自动消费队列。失败路径统一清队列并给用户提示,不留悬空数据。

一句话外卖

跨页面传递一次性数据时,消费(delete)和使用必须在同一个可靠的时间点——如果使用依赖异步就绪,就把数据存进"待发队列",让异步完成回调来投递。