美洽怎么设置访客端聊天窗口离线缓存?
访客端聊天窗口的离线缓存,核心是把“正在或曾经发生的会话状态”在访客设备上本地保存起来,并在网络恢复后把未送达或新消息与美洽后端进行可靠同步。实现方法包括:拦截 SDK 的消息收发回调、把消息(文本、附件引用、状态、时间戳、客户端 ID)持久化到 IndexedDB/ localStorage、为未发送消息建立队列并在重连时重试、处理去重与序号对齐、以及对隐私和容量做策略控制。整个过程还可以结合 Service Worker(或移动端的后台任务)、广播通道和后端校验来保证多端一致性和体验平滑。

先把问题拆开:为什么需要离线缓存,目标是什么
把事情讲清楚一点,不要一上来就代码。想象你在购物网站同时浏览商品,突然断网,回到页面后希望还能看到刚才的对话,而不是空白。这就是离线缓存要解决的真实场景。目标通常有三点:
- 可用性:断网时仍能查看历史消息和未读内容;
- 可靠性:访客输入的消息不会丢失,能在恢复网络后被送出并显示正确状态;
- 用户体验:界面能立即渲染历史消息而不是等待网络,从而节省延迟。
技术选项总览(为什么选择 IndexedDB 而不是 localStorage 等)
不同存储方式各有利弊,按常见维度列个表,方便做决定:
| 存储方式 | 优点 | 缺点 |
| localStorage | 简单、同步 API、浏览器兼容好 | 容量小(约 5MB)、同步阻塞、不适合二进制/大附件 |
| IndexedDB | 容量大、支持事务、二进制存储(Blob)、适合消息存档 | API 比较复杂(但有包装库)、异步 |
| Service Worker + Cache / Background Sync | 可在后台处理同步、支持资源缓存和离线恢复 | 实现复杂、兼容性需考虑、移动端差异 |
| Session 或 Cookie | 简单用于会话标识 | 不适合消息体缓存、安全与容量受限 |
结论(工程实践)
对于聊天消息,推荐把文本和元数据(状态、时间、client_msg_id)存到 IndexedDB;把较大附件保存为 Blob(或保存上传引用);把少量配置信息或短期标志放 localStorage;用 BroadcastChannel 或 localStorage 事件做多标签页同步;用 Service Worker 做离线报文的后台重试(如果需要)。
按步骤实现:从架构到代码(Web 浏览器场景)
下面按实际开发流程来讲,像在黑板上写步骤,边写边想。
1. 设计本地数据模型
最重要的是要有稳定的消息 ID 和状态机。常见字段:
- message_id(服务器 id,可能在远端生成)
- client_msg_id(客户端生成的唯一 ID,用于去重与重试)
- conversation_id / session_id
- sender(visitor/agent/system)
- content(文本或引用)
- attachments(数组,保存文件名、mime、本地 blob 引用或服务器 URL)
- status(sending / sent / failed / received / read)
- timestamp
2. 拦截 SDK 消息回调并持久化
大多数客服 SDK(包含美洽 SDK)会提供消息接收、发送状态回调。你需要把这些回调统一接入本地存储逻辑。
伪代码思路(不写具体 SDK 名称,按通用事件):
// 当接收到消息
sdk.on('message:receive', (msg) => {
saveMessageToIndexedDB(msg);
renderToUI(msg);
});
// 当发送消息(用户点击发送)
function sendMessage(content) {
const clientMsgId = genClientId();
const msg = { client_msg_id: clientMsgId, content, status: 'sending', ts: Date.now() };
saveMessageToIndexedDB(msg);
renderToUI(msg);
sdk.send(msg).then(serverInfo => {
// 更新状态和 server id
updateMessageStatus(clientMsgId, { status: 'sent', message_id: serverInfo.id });
}).catch(err => {
updateMessageStatus(clientMsgId, { status: 'failed' });
});
}
3. 未发送队列与重试策略
核心点:不要丢数据。常见做法:
- 把 status 为 sending 或 failed 的消息放入“待发送队列”(保存在 IndexedDB)
- 当网络恢复或 WebSocket 重连时,按时间/序号顺序逐条重发(使用 client_msg_id 防止重复)
- 实现指数退避(exponential backoff)与最大重试次数,超过后标记为 failed 并给用户重发按钮
4. 重连同步与去重策略
重连后要做三件事:拉取服务器漏掉的消息、发送本地未送达的消息、合并两端历史。具体策略:
- 按时间或 sequence 拉取服务器端最新的消息段(如果美洽有消息序号 API,就更好)
- 用 client_msg_id 去重:如果服务器返回的消息包里包含和本地 client_msg_id 相同的消息,则把本地发送记录替换为服务器记录(并更新 status)
- 如果没有 client_msg_id,需比对时间戳 + 内容 + sender 做近似去重,但要小心边界情况
附件(图片、文件)如何缓存与同步
附件比文本复杂:占空间、上传中断、需要断点续传时要做更多工作。实战建议:
- 展示:先把本地文件生成缩略图或占位图并存入 IndexedDB(Blob);UI 显示“正在上传”或“等待网络”状态。
- 上传:如果网络可用立即上传文件到美洽/自家 CDN,成功后把服务器 URL 写回消息记录;若网络不可用,把文件暂存 IndexedDB 并放入附件上传队列,重连后自动上传。
- 断点续传:如果文件很大,优先使用 SDK/后端提供的分片上传能力;如果没有,至少保证重试不会重复上传多个副本(用 client_msg_id + 上传标识)。
多标签页和移动端的同步问题
用户可能在多个浏览器标签或不同设备上打开同一会话。要考虑并发更新与一致性。
- 在浏览器内:用 BroadcastChannel 或 localStorage 的 storage 事件把缓存变更广播给其它标签,确保 UI 同步。
- 跨设备:依赖后端(美洽)去做最终一致性,客户端只做本地持久化与按需拉取。
- 移动端(iOS/Android):如果使用美洽移动 SDK,优先使用 SDK 的本地持久化机制(若提供);否则用 SQLite/Realm 做本地存储,后台任务或推送机制做同步。
安全、隐私与合规
离线缓存里会有用户私人信息,要认真保护:
- 敏感信息加密:在客户端使用 Web Crypto(浏览器)或平台密钥库(移动端)对本地存储进行加密,或对关键信息字段加密。
- 最小化存储时间:按业务需要设定缓存 TTL,用户登出或切换账号时立即清理缓存。
- 权限控制:不要把敏感 token(如登录凭证)直接存入聊天缓存,使用短期授权并通过安全通道刷新。
- 合规要求:如果涉及个人数据保护法规(例如 GDPR / 中国个人信息保护法),要提供数据查看/删除入口并记录操作日志。
性能与空间管理策略
不加控制的离线缓存会把用户设备塞满,所以要有清理规则:
- 按会话或时间窗口保留最近 N 条消息或最近 T 天的记录(可配置);
- 附件用 LRU(最近最少使用)删除策略,或只保留缩略图;
- 定期压缩(例如把多条小消息合并索引),但在实现时要平衡复杂度;
- 为大型文件或媒体提供“仅在 Wi-Fi 下保留”选项。
测试矩阵:怎么验证离线缓存功能有效
测试要覆盖断网、弱网、切换网络、并发多端、附件上传中断、重连后顺序错乱等场景。常见测试步骤:
- 无网络:加载页面,确认历史消息能从本地读取并正确渲染;
- 发消息后断网:消息在 UI 显示为 sending 或 queued,并存入本地;
- 恢复网络:消息自动重试并被服务器接受,客户端记录更新为 sent;
- 多标签页并发:一个标签发送或接收消息,另一个标签同步显示;
- 大附件:中断上传,再次恢复后能继续或成功重新上传且不重复计费/不重复展示。
实用代码片段(简化示例,用于理解设计,不依赖具体 SDK)
下面是一个极简的 IndexedDB 保存/读取消息的示意,供思路参考:
async function openDB() {
return new Promise((res, rej) => {
const req = indexedDB.open('chat-db', 1);
req.onupgradeneeded = e => {
const db = e.target.result;
if (!db.objectStoreNames.contains('messages')) {
db.createObjectStore('messages', { keyPath: 'client_msg_id' });
}
};
req.onsuccess = e => res(e.target.result);
req.onerror = e => rej(e);
});
}
async function saveMessage(msg) {
const db = await openDB();
return new Promise((res, rej) => {
const tx = db.transaction('messages', 'readwrite');
tx.objectStore('messages').put(msg);
tx.oncomplete = () => res();
tx.onerror = e => rej(e);
});
}
async function getMessages(sessionId) {
const db = await openDB();
return new Promise((res, rej) => {
const tx = db.transaction('messages', 'readonly');
const store = tx.objectStore('messages');
const req = store.getAll();
req.onsuccess = e => res(e.target.result.filter(m => m.session_id === sessionId));
req.onerror = e => rej(e);
});
}
和美洽 SDK 配合时的注意点(工程层面的建议)
虽然不同版本的美洽 SDK 在细节上会有差异,但普适的工程建议如下:
- 查 SDK 文档找出“消息接收/发送回调”、“连接状态回调”、“消息 ID/序列号”这些接口点,把它们作为缓存逻辑的钩子;
- 优先使用 SDK 提供的本地持久化能力(如果存在),因为它可能更健壮并兼容美洽服务;
- 如果 SDK 不提供或你需要自定义策略,就在 SDK 之上做一层“消息持久化中间件”,不要改动 SDK 的低层通信;
- 把 client_msg_id 作为消息可靠性设计的核心,能简化去重与幂等处理;
- 与后端(或美洽客服后端)约定冲突解决策略:例如后端以时间戳或序列号为准,或以 server_message_id 为准。
常见坑与如何规避
- 坑:重复消息——解决:一定使用 client_msg_id 去重,且在重连时不要盲目把本地记录全部覆盖;
- 坑:序列错位导致错乱展示——解决:按时间戳 + 序号排序并展示“正在发送”状态,直到服务器确认;
- 坑:本地空间耗尽——解决:限制保留条数/天数、清理策略与用户可控选项;
- 坑:隐私泄露——解决:加密、合理 TTL、登出清理;
- 坑:跨浏览器兼容——解决:在兼容性较差的环境(旧浏览器、隐私模式)回退到 localStorage 或只在有网时加载。
最终的工程实施 checklist(可复制粘贴去验收)
- 是否为每条消息生成并存储 client_msg_id?
- 是否把消息与附件持久化到 IndexedDB(或合理后备)?
- 是否实现了未发送队列与重试机制?
- 是否在网络状态变化时正确触发重连与同步?
- 是否实现了去重策略与服务器冲突处理流程?
- 是否做了删除/过期策略并遵守隐私规则?
- 是否覆盖了多标签页/多设备的测试场景?
好了,写到这里我还在想有没有遗漏的边界条件:比如客服转接、机器人接入、群聊(如果支持)会带来更多同步复杂度,实际上一般做法是把这些额外的变更点也记录为“系统消息”,同样走本地持久化与同步流程。你如果需要,我可以把这套实现按你当前使用的美洽 SDK 版本再做一次具体代码改写,或者给出移动端(iOS/Android)的实现示例。就先到这儿,边写边想,难免有点散,但把关键点都列出来了。