Zhang's Notes

WebView 中 WebSocket 与 H5 状态同步方案

cover

图片来自 https://www.pexels.com/zh-cn/photo/macbook-209701/

一、场景

APP 内嵌一个 WebView,WebView 中运行 H5 页面。

当前系统存在以下条件:

  1. APP 已经建立并维护 WebSocket 长连接。
  2. H5 页面本身有大量 HTTP API,与后端进行业务数据交互。
  3. H5 需要实时感知部分状态变化,状态可能在 1 分钟内变化 3~5 次。
  4. H5 不允许单独建立 WebSocket,否则会与 APP 的 WebSocket 连接产生冲突。
  5. H5 收到状态变化后,需要及时更新页面数据。

因此需要解决的问题是:

如何利用 APP 已有的 WebSocket,将实时状态变化传递给 H5,同时继续保持 H5 原有的 API 数据获取模式。


二、方案一:H5 单独建立 WebSocket

结构:

text
后端
├── WebSocket → APP
└── WebSocket → H5

不推荐。

H5 如果再次建立 WebSocket,会产生多个实时连接,同时与现有 APP WebSocket 的连接管理产生冲突。

如果后端同一用户只允许一个 WebSocket 连接,还可能导致 APP 的 WebSocket 被 H5 挤掉,从而影响 APP 本身的消息推送。

因此在当前限制下,H5 不应该直接建立 WebSocket。


三、方案二:APP WebSocket → JS Bridge → H5

推荐采用 APP 作为唯一的 WebSocket 客户端。

数据流:

text
后端 WebSocket
       ↓
APP WebSocket
       ↓
Native
       ↓
JS Bridge
       ↓
H5

APP 收到 WebSocket 消息后,通过 WebView 的 JavaScript Bridge 通知 H5。

例如 WebSocket 收到:

json
{
    "type": "status_changed",
    "id": 123
}

Native 将事件发送给 H5:

javascript
window.NativeBridge.emit("status_changed", {
    id: 123
});

H5 收到事件后处理对应业务。

这种方式可以避免 H5 建立第二个 WebSocket。


四、不要让 WebSocket 消息成为 H5 的数据源

由于 H5 本身已经存在大量 HTTP API,因此更合理的设计是:

WebSocket 负责发送“变化通知”,HTTP API 负责获取“最新状态”。

例如:

text
后端状态发生变化
        ↓
WebSocket
        ↓
APP
        ↓
JS Bridge
        ↓
H5 收到 status_changed
        ↓
调用 HTTP API
        ↓
获取最新状态
        ↓
更新 UI

例如 H5 收到:

json
{
    "type": "task_changed",
    "taskId": 123
}

然后调用:

http
GET /api/task/123

获取最新数据。

这样可以继续复用 H5 原有的 API、数据模型和业务逻辑,而不需要在 Android Native 中重新实现一套业务数据处理逻辑。


五、推荐采用事件机制

不建议在 Native 中直接调用大量具体的 JavaScript 方法:

text
refreshUser()
refreshTask()
refreshOrder()
refreshRoom()
refreshStatus()

随着业务增加,这种方式会导致 Native 与 H5 强耦合。

可以设计统一的事件通道:

text
Native WebSocket
      ↓
Native EventBus
      ↓
JS Bridge
      ↓
H5 EventBus
      ↓
业务模块
      ↓
HTTP API

H5:

javascript
NativeBridge.on("task_changed", (data) => {
    refreshTask(data.taskId);
});

以后新增实时事件时,只需要增加事件类型:

text
user_changed
task_changed
order_changed
room_changed
status_changed
message_received

六、状态变化频繁时的处理

如果一分钟内产生 3~5 次状态变化,通常不需要每次都立即发起 HTTP 请求。

可以在 H5 侧进行短时间防抖:

javascript
let timer = null;

NativeBridge.on("status_changed", () => {
    clearTimeout(timer);

    timer = setTimeout(() => {
        refreshStatus();
    }, 100);
});

如果短时间内连续收到多个通知:

text
A
B
C

可以合并成一次 API 请求。


七、需要考虑请求乱序

实时状态同步还需要考虑 HTTP 请求返回顺序。

例如:

text
状态 A → 请求 API A
状态 B → 请求 API B

API B 先返回
API A 后返回

如果直接使用返回结果更新 UI,可能出现旧状态覆盖新状态的问题。

可以通过服务端版本号、时间戳或序列号解决:

json
{
    "status": "online",
    "version": 123
}

H5 只接受不低于当前版本的数据。

javascript
if (data.version >= currentVersion) {
    updateUI(data);
}

八、WebView 生命周期

还需要考虑 WebView 当前是否已经准备完成。

例如:

text
WebSocket 收到消息
        ↓
WebView 尚未创建

此时不能直接执行 JavaScript。

可以由 Native 缓存最新事件,在 H5 页面初始化完成后再发送。

典型流程:

text
WebSocket
    ↓
Native 保存最新状态
    ↓
WebView 创建
    ↓
H5 Ready
    ↓
Bridge 建立
    ↓
发送最新状态事件

这样可以避免 WebSocket 消息丢失。


九、最终方案

综合当前场景,推荐采用:

text
                    Backend
                 ┌────────────┐
                 │ HTTP API   │
                 │ WebSocket  │
                 └─────┬──────┘
                       │
              ┌────────┴────────┐
              │                 │
          WebSocket             │ HTTP
              │                 │
              ▼                 ▼
        ┌───────────┐     ┌───────────┐
        │    APP    │     │    H5     │
        │ WebSocket │     │ API Client│
        └─────┬─────┘     └─────▲─────┘
              │                  │
              │ JS Bridge        │
              ▼                  │
        ┌───────────┐             │
        │    H5     │─────────────┘
        │ EventBus  │
        └───────────┘

核心职责划分:

模块职责
APP WebSocket唯一实时连接
JS BridgeNative 与 H5 之间传递事件
H5 EventBus分发实时状态变化
H5 HTTP API获取最新业务数据
H5 UI根据最新数据更新页面

最终形成:

WebSocket 负责通知,HTTP API 负责数据,JS Bridge 负责通信,H5 负责业务状态。

这种设计可以避免重复建立 WebSocket,同时最大程度复用现有 H5 API 架构,并降低 Android Native 与 H5 业务逻辑之间的耦合。