接了个挺头疼的需求要把现有 App 里用的pusher_channels实时通讯功能搬到鸿蒙端。刚拿到手的时候我以为是文档问题翻一遍发现渠道路子全通但就是连不上后来才弄明白这压根不是普通 bug是 Flutter 插件在鸿蒙上没有原生实现的问题。这篇文章就是我从零开始做pusher_channels鸿蒙化适配的完整记录包括思路选型、ArkTS 里的 WebSocket 对接、Pusher 协议事件映射、以及那些不跑一遍根本不会知道的小坑。适合正在做鸿蒙生态迁移、或者想在鸿蒙 App 里接 Pusher 实时通道的 Flutter 开发者看。1. 先搞清楚 pusher_channels 到底依赖了什么1.1 它是什么级别的插件很多人以为pusher_channels就是个纯 Dart 包其实它是一套标准的 Flutter federated plugin。外层的PusherChannels类提供 Dart API真正干活的是底层的pusher_channels_platform_interface再往下是各端实现Android 有PusherChannelsAndroidiOS 有PusherChannelsIos它们通过 Platform Channel 去调原生 SDK。在鸿蒙之前这套链路不存在Flutter 引擎找不到pusher_channels的鸿蒙实现于是一切入口直接报 MissingPluginException。理解这个层次很关键。因为鸿蒙化适配本质上不是让你重写 Pusher 客户端逻辑而是补上 鸿蒙这一层原生的实现。1.2 完整连接链路拆开看一次最普通的 Pusher 连接从 Dart 侧看只是pusher.connect()但实际跑起来是这样Dart 端PusherChannels.connect()调用平台接口Android 或 iOS 原生 SDK 建立到ws-{cluster}.pusher.com的 WebSocket连接成功后原生 SDK 订阅声明的 channel持续监听事件收到消息后通过 MethodChannel 或 EventChannel 把事件回传给 DartDart 端触发onEvent回调。在第 2 步你会发现整个插件最核心的依赖就是WebSocket。Pusher 的服务端本身不关心你用什么客户端只要你按协议发出 JSON 帧它就能回事件。这给鸿蒙化留了一条非常宽的路。1.3 所以鸿蒙化要改的其实只有一段Dart 层不用动PusherChannels的对外 API 不用动pusher_channels_platform_interface定义的接口不用动真正要做的只有一件事提供一个可以运行在鸿蒙上的原生实现让它负责建立 WebSocket、管理生命周期、收发协议消息并且把事件回传给 Dart。这句话说出来简单做起来要命。因为原生实现里你要处理的不只是 建立连接 一个动作还有订阅、取消订阅、事件分发、心跳、断线重连、连接状态变更通知。任何一个环节漏了Dart 层就会出现事件收不到、状态一直连接中、或者无缘无故断开的诡异表现。2. 鸿蒙端 WebSocket 适配的三种路线与选型动手之前我先在纸上列了三条路线每个都分析了一遍利弊。2.1 路线 A纯 Dart 层替换 WebSocket 客户端pusher_channels在 Dart 内部实际上是通过一个注入的 WebSocket 工厂来创建连接的。如果鸿蒙上的 Flutter 引擎已经支持dart:io的WebSocket那理论上我可以 fork 一份pusher_channels在 Dart 层把它的默认 WebSocket 实现换掉或者干脆不依赖任何原生代码直接把整个 Pusher 协议用 Dart 写一遍。这条路的好处是工程量看起来最小所有代码都在 Dart 层跨端复用。但风险在于鸿蒙上的 Flutter 引擎对dart:io底层 Socket 的支持成熟度还没有完全对齐 Android。遇到特定网络环境下的 WebSocket 握手失败、代理失效、IPv6 连接异常时你很难判断是引擎问题还是自己的代码问题。而且 fork 第三方包意味着以后上游更新都要手动同步维护成本很高。2.2 路线 B新增鸿蒙平台通道实现这是最正统的 Flutter 插件迁移姿势。我保留pusher_channels的 Dart API 和平台接口新建一个名为pusher_channels_harmony的插件包在这个包里用 ArkTS 调用鸿蒙系统的 WebSocket API完整实现平台接口的所有方法然后通过 Flutter MethodChannel 和 EventChannel 与 Dart 通信。这条路的工作量集中在 ArkTS 侧但收益也很直接不动原有 Dart 层不影响其他平台所有网络逻辑都走鸿蒙系统 API稳定性和系统兼容性由官方保证。对长期维护来说这是最干净的一版。2.3 路线 C用 System API 直接桥接如果在鸿蒙原生层不想用系统 WebSocket也可以直接通过ohos.net.webSocket封装一个客户端然后暴露给 Flutter。实际上路线 B 和 C 在实现层面是重叠的区别只在于有没有中间层。如果未来 Pusher 官方出了鸿蒙 SDK我只需要把内部实现换成官方 SDK对外契约不用变。所以我把 B 和 C 合并成了一种在鸿蒙原生实现里优先选择系统 WebSocket后续可以无缝切换原生 SDK。2.4 我的最终选型最终我选的是 B 为主、A 为辅的组合方案主体采用新增pusher_channels_harmony插件的方式在 ArkTS 内使用ohos.net.webSocket实现 WebSocket 连接如果某些场景下 ArkTS 层跑不通留一个 Dart 层的可替换 WebSocket 客户端作为降级开关。这个组合的好处是我不需要为 某个极端环境里系统 WebSocket 连不上 卡死整个项目Dart 层也能临时顶上。虽然看起来多了一条路径但代码层面做了一个工厂抽象不会有太多冗余。3. 实操新建 pusher_channels_harmony 插件这一趴是纯干活的内容。你不需要照抄我全部代码核心是把结构和方法看清楚。3.1 创建插件工程与目录结构在 Flutter 工程里我习惯用源码依赖的方式来做。先在项目根目录建一个plugins/pusher_channels_harmony目录然后发布到 pub 前先通过 path 依赖引入dependencies: pusher_channels: ^2.2.0 pusher_channels_platform_interface: ^2.2.0 pusher_channels_harmony: path: plugins/pusher_channels_harmony插件包的目录结构大概是pusher_channels_harmony/ pubspec.yaml lib/ pusher_channels_harmony.dart harmony/ entry/src/main/ ets/ PushChannelsHarmonyPlugin.ets PusherWebSocketClient.ets PusherEventMapper.ets module.json5pubspec.yaml里要声明这是pusher_channels_platform_interface的一个注册实现name: pusher_channels_harmony description: HarmonyOS implementation for pusher_channels. version: 1.0.0 environment: sdk: 3.0.0 4.0.0 dependencies: flutter: sdk: flutter pusher_channels_platform_interface: ^2.2.0 flutter: plugin: platforms: ohos: default_package: pusher_channels_harmony package: dev.pusher.pusher_channels_harmony这里的ohos平台标识是鸿蒙插件注册的关键别写错了。3.2 配置 HarmonyOS 权限与网络声明鸿蒙应用访问网络必须在module.json5里声明ohos.permission.INTERNET权限。不声明的话WebSocket 连接会直接报 Permission denied。{ module: { name: entry, type: entry, requestPermissions: [ { name: ohos.permission.INTERNET } ], abilities: [ { name: EntryAbility, srcEntry: ./ets/entryability/EntryAbility.ets, exported: true } ] } }另外Pusher 服务通常走的是 TCP 443 端口属于明文网络吗不它是 WSS所以不需要额外配置明文传输允许。但如果你本地的测试环境是ws://非加密连接鸿蒙默认还会禁止非加密流量这时候就要在网络安全配置里放开。我建议直接测试环境也走 WSS省掉这个坑。3.3 在 ArkTS 中封装一个 WebSocket 客户端鸿蒙的ohos.net.webSocket提供了一个比较完整的客户端能力。第一步是创建一个PusherWebSocketClient.ets把系统 WebSocket 的创建、连接、收消息、发送、关闭封装成一个可复用的类。先导入模块并创建实例import { webSocket } from kit.NetworkKit; export class PusherWebSocketClient { private ws: webSocket.WebSocket | null null; private url: string ; private isConnected: boolean false; connect(url: string, options?: webSocket.WebSocketRequestOptions): void { this.url url; this.ws webSocket.createWebSocket(); this.ws.on(open, (err, value) { if (err) { this.onError(err); return; } this.isConnected true; this.onOpen(); }); this.ws.on(message, (err, data) { if (err) { this.onError(err); return; } // 注意data 可能是 string 也可能是 ArrayBuffer const message typeof data string ? data : (data as ArrayBuffer).toString(); this.onMessage(message); }); this.ws.on(close, (err, value) { this.isConnected false; this.onClose(value?.code, value?.reason); }); this.ws.on(error, (err) { this.isConnected false; this.onError(err); }); this.ws.connect(url, options); } send(data: string): void { if (this.isConnected this.ws) { this.ws.send(data); } } close(): void { if (this.ws) { this.ws.close(); } } }这几个事件就是整个监听器的骨架。onOpen、onMessage、onClose、onError需要由插件层注册进来这样插件才能把它们转换成 Flutter 能识别的通道消息。3.4 通过 MethodChannel 暴露连接、订阅、发事件鸿蒙端实现PusherChannelsHarmonyPlugin时我用MethodChannel处理 Dart 侧的调用。Dart 侧要能执行四类操作初始化、连接、订阅、发送事件。import { MethodCall, MethodChannel } from flutter-sdk; export class PusherChannelsHarmonyPlugin implements FlutterPlugin, MethodCallHandler { private channel: MethodChannel | null null; private client: PusherWebSocketClient | null null; onAttachedToEngine(binding: FlutterPluginBinding): void { this.channel new MethodChannel(binding.getBinaryMessenger(), pusher_channels_harmony); this.channel.setMethodCallHandler(this); } onMethodCall(call: MethodCall, result: MethodResult): void { switch (call.method) { case connect: const url call.arguments[url] as string; this.client new PusherWebSocketClient(); this.client.onMessage (msg: string) this.handleSocketMessage(msg); this.client.connect(url); result.success(null); break; case subscribe: const channel call.arguments[channel] as string; this.sendSubscribe(channel); result.success(null); break; case sendEvent: const event call.arguments[event] as string; const data call.arguments[data] as string; this.client?.send(JSON.stringify({ event, data })); result.success(null); break; case disconnect: this.client?.close(); result.success(null); break; default: result.notImplemented(); } } }这里要注意Pusher 的订阅操作并不是直接掉一个 native 方法而是要发一个 JSON 帧到 WebSocket 里所以我subscribe方法内部的sendSubscribe本质上也是走send。3.5 通过 EventChannel 把消息推送到 DartMethodChannel 只能解决 Dart 调原生 的问题反过来 原生主动给 Dart 推事件 要用 EventChannel。鸿蒙端持有一个 EventSink当 WebSocket 收到消息时解析成标准结构然后sink.success(data)把它推到 Dart 流里export class PusherChannelsHarmonyPlugin implements EventStreamHandler { private eventSink: EventSink | null null; onListen(parameters: string, sink: EventSink): void { this.eventSink sink; } onCancel(parameters: string): void { this.eventSink null; } private handleSocketMessage(message: string): void { try { const json JSON.parse(message); // 转换为统一的 event payload const payload this.mapper.map(json); this.eventSink?.success(payload); } catch (e) { // 解析失败不能 crash 原生层记录一下即可 } } }对应 Dart 侧我会把 EventChannel 的 Stream 暴露给pusher_channels_platform_interface这样上层PusherChannels.onEvent就能收到数据了。到这里最基础的连通链路已经打通。但真的跑起来你会发现还差一大截因为 Pusher 协议本身要比 连上了、能发字符串 复杂得多。4. 核心协议细节Pusher 事件与订阅状态机4.1 先看懂 Pusher 的 WebSocket 消息格式Pusher 的协议层并不神秘本质就是 WebSocket 上来回传 JSON 字符串。从服务端到客户端的消息格式是{ event: pusher:subscription_succeeded, channel: my-channel, data: {\myData\:\value\} }从客户端到服务端的格式是{ event: pusher:subscribe, data: { channel: my-channel } }注意data字段有时是字符串有时是对象。Pusher 官方很多消息里data都是 JSON 字符串需要二次解析。这一个点如果不处理干净后面所有业务数据处理都会出问题。我在鸿蒙端专门写了一个PusherEventMapper统一把所有消息标准化不再向下游传原始字符串export class PusherEventMapper { map(raw: object): object { const event raw[event] as string; const channel raw[channel] as string; let data raw[data]; if (typeof data string) { try { data JSON.parse(data); } catch (e) { // 保留原字符串 } } return { event, channel, data }; } }4.2 连接建立别漏掉 socket_id当 WebSocket 建立成功后Pusher 服务端会先发一条pusher:connection_established事件它的data里带一个socket_id。这个socket_id是当前连接的唯一标识用途很多服务端鉴权时校验客户端身份控制台排查问题时定位到具体连接某些应用需要把socket_id传给业务后端来生成签名。如果适配的时候没有把socket_id保存下来后续做私密 channel 鉴权时会发现签名一直对不上。我在 Dart 侧的 abstraction 里把socket_id作为连接成功的信令原生端必须把这个事件翻译成 连接成功 状态。4.3 订阅三个状态缺一不可调用订阅后客户端发一个pusher:subscribe服务端会回pusher:subscription_succeeded。但在失败场景下还会收到pusher:subscription_error。我建议在适配层把订阅状态机做全至少包括订阅中、订阅成功、订阅失败。很多初版适配只处理了 发送 subscribe 帧 和 收到 success 帧忽略了 error结果线上出现订阅失败后Dart 层永远无回调只能等超时。正确做法是在handleSocketMessage里优先判断事件名字是这三类中的哪一种然后分别触发对应回调。4.4 心跳与断线重连Pusher 服务端默认会通过 WebSocket 层的 ping/pong 做连接健康检查。但实际在鸿蒙上我发现如果一段时间没有消息往来某些网络环境尤其带 NAT 超时的运营商网络会自动断开连接而客户端还没有感知。所以在鸿蒙适配里我在原生侧加了一个应用层心跳每 30 秒发一次{event:pusher:ping}收到服务端的pusher:pong就认为连接活跃。如果连续 3 次没有收到 pong主动触发重连private heartbeatTimer: number | null null; startHeartbeat(): void { this.stopHeartbeat(); this.heartbeatTimer setInterval(() { if (this.isConnected this.missedPongCount 3) { this.client?.send(JSON.stringify({ event: pusher:ping })); this.missedPongCount; } else { this.reconnect(); } }, 30000); } stopHeartbeat(): void { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer null; } }重连策略我做了指数退避第 1 次 1 秒、第 2 次 2 秒、第 3 次 4 秒最多 30 秒。同时要确保 Dart 层能收到pusher:connection_state_change之类的状态事件否则上层 UI 一直卡在 连接中。5. 我踩过的坑常见问题与排查实录这一节应当是全文最有价值的部分全是跑真机时遇到的真实情况。现象可能原因解决办法Dart 层一直报MissingPluginException插件没有在鸿蒙侧注册确认pubspec.yaml中 plugin platforms 里有ohos并执行flutter pub get后重新构建连接直接 Permission deniedohos.permission.INTERNET没配置在module.json5的requestPermissions里补全权限WebSocket open 成功但收不到pusher:connection_established消息事件回调没接到 message检查ws.on(message)注册时机必须写在connect()之前订阅发送成功但没有回调忘了解析pusher:subscription_succeeded在 mapper 中把subscription_succeeded单独映射为订阅成功状态业务事件收到后data是字符串而不是对象未做二次 JSON 解析统一在PusherEventMapper里二次JSON.parse一段时间后 WebSocket 静默断开NAT 超时或心跳丢失开启应用层 ping/pong并做指数退避重连连接成功后 socket_id 拿不到没有存储初始事件收到pusher:connection_established时立即缓存 socket_id 存到插件字段多 channel 订阅时事件串 channel分发时没有按 channel 过滤Dart 层收到事件后先按 channel 匹配原生只做透传5.1 最坑的on(message)注册顺序鸿蒙 WebSocket 的open事件触发很快我之前先写了connect()再注册on(message)结果 open 之后服务端立刻发来的第一条pusher:connection_established就丢了。后面才意识到所有on注册都要在connect()之前完成。这个顺序问题在文档里根本不会特别提示但实际影响非常大。如果你发现 WebSocket 明明显示已连接但 Dart 端永远等不到连接成功优先查这个。5.2 进程重建后连接状态残留鸿蒙上应用切到后台再回前台如果系统回收了 WebSocket 但 Flutter 引擎还活着就会出现 Dart 端认为 已连接 但实际网络层已经断开的情况。我的处理方案是在插件里监听on(close)事件只要 close 就立即向 Dart 推一个断开事件同时清理心跳计时器。这样上层可以在收到 close 后主动重连而不是等着下次发消息时才发现已经断了。5.3 多 Dart entry 初始化冲突Flutter 应用一旦用了多引擎或者混合开发鸿蒙插件可能会被初始化多次。如果每次初始化都 new 一个 WebSocketClient旧实例没有释放就会出现多个 socket 同时连接 Pusher产生重复订阅。我的做法是插件类持有单例onAttachedToEngine时判断是否已有 client有就复用否则才创建。6. 这次适配做完我对 Flutter 插件鸿蒙化的几点体会很多人问是不是所有 Flutter 三方库都能直接照这个方案搬到鸿蒙其实分情况。像pusher_channels这种底层依赖协议相对简单的插件鸿蒙化成本并不高核心就一个 WebSocket 连接。但如果插件依赖了 Android/iOS 的复杂原生服务比如推送厂商 SDK、安全芯片、本地数据库那就不是写几百行 ArkTS 能解决的了得等官方或社区出对应鸿蒙 SDK。我在这次适配里比较大的收获是鸿蒙化适配的关键不是把 API 对着抄一遍而是先把平台接口的边界切清楚。Dart 层按平台接口定义好契约鸿蒙端只关心通信和事件分发这两者解耦后未来不管底层换成鸿蒙官方 SDK 还是第三方 SDK都不影响上层调用。另外一个小技巧调试鸿蒙 WebSocket 时不要一上来就直连 Pusher 正式服务我用了一个本地 echo websocket server 验证收发链路确认 ping/pong、连接断开事件都正常后才切换到 Pusher。这样能把插件自身的问题和网络环境的问题快速隔离开。最后提醒一下鸿蒙侧 WebSocket 的二进制帧和文本帧处理方式不同Pusher 的协议数据都是 UTF-8 文本所以我在on(message)里优先按 string 处理对ArrayBuffer的兼容只是兜底。如果后续要支持 Pusher 的二进制消息比如传给 channel 的 ArrayBuffer这里还要再单独扩展不过常见的消息结构和事件通知都是 JSON够用了。这次适配做完后我又把工程里其他几个 Flutter 实时类组件全部盘了一遍发现只要协议层是 WebSocket JSON 这套标准玩法鸿蒙化思路几乎可以复用。如果你正卡在类似的三方库适配里建议先把协议抓包看明白再动手改原生层你会发现省掉很多没必要踩的坑。
阅读完成 · 觉得有帮助?