UniApp 对接 IM 即时通讯全攻略(下篇)

UniApp 对接 IM 即时通讯全攻略(下篇)

在上篇中,我们完成了 WebSocket 模块封装和 Pinia 状态管理的搭建。本篇将基于此,实现 IM 的核心页面:会话列表页和聊天页,并处理 Tab 未读角标,最后介绍一些性能优化和常见问题。


一、会话列表页面

会话列表是 IM 应用的首页,展示所有最近会话,包括最后一条消息预览、未读数和时间等。页面数据来自 Pinia 中的 conversations。

1.1 模板示例

vue 复制代码
<template>
  <view>
    <view v-for="conv in imStore.conversations" :key="conv.id" @click="openChat(conv.id)">
      <image :src="conv.avatar" />
      <view>
        <text>{{ conv.name }}</text>
        <text>{{ conv.lastMessage }}</text>
      </view>
      <view>
        <text>{{ conv.lastTime }}</text>
        <text v-if="conv.unreadCount > 0" class="badge">{{ conv.unreadCount }}</text>
      </view>
    </view>
  </view>
</template>

1.2 逻辑实现

vue 复制代码
<script setup>
import { useImStore } from '@/stores/im';
import { onShow, onPullDownRefresh } from '@dcloudio/uni-app';

const imStore = useImStore();

onShow(() => {
  imStore.fetchConversations(); // 每次显示刷新
});

onPullDownRefresh(async () => {
  await imStore.fetchConversations();
  uni.stopPullDownRefresh();
});

function openChat(convId) {
  uni.navigateTo({ url: `/pages/im/chat?id=${convId}` });
}
</script>

要点:

  • 在 onShow 中拉取会话列表,保证数据最新。
  • 下拉刷新重新拉取,覆盖本地数据。
  • 新消息到达时,Pinia 的 handleIncomingMessage 会自动更新会话列表,无需额外监听。

二、聊天页面

聊天页面负责展示与某个联系人或群组的所有消息,并支持发送新消息、加载更早历史消息。

2.1 模板结构

vue 复制代码
<template>
  <view>
    <scroll-view scroll-y :scroll-into-view="scrollIntoView" class="message-list">
      <view v-for="msg in imStore.currentMessages" :key="msg.id" :id="'msg-' + msg.id">
        <text>{{ msg.content }}</text>
      </view>
    </scroll-view>
    <view class="input-bar">
      <input v-model="inputText" @confirm="sendMessage" placeholder="输入消息..." />
    </view>
  </view>
</template>

2.2 逻辑实现

vue 复制代码
<script setup>
import { ref, watch, nextTick } from 'vue';
import { useImStore } from '@/stores/im';
import { onLoad, onUnload, onPullDownRefresh } from '@dcloudio/uni-app';

const imStore = useImStore();
const inputText = ref('');
const scrollIntoView = ref('');

onLoad((options) => {
  const convId = options.id;
  imStore.fetchMessages(convId, 1);
  imStore.markConversationRead(convId);
});

onUnload(() => {
  imStore.clearCurrentConversation(); // 可选,释放内存
});

onPullDownRefresh(async () => {
  if (imStore.hasMore) {
    const nextPage = imStore.currentPage + 1;
    await imStore.fetchMessages(imStore.currentConvId, nextPage);
  }
  uni.stopPullDownRefresh();
});

// 自动滚动到底部
watch(
  () => imStore.currentMessages.length,
  async () => {
    await nextTick();
    if (imStore.currentMessages.length > 0) {
      const lastMsg = imStore.currentMessages[imStore.currentMessages.length - 1];
      scrollIntoView.value = 'msg-' + lastMsg.id;
    }
  }
);

async function sendMessage() {
  if (!inputText.value.trim()) return;
  await uni.request({
    url: '/api/sendMessage',
    method: 'POST',
    data: {
      conversationId: imStore.currentConvId,
      content: inputText.value.trim()
    }
  });
  inputText.value = '';
}
</script>

说明:

  • 打开页面时加载第一页消息,并标记会话已读。
  • 下拉加载更早的消息(向前翻页)。
  • 发送消息使用 HTTP API,发送成功后由服务器通过 WebSocket 推送给双方,本地无需手动插入消息(也可乐观更新,但要注意去重)。
  • 页面卸载时清空当前会话数据,避免占用内存。

三、Tab 未读角标处理

底部 Tab 通常需要显示消息未读总数。我们可以通过 Pinia 的 totalUnread 动态更新 TabBar 的角标。

3.1 在自定义 TabBar 组件中监听

如果使用自定义 TabBar,可以直接在组件中读取 imStore.totalUnread 并渲染。

vue 复制代码
<template>
  <view class="tab-bar">
    <!-- 其他 tab -->
    <view class="tab-item">
      <text>消息</text>
      <text v-if="imStore.totalUnread > 0" class="badge">{{ imStore.totalUnread > 99 ? '99+' : imStore.totalUnread }}</text>
    </view>
  </view>
</template>

<script setup>
import { useImStore } from '@/stores/im';
const imStore = useImStore();
</script>

3.2 使用原生 TabBar 的 setTabBarBadge

如果使用原生 TabBar,可以在 App.vue 或全局监听 imStore.totalUnread 变化,调用 uni.setTabBarBadge。

javascript 复制代码
// 在 App.vue 中 watch totalUnread
watch(() => imStore.totalUnread, (val) => {
  if (val > 0) {
    uni.setTabBarBadge({ index: 1, text: val > 99 ? '99+' : String(val) });
  } else {
    uni.removeTabBarBadge({ index: 1 });
  }
});

四、性能优化与注意事项

4.1 消息去重与排序

网络异常或重连可能导致消息重复。建议在 Store 中维护一个 msgId 集合,插入前检查:

javascript 复制代码
// 在 handleIncomingMessage 中
if (this.currentMessages.some(m => m.id === msg.id)) return;
this.currentMessages.push(msg);
// 可根据服务器返回的时间戳排序
this.currentMessages.sort((a, b) => a.timestamp - b.timestamp);

4.2 心跳与重连参数调优

  • 心跳间隔:移动网络建议 30~60 秒,过短会消耗流量和电量。
  • 重连间隔:建议指数退避(如 1s、3s、5s...),避免服务器压力过大。

4.3 清理监听器

  • 使用 registerMessageHandler 注册的处理器,在页面卸载时应调用其返回的取消函数(如果只在 App.vue 中注册一次则可忽略)。
  • 使用 uni.$emit 时,务必在 onUnload 中 uni.$off。

4.4 安全与鉴权

建议 WebSocket 连接时在 URL 中携带 token,或连接后发送鉴权消息,避免通过 bangUid 明文绑定用户。


五、总结

通过上下两篇,我们完成了一个完整的 UniApp IM 模块搭建:

  • 上篇:WebSocket 连接管理、Pinia 状态设计、消息分发机制。
  • 下篇:会话列表、聊天页面、未读角标及优化。

这套方案已在多个项目中验证,具备良好的稳定性和扩展性。你可以根据实际后端协议调整接口路径、消息格式等。希望对你有所帮助!

有任何疑问或建议,欢迎在评论区交流。

相关推荐
特创数字科技5 小时前
【uni-app x】告别系统相机限制!三端通用自定义相机组件:任意布局、静默拍照、录像、水印叠加、连拍回图一次搞定
前端·数码相机·uni-app
Martin -Tang5 小时前
uniapp app嵌套webview 弹窗方式
android·ios·uni-app
郑州光合科技余经理16 小时前
同城电商系统:库存变更怎么同步到订单
java·开发语言·前端·后端·uni-app·php·ai编程
CHB1 天前
一文讲透原生渲染和自渲染
flutter·react native·uni-app
码兄科技7 天前
24小时自助健身房系统软件开发实战指南:功能设计与部署方案
java·spring boot·uni-app
2501_916007477 天前
苹果商店iOS应用上架费用全面解析:开发者计划与审核费用计算
android·ios·小程序·https·uni-app·iphone·webview
2501_916008897 天前
如何实现iOS App代码混淆:SwiftShield使用指南
android·ios·小程序·https·uni-app·iphone·webview
2501_916008897 天前
怎么用 Godot 导出 iOS 应用并上架 App Store?签名字段该填什么?
android·ios·小程序·https·uni-app·iphone·webview
郑州光合科技余经理8 天前
同城外卖小程序开发:下单成功后,后台导出能不能对上用户端状态
开发语言·前端·git·后端·uni-app·php·ai编程
白远山8 天前
城市电竞陪玩调度系统实战:从派单算法到多端协同的架构拆解
java·架构·uni-app·需求分析