背景痛点:三端差异带来的“小惊喜”
在 Uniapp 项目里接入七鱼智能客服,最大的拦路虎不是功能本身,而是“跨平台”三个字。
同样一段初始化代码,在 H5 端跑得飞快,到 Android 9 却直接白屏;iOS 15 上客服窗口弹不出来,一查才发现是WKWebView对window.postMessage的兼容策略变了。
更隐蔽的是 WebSocket:浏览器里一条心跳 30s 稳稳当当,原生 App 切后台 5 分钟就被系统掐了,重连时七鱼后台会把离线会话算成新会话,导致用户看到“客服已离线”的幽灵提示。
这些差异如果不在架构阶段就留好口子,后期补洞会把排期直接拉爆。
技术选型:为什么放弃纯 WebView
| 方案 | 优点 | 缺点 | 结论 |
|---|---|---|---|
| 纯 WebView 内嵌 H5 客服页 | 接入快、热更新友好 | 1. 原生能力(推送、相册、录音)需要桥接,延迟高 2. iOS 低版本 cookie 写不进去,登录态同步失败 | 放弃 |
| 原生 SDK 全量接入 | 性能最好、权限粒度细 | 1. 要写两份原生插件(iOS/Android) 2. 与 Uniapp 的 Vue 生命周期耦合痛苦 | 放弃 |
| 官方混合模式(npm 包 + 原生插件) | 1. 官方已封装好uniapp-qiyu-sdk2. 暴露统一 JS 接口,内部走原生长连接 | 包体积 +1.8 M | 采用 |
结论:用七鱼官方维护的qiyu-uniapp-plugin,既保留原生长连接,又能在 Vue 里直接import,性价比最高。
核心实现:从 npm 到第一条真人消息
1. 安装与权限申请
# 项目根目录 npm install @qiyu/uniapp-sdk --save- Android:
manifest.json→ App 模块配置 → 勾选文件读写、录音、相机 - iOS:
manifest.json→ iOS 模块配置 → 补充下面三项,缺一项都可能导致审核被拒
NSCameraUsageDescription:客服需要拍摄照片 NSMicrophoneUsageDescription:客服需要发送语音 NSPhotoLibraryUsageDescription:客服需要选择图片2. 初始化(main.ts)
import {QYIM} from '@qiyu/uniapp-sdk' // 类型声明,避免 anyscript interface QYConfig { appKey: string userId?: string userName?: string authToken?: string } const config: QYConfig = { appKey: '你的七鱼 AppKey', userId: uni.getStorageSync('uid'), userName: uni.getStorageSync('uname'), // 如果后台开启了 Token 校验,就填 authToken: uni.getStorageSync('qiyu_token') } // 生命周期钩子,保证只在 App 启动时初始化一次 export function initQY() { QYIM.init(config).then(() => { console.log('[七鱼] SDK 初始化完成') // 注册全局未读回调,供各页面订阅 uni.$emit('qiyuUnread', QYIM.unreadCount) }).catch((e: any) => { console.error('[七鱼] 初始化失败', e) }) }3. 打开会话页
// pages/mine/service.vue <template> <view @tap="openService">联系客服</view> </template> <script setup lang="ts"> import {QYIM} from '@qiyu/uniapp-sdk' function openService() { // 七鱼会自动判断有没有登录态,没有会弹出“匿名会话” QYIM.openServiceWindow({ // 自定义导航条颜色,与主题色保持一致 navigationBarColor: '#07c160', // 是否显示机器人优先 robotFirst: true }) } </script>4. WebSocket 断线重连
七鱼 SDK 内部已做指数退避重连,但 App 切后台会被系统杀连接,回到前台后需要“手动心跳”触发一次:
// App.vue onShow() { // 应用回到前台,立刻 ping 一次 QYIM.pingServer().then(online => { if (!online) uni.showToast({title: '客服离线', icon: 'none'}) }) }性能优化:让低端机也能流畅聊天
消息本地缓存
利用七鱼提供的onMessageReceive钩子,把每条消息写进plus.sqlite,页面加载优先读库,再增量拉新消息,首屏渲染从 400ms 降到 80ms。图片懒加载 + 压缩
发送端:先调plus.zip.compressImage把图压到 1280 边界,体积平均降 65%。
接收端:用IntersectionObserver(H5)与原生lazyLoad双降级,滑动到可视区域才下载原图。Workers 解析富文本
客服常发商品卡片,带 HTML 标签。把html2json放到 Worker 线程里跑,解析完再postMessage回主线程,避免 200ms 的掉帧。
避坑指南:血泪踩出来的 checklist
- iOS 13+ 需要在
Info.plist里加UISceneConfigurations,否则 SDK 在冷启动时拿不到window,初始化直接失败。 - Android 端 WebView 低于 66 不支持
ResizeObserver,导致键盘弹起时输入框被遮挡,需在manifest强制升级x5内核。 - 用户状态同步幂等:后台客服给同一用户发多条“正在输入”,客户端用
Map<uid, timer>做防抖,相同内容 3s 内只展示一次,防止头像列表疯狂抖动。
安全考量:别把聊天记录裸奔出去
- 传输层:七鱼默认走 HTTPS,额外再开
TLS1.3强制校验,抓包只能看到二进制帧。 - 内容层:对订单号、手机号等敏感字段,业务侧先走 AES-128-CBC,密钥放在原生层,JS 层拿不到。
- XSS 防御:富文本渲染用
dompurify白名单,只放行<img><a>两个标签,且a标签强制加rel="noreferrer"。
效果验证:跑起来看看
左:iOS 15 真机;右:Android 11 低端机,聊天列表滑动 60fps 稳定。
完整可运行示例已推到 GitHub,克隆后把main.ts里的appKey换成自己的就能直接体验:
https://github.com/yourname/qiyu-uniapp-demo
思考题:离线消息同步怎么做?
七鱼目前保证“在线消息 0 丢失”,但 App 被杀进程后,离线期间的留言靠推送兜底。
如果想做到“用户打开 App 后,离线期间的客服消息也能无缝拼到列表里”,你会如何设计?
提示:可以结合本地时间戳、消息msgId幂等、后台提供“补录接口”三点展开。欢迎留言交流。