背景痛点:原生 WebView 方案踩过的那些坑
去年做电商小程序时,老板一句“把客服系统接进来”,我们直接内嵌了一个 H5 页面。结果上线一周就炸锅:
- 安卓端 WebView 在息屏 5 分钟后必断,用户重新打开看到的是“客服已离线”,投诉率飙升
- iOS 端键盘弹起后输入框被顶飞,样式错位,用户只能盲打
- 微信里返回上一页会把整个 WebView 销毁,历史消息全丢,体验堪比“一次性客服”
跨端样式更是一地鸡毛:同一句.bubble { padding: 10px }在 APP 里正常,到 H5 被浏览器默认字体撑爆,到小程序又被 rpx 换算整崩。维护三套 CSS 的结果就是“改一行,测三天”。
痛定思痛,决定把客服模块彻底拉回 Uniapp 生态,用原生导航 + 本地缓存 + 长连接重搞一遍,目标只有一个:让消息“不丢、不重、不卡”。
公众号配图,先上张图让大家感受下当时的崩溃现场:
技术选型:融云、环信、野狗,谁更适合 Uniapp
我把主流 IM 厂商拉进同一张表格,从“集成成本”“消息到达率”“离线推送”三个维度打分(满分 5 分):
| 维度 | 融云 | 环信 | 野狗 |
|---|---|---|---|
| 集成成本 | 4 | 3 | 2 |
| 到达率 | 4.5 | 4 | 3.5 |
| 离线推送 | 原生 | 需配置 | 需配置 |
| 小程序支持 | 官方 | 社区 | 无 |
- 融云提供了 uni-app 专用插件,一条
uni.requireNativePlugin就能拉起原生长连接,最香 - 环信社区版 SDK 体积 1.8 M,plus 打包后 APK 增大 5 M,老板嫌包体大
- 野狗没有小程序插件,只能 WebSocket 裸连,到微信就废
最终敲定「融云 + 自研 WebSocket 降级」混合方案:APP 端走原生插件,小程序/H5 走 WebSocket,一套业务代码,两套传输层,自动切换,10 分钟搞定分支判断。
核心实现:让消息“不丢、不重、不卡”
1. WebSocket 长连接保活机制
小程序里系统回收比安卓还凶,30 秒不心跳就断。下面这段代码在utils/socket.js里常驻,负责“保活 + 重连 + 幂等”。
// utils/socket.js let ws = null let heartTimer = null let reconnectCount = 0 const MAX_RECONNECT = 5 /** 建立连接 */ export function connect(url, onMsg) { return new Promise((resolve, reject) => { ws = uni.connectSocketurl({ url }) ws.onOpen(() => { reconnectCount = 0 resolve() startHeartbeat() }) ws.onMessage(({ data }) => onMsg && onMsg(JSON.parse(data))) ws.onClose(() => { if (reconnectCount < MAX_RECONNECT) { reconnectCount++ setTimeout(() => connect(url, onMsg), 2000 * reconnectCount) } }) ws.onError(() => ws.close()) }) } /** 心跳包 */ function startHeartbeat() { heartTimer = setInterval(() => { ws.send({ data: JSON.stringify({ type: 'ping' }) }) }, 25000) } /** 销毁连接 */ export function close() { clearInterval(heartTimer) ws && ws.close() }2. Vuex 消息状态管理(含去重)
客服聊天最怕同一条消息重复渲染,利用msgId做幂等,代码直接放store/modules/chat.js:
const state = { list: [] // {msgId, from, content, time} } const mutations = { PUSH_MSG(state, payload) { // 简单幂等:msgId 已存在直接 return if (state.list.some(m => m.msgId === payload.msgId)) return state.list.push(payload) } } const actions = { /** 收到长连接消息 */ receive({ commit }, raw) { if (raw.type !== 'chat') return commit('PUSH_MSG', { msgId: raw.msgId, from: raw.from, content: raw.content, time: Date.now() }) } }页面里用mapState拉取list,配合scroll-into-view自动滚动到底部,体验丝滑。
##顺手贴一张调试时的截图,左边心跳,右边消息,顺序一目了然:
性能优化:别让 500 条历史记录卡死页面
1. 消息分片 + 懒加载
客服场景里用户上拉“查看更多”是刚需,直接把 500 条全塞进 DOM 必卡。思路:
- 后端一次性给 500 条,前端按
pageSize=20切片 - 页面只渲染当前片,滚动到顶部再
unshift上一片
代码片段(核心逻辑):
// pages/chat/chat.vue data() { return { page: 0, hasMore: true, renderList: [] } }, methods: { /** 上拉加载更多 */ loadMore() { if (!this.hasMore) return const list = this.$store.state.chat.list const start = list.length - (this.page + 1) * 20 const end = list.length - this.page * 20 if (start <= 0) this.hasMore = false this.renderList.unshift(...list.slice(Math.max(0, start), end)) this.page++ } }2. 不同端 CSS 适配
uni.upx2px可把设计稿 750 宽自动换算成物理像素,再包一层calc兼容 H5:
/* chat-bubble.css */ .bubble { padding: calc(20rpx + constant(safe-area-inset-bottom)); padding: calc(20rpx + env(safe-area-inset-bottom)); max-width: 540rpx; }- APP 端:rpx 会按屏幕密度自动乘系数
- H5 端:编译后变成
calc(20px + env(...)),浏览器也能认 - 小程序端:rpx 原生支持,无需额外处理
一套样式,三端通用,再也不用写三套.wxss .css .scss了。
避坑指南:iOS 后台 + 安卓断网双重暴击
1. iOS 后台运行限制
iOS 锁屏后 3 分钟系统会挂起 WebSocket,这是系统策略,不是 Bug。解法:
- 把“融云原生插件”打开
backgroundMode=audio,借系统音频保活通道 - 退到后台时记录时间戳,回到前台若间隔 > 30s,主动拉一次历史消息补偿
onHide() { this.leaveTime = Date.now() } onShow() { if (Date.now() - this.leaveTime > 30000) { this.$store.dispatch('chat/pullHistory') } }2. 安卓 WebSocket 自动重连异常
部分国产 ROM 把connectSocket当垃圾回收,断网后不会触发onClose,结果重连逻辑永远不进。解决:
- 在
onError里手动ws.close(),强制走入关闭分支 - 用
uni.onNetworkStatusChange监听网络恢复,一旦在线立即重连
uni.onNetworkStatusChange(({ isConnected }) => { if (isConnected && !ws) connect(url, onMsg) })延伸思考:离线消息提醒还能怎么玩
WebSocket 只能保证“在线可达”,用户杀进程就真没辙了。下一步可以:
- 把
uni-push与厂商通道(华为/小米/OPPO)全部打通,客服发消息时先走 IM 长连接,失败再降级到 Push - 服务端记录「离线」状态,推送正文只带
msgId,客户端收到后调 REST 拉取完整内容,节省流量 - 小程序里订阅
wx.getUserProfile一次性拿到formId,48 小时内可发 3 条模板消息,作为兜底
这样“长连接 + Push + 模板消息”三段式,离线场景也能把到达率再抬 10 个点。
写在最后的碎碎念
整套方案跑下来,客服响应速度从平均 8s 降到 5s 左右,投诉量降了 30%,老板终于不再每天 @ 我改 Bug。最重要的是,代码完全掌控在自个儿手里,再也不用被 H5 页面牵着鼻子走。
如果你也在 Uniapp 里被客服集成折磨,不妨先拿 WebSocket 搭个最小可运行 demo,再把融云插件插进去做增量替换,边跑边测,逐步替换,比一口气重构风险小得多。祝各位早日脱离客服泥潭,把时间省下来去撸更有价值的业务需求。