HarmonyOS开发:即时通讯聊天功能
HarmonyOS开发:即时通讯聊天功能
核心要点:IM聊天架构的核心是WebSocket长连接,消息收发与本地存储保证可靠性,聊天记录与未读消息管理是用户体验的关键。
背景与动机
你用微信聊天,发一条消息对方秒回——这背后是什么?WebSocket长连接。你的消息不是通过HTTP请求发出去的,而是通过一个持续打开的TCP连接实时推送的。
为什么不用HTTP轮询?你每隔5秒问一次服务器"有新消息吗?"——100万用户同时在线,每秒20万次请求,服务器扛不住。而且5秒的延迟,聊天体验像发邮件。
WebSocket长连接也不是万能的。连接断了怎么办?消息丢了怎么办?对方离线了消息怎么存?重连后怎么同步历史消息?1000条未读消息怎么高效加载?
IM聊天是移动端最复杂的模块之一。这篇文章把聊天架构、WebSocket连接、消息收发、聊天记录、未读消息全拆开讲。
核心原理
IM聊天的核心是长连接+本地存储+增量同步。WebSocket保持实时连接,消息同时写入本地数据库,断线重连后增量同步未收到的消息。
flowchart TD
A[用户打开聊天页] --> B[建立WebSocket连接]
B --> C{连接成功?}
C -->|是| D[发送登录认证]
C -->|否| E[启动重连机制]
D --> F[同步离线消息]
F --> G[进入实时聊天模式]
E --> H[指数退避重连]
H --> C
G --> I{消息事件}
I -->|收到新消息| J[写入本地数据库]
J --> K[更新UI]
K --> L[发送已读回执]
I -->|发送消息| M[写入本地数据库]
M --> N[通过WebSocket发送]
N --> O{服务端确认?}
O -->|是| P[更新消息状态为已发送]
O -->|否| Q[标记为发送失败]
I -->|连接断开| E
classDef connect fill:#1565C0,color:#fff,stroke:#0D47A1
classDef message fill:#2E7D32,color:#fff,stroke:#1B5E20
classDef error fill:#C62828,color:#fff,stroke:#B71C1C
classDef sync fill:#6A1B9A,color:#fff,stroke:#4A148C
class A,B,C,D,E,H,connect
class I,J,K,L,M,N,O,P,Q,message
class error
class F,sync
消息可靠性保证
IM消息必须满足三个条件:
- 不丢:每条消息都有唯一ID,服务端确认后才标记为已发送
- 不重:消息ID去重,同一条消息不会显示两次
- 有序:消息按时间戳排序,不会乱序
消息状态流转
一条消息从"发送中"到"已读"经历四个状态:
- 发送中:消息已写入本地,等待服务端确认
- 已发送:服务端已确认收到
- 已送达:对方设备已收到
- 已读:对方已查看
代码实战
基础用法:WebSocket连接管理
先搞定WebSocket连接——建立、认证、心跳、重连。
// WebSocketManager.ets — WebSocket连接管理
import { webSocket } from '@kit.NetworkKit'
// 连接状态
enum ConnectState {
DISCONNECTED = 'DISCONNECTED',
CONNECTING = 'CONNECTING',
CONNECTED = 'CONNECTED',
AUTHENTICATING = 'AUTHENTICATING',
AUTHENTICATED = 'AUTHENTICATED',
}
// 消息类型
enum MessageType {
TEXT = 'TEXT',
IMAGE = 'IMAGE',
VOICE = 'VOICE',
VIDEO = 'VIDEO',
SYSTEM = 'SYSTEM',
}
// 聊天消息
interface ChatMessage {
id: string
conversationId: string // 会话ID
senderId: string
receiverId: string
type: MessageType
content: string
mediaUrl?: string
timestamp: number
status: 'sending' | 'sent' | 'delivered' | 'read' | 'failed'
isRead: boolean
}
class WebSocketManager {
private static instance: WebSocketManager
private ws: webSocket.WebSocket | null = null
private connectState: ConnectState = ConnectState.DISCONNECTED
private wsUrl: string = 'wss://im.example.com/ws'
private token: string = ''
private heartbeatInterval: number = -1
private reconnectTimer: number = -1
private reconnectAttempts: number = 0
private maxReconnectAttempts: number = 10
private messageHandlers: ((msg: ChatMessage) => void)[] = []
private constructor() {}
static getInstance(): WebSocketManager {
if (!WebSocketManager.instance) {
WebSocketManager.instance = new WebSocketManager()
}
return WebSocketManager.instance
}
// ========== 建立连接 ==========
async connect(token: string): Promise<boolean> {
if (this.connectState === ConnectState.CONNECTED ||
this.connectState === ConnectState.AUTHENTICATED) {
return true // 已连接
}
this.token = token
this.connectState = ConnectState.CONNECTING
try {
this.ws = webSocket.createWebSocket()
// 监听连接打开
this.ws.on('open', () => {
console.info('[WS] 连接已建立')
this.connectState = ConnectState.CONNECTED
this.reconnectAttempts = 0
this.authenticate()
})
// 监听消息
this.ws.on('message', (err: Error, data: string | ArrayBuffer) => {
if (err) {
console.error(`[WS] 消息错误: ${err.message}`)
return
}
this.handleMessage(typeof data === 'string' ? data : '')
})
// 监听关闭
this.ws.on('close', () => {
console.info('[WS] 连接已关闭')
this.connectState = ConnectState.DISCONNECTED
this.stopHeartbeat()
this.scheduleReconnect()
})
// 监听错误
this.ws.on('error', (err: Error) => {
console.error(`[WS] 连接错误: ${err.message}`)
this.connectState = ConnectState.DISCONNECTED
this.scheduleReconnect()
})
// 发起连接
await this.ws.connect(this.wsUrl)
return true
} catch (error) {
console.error(`[WS] 连接失败: ${JSON.stringify(error)}`)
this.connectState = ConnectState.DISCONNECTED
this.scheduleReconnect()
return false
}
}
// ========== 认证 ==========
private authenticate(): void {
this.connectState = ConnectState.AUTHENTICATING
this.sendRaw({
type: 'auth',
token: this.token
})
}
// ========== 心跳 ==========
private startHeartbeat(): void {
this.stopHeartbeat()
this.heartbeatInterval = setInterval(() => {
this.sendRaw({ type: 'ping', timestamp: Date.now() })
}, 30000) // 30秒一次心跳
}
private stopHeartbeat(): void {
if (this.heartbeatInterval !== -1) {
clearInterval(this.heartbeatInterval)
this.heartbeatInterval = -1
}
}
// ========== 重连 ==========
private scheduleReconnect(): void {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('[WS] 超过最大重连次数')
return
}
// 指数退避:1s, 2s, 4s, 8s, 16s, 32s...
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 32000)
this.reconnectAttempts++
console.info(`[WS] ${delay}ms后重连(第${this.reconnectAttempts}次)`)
this.reconnectTimer = setTimeout(() => {
this.connect(this.token)
}, delay)
}
// ========== 处理收到的消息 ==========
private handleMessage(rawData: string): void {
try {
const data = JSON.parse(rawData)
switch (data.type) {
case 'auth_ok':
this.connectState = ConnectState.AUTHENTICATED
this.startHeartbeat()
console.info('[WS] 认证成功')
break
case 'pong':
// 心跳响应,忽略
break
case 'new_message':
const message = data.message as ChatMessage
// 通知所有消息处理器
this.messageHandlers.forEach(handler => handler(message))
break
case 'message_ack':
// 消息发送确认
console.info(`[WS] 消息已确认: ${data.messageId}`)
break
case 'message_read':
// 对方已读回执
console.info(`[WS] 消息已读: ${data.messageId}`)
break
default:
console.warn(`[WS] 未知消息类型: ${data.type}`)
}
} catch (error) {
console.error(`[WS] 消息解析失败: ${JSON.stringify(error)}`)
}
}
// ========== 发送消息 ==========
sendMessage(message: ChatMessage): boolean {
if (this.connectState !== ConnectState.AUTHENTICATED) {
console.error('[WS] 未认证,无法发送消息')
return false
}
return this.sendRaw({
type: 'send_message',
message: message
})
}
// ========== 发送原始数据 ==========
private sendRaw(data: Record<string, Object>): boolean {
if (!this.ws || this.connectState === ConnectState.DISCONNECTED) {
return false
}
try {
this.ws.send(JSON.stringify(data))
return true
} catch (error) {
console.error(`[WS] 发送失败: ${JSON.stringify(error)}`)
return false
}
}
// ========== 注册消息处理器 ==========
onMessage(handler: (msg: ChatMessage) => void): void {
this.messageHandlers.push(handler)
}
// ========== 断开连接 ==========
disconnect(): void {
this.stopHeartbeat()
if (this.reconnectTimer !== -1) {
clearTimeout(this.reconnectTimer)
}
if (this.ws) {
this.ws.close()
this.ws = null
}
this.connectState = ConnectState.DISCONNECTED
}
getState(): ConnectState {
return this.connectState
}
}
进阶用法:聊天页面与消息列表
WebSocket搭好了,接下来是聊天页面——消息列表、发送消息、图片消息。
// ChatPage.ets — 聊天页面
import { router } from '@kit.ArkUI'
@Entry
@Component
struct ChatPage {
@State messages: ChatMessage[] = []
@State inputText: string = ''
@State contactName: string = ''
@State contactAvatar: string = ''
@State currentUserId: string = 'user_001'
@State isLoadingMore: boolean = false
private conversationId: string = ''
private wsManager: WebSocketManager = WebSocketManager.getInstance()
private listScroller: Scroller = new Scroller()
aboutToAppear() {
const params = router.getParams() as Record<string, string>
this.conversationId = params?.conversationId || ''
this.contactName = params?.contactName || '聊天对象'
this.contactAvatar = params?.contactAvatar || ''
// 加载本地聊天记录
this.loadChatHistory()
// 监听新消息
this.wsManager.onMessage((msg: ChatMessage) => {
if (msg.conversationId === this.conversationId) {
this.messages = [...this.messages, msg]
// 滚动到底部
setTimeout(() => {
this.listScroller.scrollToIndex(this.messages.length - 1)
}, 50)
}
})
}
build() {
Column() {
// 顶部导航
Row() {
Image($r('app.media.ic_back'))
.width(24).height(24).fillColor('#333333')
.onClick(() => { router.back() })
Text(this.contactName)
.fontSize(18).fontWeight(FontWeight.Bold).fontColor('#333333')
.margin({ left: 12 })
Blank()
Image($r('app.media.ic_more'))
.width(24).height(24).fillColor('#333333')
}
.width('100%').height(48).padding({ left: 16, right: 16 })
.backgroundColor(Color.White)
// 消息列表
List({ scroller: this.listScroller }) {
ForEach(this.messages, (msg: ChatMessage, index?: number) => {
ListItem() {
this.MessageBubble(msg)
}
}, (msg: ChatMessage) => msg.id)
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.padding({ left: 12, right: 12, top: 8 })
.backgroundColor('#EDEDED')
// 输入栏
this.InputBar()
}
.width('100%').height('100%').backgroundColor('#EDEDED')
}
// ========== 消息气泡 ==========
@Builder
MessageBubble(msg: ChatMessage) {
// 判断是自己的消息还是对方的
const isSelf = msg.senderId === this.currentUserId
Row() {
if (isSelf) {
Blank()
}
// 头像
if (!isSelf) {
Image(this.contactAvatar)
.width(36).height(36).borderRadius(18).objectFit(ImageFit.Cover)
}
// 消息内容
Column() {
if (msg.type === MessageType.TEXT) {
Text(msg.content)
.fontSize(15)
.fontColor(isSelf ? Color.White : '#333333')
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.backgroundColor(isSelf ? '#1DA1F2' : Color.White)
.borderRadius(isSelf ?
{ topLeft: 12, topRight: 12, bottomLeft: 12, bottomRight: 4 } :
{ topLeft: 12, topRight: 12, bottomLeft: 4, bottomRight: 12 })
} else if (msg.type === MessageType.IMAGE) {
Image(msg.mediaUrl || '')
.width(160).height(160).borderRadius(8).objectFit(ImageFit.Cover)
}
// 消息状态
if (isSelf && msg.status === 'sending') {
Text('发送中...')
.fontSize(10).fontColor('#999999').margin({ top: 2 })
} else if (isSelf && msg.status === 'failed') {
Text('发送失败')
.fontSize(10).fontColor('#FF4444').margin({ top: 2 })
}
}
.constraintSize({ maxWidth: '65%' })
.alignItems(isSelf ? HorizontalAlign.End : HorizontalAlign.Start)
// 头像
if (isSelf) {
Image($r('app.media.ic_avatar_default'))
.width(36).height(36).borderRadius(18).fillColor('#1DA1F2')
}
if (!isSelf) {
Blank()
}
}
.width('100%')
.margin({ bottom: 12 })
}
// ========== 输入栏 ==========
@Builder
InputBar() {
Row() {
Image($r('app.media.ic_voice'))
.width(28).height(28).fillColor('#666666')
.onClick(() => { /* 语音输入 */ })
TextInput({ placeholder: '输入消息...', text: $$this.inputText })
.layoutWeight(1)
.height(36)
.fontSize(15)
.backgroundColor('#F5F5F5')
.borderRadius(18)
.padding({ left: 12, right: 12 })
.margin({ left: 8, right: 8 })
.onSubmit(() => {
this.sendTextMessage()
})
Image($r('app.media.ic_emoji'))
.width(28).height(28).fillColor('#666666')
.margin({ right: 8 })
if (this.inputText.trim().length > 0) {
Button('发送')
.fontSize(13).fontColor(Color.White)
.backgroundColor('#1DA1F2').borderRadius(16)
.height(32).padding({ left: 12, right: 12 })
.onClick(() => { this.sendTextMessage() })
} else {
Image($r('app.media.ic_plus'))
.width(28).height(28).fillColor('#666666')
.onClick(() => { /* 更多功能 */ })
}
}
.width('100%')
.height(52)
.padding({ left: 12, right: 12 })
.backgroundColor(Color.White)
.alignItems(VerticalAlign.Center)
}
// ========== 发送文字消息 ==========
sendTextMessage() {
const text = this.inputText.trim()
if (!text) return
const message: ChatMessage = {
id: `msg_${Date.now()}`,
conversationId: this.conversationId,
senderId: this.currentUserId,
receiverId: '',
type: MessageType.TEXT,
content: text,
timestamp: Date.now(),
status: 'sending',
isRead: false,
}
// 先写入本地,立即显示
this.messages = [...this.messages, message]
this.inputText = ''
// 滚动到底部
setTimeout(() => {
this.listScroller.scrollToIndex(this.messages.length - 1)
}, 50)
// 通过WebSocket发送
const sent = this.wsManager.sendMessage(message)
if (!sent) {
// 发送失败,更新状态
const idx = this.messages.findIndex(m => m.id === message.id)
if (idx >= 0) {
this.messages[idx].status = 'failed'
this.messages = [...this.messages]
}
}
}
// ========== 加载聊天记录 ==========
loadChatHistory() {
// 模拟数据
this.messages = [
{ id: '1', conversationId: this.conversationId, senderId: 'user_002', receiverId: this.currentUserId, type: MessageType.TEXT, content: '你好,最近怎么样?', timestamp: Date.now() - 3600000, status: 'read', isRead: true },
{ id: '2', conversationId: this.conversationId, senderId: this.currentUserId, receiverId: 'user_002', type: MessageType.TEXT, content: '挺好的,你呢?', timestamp: Date.now() - 3500000, status: 'read', isRead: true },
{ id: '3', conversationId: this.conversationId, senderId: 'user_002', receiverId: this.currentUserId, type: MessageType.TEXT, content: '我也不错,周末有空一起吃饭吗?', timestamp: Date.now() - 3400000, status: 'read', isRead: true },
]
}
}
完整示例:会话列表与未读消息
把会话列表、未读消息管理、聊天入口串成完整IM系统。
// ConversationListPage.ets — 会话列表
import { router } from '@kit.ArkUI'
// 会话数据
interface Conversation {
id: string
contactId: string
contactName: string
contactAvatar: string
lastMessage: string // 最后一条消息摘要
lastMessageTime: number // 最后消息时间戳
unreadCount: number // 未读消息数
isPinned: boolean // 是否置顶
}
@Entry
@Component
struct ConversationListPage {
@State conversations: Conversation[] = []
@State isLoading: boolean = false
private wsManager: WebSocketManager = WebSocketManager.getInstance()
aboutToAppear() {
this.loadConversations()
// 连接WebSocket
this.wsManager.connect('user_token')
// 监听新消息,更新会话列表
this.wsManager.onMessage((msg: ChatMessage) => {
this.onNewMessage(msg)
})
}
build() {
Column() {
// 顶部栏
Row() {
Text('消息')
.fontSize(20).fontWeight(FontWeight.Bold).fontColor('#333333')
Blank()
Image($r('app.media.ic_add_chat'))
.width(24).height(24).fillColor('#333333')
}
.width('100%').height(48).padding({ left: 16, right: 16 })
.backgroundColor(Color.White)
// 会话列表
List() {
ForEach(this.conversations, (conv: Conversation) => {
ListItem() {
this.ConversationItem(conv)
}
.swipeAction({ end: this.SwipeActions(conv) })
}, (conv: Conversation) => conv.id)
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.divider({ strokeWidth: 0.5, color: '#F0F0F0', startMargin: 72 })
}
.width('100%').height('100%').backgroundColor(Color.White)
}
// ========== 会话项 ==========
@Builder
ConversationItem(conv: Conversation) {
Row() {
// 头像
Stack() {
Image(conv.contactAvatar)
.width(48).height(48).borderRadius(8).objectFit(ImageFit.Cover)
// 未读数角标
if (conv.unreadCount > 0) {
Text(conv.unreadCount > 99 ? '99+' : `${conv.unreadCount}`)
.fontSize(10).fontColor(Color.White)
.backgroundColor('#FF4444').borderRadius(10)
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.position({ x: 36, y: -4 })
}
}
.width(48).height(48)
// 会话信息
Column() {
Row() {
Text(conv.contactName)
.fontSize(16).fontColor('#333333').fontWeight(FontWeight.Medium)
Blank()
Text(this.formatTime(conv.lastMessageTime))
.fontSize(12).fontColor('#999999')
}.width('100%')
Text(conv.lastMessage)
.fontSize(14).fontColor('#999999').maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 4 })
}
.layoutWeight(1)
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.backgroundColor(conv.isPinned ? '#F5F5F5' : Color.White)
.onClick(() => {
// 进入聊天页
router.pushUrl({
url: 'pages/ChatPage',
params: {
conversationId: conv.id,
contactName: conv.contactName,
contactAvatar: conv.contactAvatar
}
})
})
}
// ========== 左滑操作 ==========
@Builder
SwipeActions(conv: Conversation) {
Row() {
Button(conv.isPinned ? '取消置顶' : '置顶')
.fontSize(13).fontColor(Color.White)
.backgroundColor('#999999').borderRadius(0)
.width(70).height('100%')
.onClick(() => { this.togglePin(conv.id) })
Button('删除')
.fontSize(13).fontColor(Color.White)
.backgroundColor('#FF4444').borderRadius(0)
.width(70).height('100%')
.onClick(() => { this.deleteConversation(conv.id) })
}
}
// ========== 收到新消息 ==========
onNewMessage(msg: ChatMessage) {
// 找到对应会话
const idx = this.conversations.findIndex(c => c.id === msg.conversationId)
if (idx >= 0) {
// 更新会话
const conv = this.conversations[idx]
conv.lastMessage = msg.type === MessageType.TEXT ? msg.content : '[图片]'
conv.lastMessageTime = msg.timestamp
conv.unreadCount++
// 移到列表顶部(置顶的除外)
this.conversations.splice(idx, 1)
const pinIdx = this.conversations.findIndex(c => !c.isPinned)
this.conversations.splice(pinIdx >= 0 ? pinIdx : 0, 0, conv)
this.conversations = [...this.conversations]
}
}
// ========== 辅助方法 ==========
formatTime(timestamp: number): string {
const now = Date.now()
const diff = now - timestamp
if (diff < 60000) return '刚刚'
if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`
if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`
return `${Math.floor(diff / 86400000)}天前`
}
togglePin(convId: string) {
const conv = this.conversations.find(c => c.id === convId)
if (conv) {
conv.isPinned = !conv.isPinned
this.conversations = [...this.conversations]
}
}
deleteConversation(convId: string) {
this.conversations = this.conversations.filter(c => c.id !== convId)
}
loadConversations() {
this.conversations = [
{ id: 'conv1', contactId: 'u2', contactName: '张三', contactAvatar: 'https://picsum.photos/80/80?random=1', lastMessage: '周末一起吃饭吗?', lastMessageTime: Date.now() - 300000, unreadCount: 3, isPinned: true },
{ id: 'conv2', contactId: 'u3', contactName: '产品讨论组', contactAvatar: 'https://picsum.photos/80/80?random=2', lastMessage: '李四: 新版设计稿出了', lastMessageTime: Date.now() - 3600000, unreadCount: 12, isPinned: false },
{ id: 'conv3', contactId: 'u4', contactName: '王五', contactAvatar: 'https://picsum.photos/80/80?random=3', lastMessage: '收到,谢谢!', lastMessageTime: Date.now() - 86400000, unreadCount: 0, isPinned: false },
]
}
}
踩坑与注意事项
坑1:WebSocket连接频繁断开
移动端网络不稳定,WebSocket连接隔几分钟就断一次。每次断开都重连,用户体验很差。
解决方案:
- 心跳机制:30秒发一次ping,3次无响应则断开重连
- 指数退避重连:1s, 2s, 4s, 8s…不要每次都1秒重连,服务器扛不住
- 网络状态监听:网络恢复时立即重连,不用等心跳超时
坑2:消息发送失败处理
消息发送失败后,用户看不到任何提示,以为发出去了。
解决方案:每条消息都有状态(sending/sent/failed),失败的消息显示红色感叹号,点击可以重发。
坑3:聊天记录加载性能
打开聊天页要加载1000条历史消息,一次性全部渲染?内存爆了。
解决方案:分页加载,每次20条,上滑加载更多。用LazyForEach只渲染可见区域。图片消息用缩略图。
坑4:未读消息数不一致
会话列表显示3条未读,点进去只有2条——因为有一条消息被撤回了。
解决方案:未读数由服务端维护,客户端只负责展示。进入聊天页时,上报已读回执,服务端更新未读数。
坑5:消息时序问题
用户快速发送5条消息,服务端收到的顺序可能是3-1-2-5-4——因为网络延迟。
解决方案:消息按客户端时间戳排序,不按服务端接收时间排序。或者服务端给每条消息分配递增的序列号。
HarmonyOS 6适配说明
HarmonyOS 6对IM相关能力做了以下更新:
- WebSocket增强:WebSocket Kit新增了自动重连能力,不需要手动实现重连逻辑。设置
autoReconnect: true后,连接断开时SDK自动重连,支持自定义重连策略。
// HarmonyOS 6自动重连
const ws = webSocket.createWebSocket({
autoReconnect: true,
reconnectInterval: [1000, 2000, 4000, 8000, 16000], // 递增间隔
maxReconnectAttempts: 10
})
-
关系型数据库RDB增强:RDB新增了FTS5全文搜索扩展,聊天记录可以用SQL做全文搜索,不用自己实现搜索算法。
-
通知增强:Notification Kit新增了消息样式通知,支持显示头像、消息内容、回复按钮。用户在通知栏直接回复消息,不需要打开App。
-
后台长连接保活:HarmonyOS 6优化了后台长连接的保活策略,App切到后台后WebSocket连接不会被系统杀掉,保证消息实时到达。
-
@Reusable组件复用:聊天消息气泡用
@Reusable标记后,滑出屏幕的消息组件自动复用,不再重复创建。1000条消息的聊天页内存占用降低约70%。
总结
IM聊天的核心是可靠性。消息不能丢、不能重、不能乱序——这三个条件都满足,才算一个合格的IM系统。
核心记住三点:
- WebSocket必须有心跳和重连,移动端网络不稳定,没有重连机制的IM就是玩具
- 消息必须本地持久化,断网时消息先存本地,联网后自动发送
- 未读数由服务端维护,客户端只展示,避免不一致
| 评估维度 | 说明 |
|---|---|
| 学习难度 | ⭐⭐⭐⭐⭐ WebSocket管理、消息可靠性、本地存储都需要经验 |
| 使用频率 | ⭐⭐⭐⭐⭐ 社交App的核心功能 |
| 重要程度 | ⭐⭐⭐⭐⭐ 消息丢了就是用户投诉,没有比这更重要的了 |
聊天消息发出去对方收不到——这不是bug,这是信任危机。
- 点赞
- 收藏
- 关注作者
评论(0)