NativeBridge:实现原理解析
开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情
系列(一):NativeBridge:基于webivew_flutter的JSBridge插件 系列(二):NativeBridge:实现原理解析 系列(三):App实现JSBridge的最佳方案 系列(四):NativeBridge:我在Pub上发布的第一个插件
Tip: 目前 NativeBridge 插件已迭代更新多个版本,该文章源码为 Tag: v0.0.1
前沿
上一篇文章《NativeBridge:基于webivew_flutter的JSBridge插件》有讲到 NativeBridge 的集成和使用。趁热打铁,我们今天解析下 NativeBridge 的实现原理和超时检测机制。
webview_flutter 插件分析
在说 NativeBridge 之前,我们先了解下 webview_flutter,毕竟 NativeBridge 是对其能力的拓展。
webview_flutter 是提供一个 WebView Widget 的 Flutter 插件。在 iOS 上,WebView Widget 由 WKWebView 支持;在 Android 上,WebView Widget 由 WebView 支持。官方原文:
A Flutter plugin that provides a WebView widget.
On iOS the WebView widget is backed by a WKWebView; On Android the WebView widget is backed by a WebView.
JSBridge 的调用。其通过 javascriptChannels 实现对 H5 端 JSBridge 调用的支持。代码如下:
WebView(
...
javascriptMode: JavascriptMode.unrestricted,
javascriptChannels: <JavascriptChannel>{
_toasterJavascriptChannel(context),
},
);
它最大的局限在于 App 端与 H5 端之间的通信只支持单向通信,无法直接获取另一端的返回值。如官方 example 中的实现:
// (1)通过 runJavascript 执行H5端方法,向App端发送 userAgent 信息
Future<void> _onShowUserAgent(
WebViewController controller, BuildContext context) async {
// Send a message with the user agent string to the Toaster JavaScript channel we registered
// with the WebView.
await controller.runJavascript(
'Toaster.postMessage("User Agent: " + navigator.userAgent);');
}
// (2)接收H5端的消息
JavascriptChannel _toasterJavascriptChannel(BuildContext context) {
return JavascriptChannel(
name: 'Toaster',
onMessageReceived: (JavascriptMessage message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message.message)),
);
});
}
NativeBridge 的源码分析
NativeBridge 本质上是对 webview_flutter 的单向通信能力进行扩展和封装。
内部由 NativeBridgeImpl、NativeBridgeHelper 和 NativeBridge 三个类以及一个实体类 Message 构成,结构比较简单。
Message实体类。主要定义了需要调用的方法 api、数据 data 和用于执行返回操作的标识 callbackId。
Message({
required this.api, // 方法API
this.data, // 发送的数据
this.callbackId, // 返回操作标识
});
NativeBridgeImpl。是一个抽象类,定义了一套需要用户实现的协议。包括:javascript 的 name、支持的 callMethodMap 集合和执行 JS 的 runJavascript() 方法。
abstract class NativeBridgeImpl {
NativeBridgeImpl({required this.name, required this.callMethodMap});
/// javascript channel name
final String name;
/// call method map
final Map<String, Function?> callMethodMap;
/// 执行JS
void runJavascript(String javaScriptString);
}
NativeBridge。实现 WebView 的 JavascriptChannel 类用于 JSBridge 的初始化,其中 name 和 onMessageReceived 都是通过 NativeBridgeImpl 代理实现。
class NativeBridge implements JavascriptChannel {
NativeBridge({required this.controller});
final NativeBridgeImpl controller;
@override
String get name => controller.name;
@override
JavascriptMessageHandler get onMessageReceived => (message) async {
...
controller.runJavascript("receiveMessage($json)");
}
};
}
NativeBridgeHelper。顾名思义是 NativeBridge 的帮助类,主要实现发送消息和接受消息的处理,并根据 callbackId 标识进行 Future 的回调。
/// 发送消息
static Completer sendMessage(Message message, NativeBridgeImpl nativeBridgeImpl) {
Completer completer = Completer();
var callbackId = _pushCallback(message.api, completer);
message.callbackId = callbackId;
// H5接受消息
final res = messageToJson(message);
nativeBridgeImpl.runJavascript("receiveMessage($res)");
return completer;
}
/// 接收消息
static void receiveMessage(String json) {
var map = jsonDecode(json);
var callbackId = map["callbackId"];
var data = map["data"];
var completer = _popCallback(callbackId);
completer?.complete(Future.value(data));
}
NativeBridge 双向通信的实现原理
通过 example 的例子可以看到,我们可以直接通过 NativeBridgeHelper 获取到 H5 端的返回值。那么它是如何实现的呢?答案是 Completer 。
var isHome = await NativeBridgeHelper.sendMessage(
Message(api: "isHome"),
_nativeBridgeController,
).future;
Completer。简单来说就是生成一个异步的 Future,允许我们在接受到 H5 端的消息后进行异步回调。就像网络请求一样,发送请求后等待服务器响应请求并返回数据。(Tip:我们用到的 dio 网络库也是通过 Completer 来实现异步响应)
用上面的获取 H5 端的 isHome 值为例。我们先发送一条需要获取 isHome 的消息,然后异步等待 H5 端接收消息并进行处理,H5 端接收到消息后,根据收到的 api 回复一条携带返回值的消息给 App 端,App 端再通过 callbackId 查询到前面缓存的 Completer 执行 complete() 方法调用,最后获取到我们想要的值。
执行流程如下:
(1)App 端根据发送的消息创建 Completer 并缓存到 Completer 栈,给 H5 端发送消息。
// native_bridge_helper.dart sendMessage()
Completer completer = Completer();
var callbackId = _pushCallback(message.api, completer);
message.callbackId = callbackId;
// H5接受消息
final res = messageToJson(message);
nativeBridgeImpl.runJavascript("receiveMessage($res)");
(2)H5 端接收 App 端的消息,根据消息的 api 回复一条消息。
// jsBridgeHelper.js receiveMessage()
if (message.api === 'isHome') {
this._postMessage(message.api, true.toString(), message.callbackId)
}
(3)App 端接收到 H5 端的消息后,根据 callbackId 获取到缓存的 Completer 并执行 complete() 完成回调。
// native_bridge_helper.dart receiveMessage()
var map = jsonDecode(json);
var callbackId = map["callbackId"];
var data = map["data"];
var completer = _popCallback(callbackId);
completer?.complete(Future.value(data));
至此,完成了从 App 端获取 H5 端值的整个流程。
NativeBridge 的超时机制
就像网络请求一样,我们不能让代码执行一直阻塞在获取返回值的位置上。因为单向发送消息是不可靠的,可能存在消息丢失,或者 H5 端不响应消息的情况。因此我们需要类似网络请求一样,新增超时机制。
在 NativeBridge 的 sendMessage() 方法中通过设置倒计时,一旦超过倒计时还没有响应时,我们就将当前的 Completer 取出并执行 complete(Future.value(null)) 表示没有获取到对应的值。
// 增加回调异常容错机制,避免消息丢失导致一直阻塞
Future.delayed(const Duration(milliseconds: 200), (){
var completer = _popCallback(callbackId);
completer?.complete(Future.value(null));
});
对于 H5 端的实现
对于 H5 端的实现和 App 端同理,只是将 Completer 替换成了 Promise,其余逻辑和处理都相同。具体实现参考 example/assets/test/jsBridgeHelper.js
总结
我们先通过分析 webview_flutter 插件的实现和存在的缺陷,引申出 NativeBridge 的作用和源码构成,再通过 Completer 来实现异步 future 的调用,最后介绍了添加超时机制的理由和具体实现。至此,我们完成了整个插件的实现原理解析。
转载自:https://juejin.cn/post/7172840863234523173