RabbitMQ消息确认机制的实战踩坑与可靠传输实现

柯豪~ 交互 阅读 1,724
赞 33 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

项目上线两周后,客服开始反馈:「用户点删除按钮,点了三遍才弹出确认框」「撤回操作经常没反应,等两秒突然弹出来」。我第一反应是「这谁写的破逻辑」,结果一查,是我自己上个月写的……

RabbitMQ消息确认机制的实战踩坑与可靠传输实现

我们有个消息列表页,每条消息右滑出现「撤回」「删除」「标记已读」三个操作按钮,点击任一操作都会触发一个带文字+两个按钮(确定/取消)的确认弹窗。听起来很常规对吧?但问题就出在这儿——每次点击都新建 DOM、挂载 Vue 组件、走一遍 mounted + computed + watch 链路

实测:在中低端安卓机(Redmi Note 9)上,从点击到弹窗完全可交互,平均耗时 5.2s。不是手抖,是真·五秒起步。用户点完以为没点上,又点,结果连弹三个弹窗……

找到瘼颈了!

先开 Chrome DevTools 的 Performance 面板录了一次点击流程,火焰图里一眼看到两个红块:一个是 VueComponent._update 耗了 1800ms,另一个是 Element.insertAdjacentHTML 后跟着一堆样式重排(Layout),占了 1200ms。

再切到 Memory,点几次确认框,堆内存直线上升,GC 频繁触发——说明组件没卸载干净,或者 DOM 挂着不删。

最后用 console.time 手动埋点,在弹窗组件的 beforeCreatemountedactivated 里打点,发现:光是 new Vue() 实例化就花了 600ms,比渲染还慢。原来我们为了「解耦」,把确认框封装成独立 Vue 单文件组件,每次调用都 new ConfirmDialog({ props }) + $mount()……

踩坑提醒:Vue 2 的 new Vue().$mount() 在低配设备上就是性能黑洞,尤其带 scoped CSS 和 computed 的组件。别信「组件化就一定好」,得看场景。

试了几种方案

  • 方案一:改用全局事件总线 + 静态 DOM——提前在 body 末尾写死一个 <div id="confirm-root"></div>,所有确认框都复用这个容器,用 innerHTML 替换内容。试了,快是快了,但状态管理乱成麻,取消回调丢失、多次调用覆盖、异步 Promise 返回时机错乱……折腾两天放弃了。
  • 方案二:用 Vue.observable 管理弹窗状态——把弹窗的 show / title / message / resolve / reject 存进一个响应式对象,模板里 v-if 控制显隐。这个行,但有个硬伤:它不能跨组件调用。比如 A.vue 里点删除,要等 B.vue(弹窗组件)的 mounted 完才能响应,还是有延迟。
  • 方案三:预挂载 + 函数式调用(最终落地)——提前 mount 一个 ConfirmDialog 实例,暴露 show(options) 方法,内部用 $nextTick + 强制 reflow 触发过渡动画。这个最稳,也最快。

核心代码就这几行

先建一个全局 ConfirmDialog.vue(注意:没有 template,纯 script):

<!-- ConfirmDialog.vue -->
<template>
  <transition name="fade">
    <div v-show="visible" class="confirm-overlay">
      <div class="confirm-box">
        <h3 class="title">{{ title }}</h3>
        <p class="message">{{ message }}</p>
        <div class="buttons">
          <button @click="onCancel" class="btn-cancel">{{ cancelText }}</button>
          <button @click="onConfirm" class="btn-confirm">{{ confirmText }}</button>
        </div>
      </div>
    </div>
  </transition>
</template>

<script>
export default {
  name: 'ConfirmDialog',
  data() {
    return {
      visible: false,
      title: '',
      message: '',
      cancelText: '取消',
      confirmText: '确定',
      resolve: null,
      reject: null,
      // 关键:避免重复 resolve/reject
      pending: false
    }
  },
  methods: {
    show(options = {}) {
      // 防止连点
      if (this.pending) return Promise.reject(new Error('pending'))

      this.title = options.title || '确认操作'
      this.message = options.message || ''
      this.cancelText = options.cancelText || '取消'
      this.confirmText = options.confirmText || '确定'

      this.pending = true
      this.visible = true

      // 等过渡帧完成,再 resolve promise
      return new Promise((resolve, reject) => {
        this.resolve = resolve
        this.reject = reject
      })
    },
    onConfirm() {
      if (!this.pending) return
      this.pending = false
      this.visible = false
      this.resolve?.(true)
      this.clear()
    },
    onCancel() {
      if (!this.pending) return
      this.pending = false
      this.visible = false
      this.reject?.(new Error('cancelled'))
      this.clear()
    },
    clear() {
      // 清空引用,防内存泄漏
      this.resolve = null
      this.reject = null
      this.title = ''
      this.message = ''
    }
  }
}
</script>

然后在 main.js 里预挂载一次:

// main.js
import ConfirmDialog from './components/ConfirmDialog.vue'

const confirmInstance = new Vue({
  render: h => h(ConfirmDialog)
}).$mount()

// 挂到 body 最后
document.body.appendChild(confirmInstance.$el)

// 暴露全局方法
window.$confirm = (options) => {
  return confirmInstance.$children[0].show(options)
}

业务组件里直接用:

// MessageItem.vue
methods: {
  async handleDelete() {
    try {
      const confirmed = await window.$confirm({
        title: '删除这条消息?',
        message: '删除后无法恢复',
        confirmText: '删除'
      })
      if (confirmed) {
        await fetch('https://jztheme.com/api/messages/123', { method: 'DELETE' })
      }
    } catch (e) {
      // 用户点了取消 or error
    }
  }
}

优化后:流畅多了

预挂载之后,点击到弹窗完全可交互,时间压到了 800ms 左右(Redmi Note 9)。更关键的是:无卡顿、无连弹、无内存泄漏。测试连续点 20 次,内存曲线平稳,GC 频率降到 1/5。

顺手加了个小优化:在 show() 里加了 if (this.visible) this.visible = false; this.$nextTick(() => this.visible = true),强制重置 transition,让每次弹出都有「新入场感」,不然快速连点会卡住动画。

这里注意我踩过好几次坑:不要用 v-if 切换,要用 v-show + transition;$nextTick 必须包在 this.visible = false 后面,否则过渡类名不会重新触发。

性能数据对比

指标 优化前 优化后 提升
首帧响应时间(ms) 5200 780 85% ↓
内存峰值(MB) 42 18 57% ↓
GC 次数 / 分钟 23 4 82% ↓
连点 10 次弹窗数 平均 6.2 个 稳定 1 个 ✅ 修复

踩坑提醒:这三点一定注意

  • 别在 show() 里直接 new Vue()——这是最大雷区,Vue 2 的实例化成本太高,尤其带 scoped CSS 和 watcher 的组件。
  • Promise 必须和 visible 绑定生命周期——否则用户关掉弹窗后,后续 resolve/reject 还可能执行,造成状态错乱。我的做法是:visible 为 false 时立刻清空 resolve/reject 引用。
  • transition 的 name 一定要唯一——我们项目里有多个弹窗共用 fade 类,结果互相干扰动画。后来给 confirm 加了 name="confirm-fade" 就稳了。

以上是我踩坑后的总结,希望对你有帮助

这个方案不是理论最优(比如 React 可以用 useReducer + context 更轻量),但在我们 Vue 2 + Webpack 4 的老项目里,它最简单、最可控、上线后零投诉。如果你有更好的实现方式——比如用 Portal + teleport(Vue 3)、或者基于 requestIdleCallback 的懒加载弹窗,欢迎评论区交流。

后续可能会写写「如何给消息气泡加防抖点击」和「长按复制文案的兼容性填坑」,都是真实项目里被 QA 打回来三次才搞定的……

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论
司空宇彤
文笔细腻,读起来就像听朋友聊天一样亲切。
点赞
2026-02-19 22:25