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 状态设计、消息分发机制。
- 下篇:会话列表、聊天页面、未读角标及优化。
这套方案已在多个项目中验证,具备良好的稳定性和扩展性。你可以根据实际后端协议调整接口路径、消息格式等。希望对你有所帮助!
有任何疑问或建议,欢迎在评论区交流。