最近项目中,把 APP 和 小程序 里 80% 的页面都换成了 H5,目的是快速开发,方便热更新。前端除了要保证代码高度复用并且降低耦合,还要主动和负责 Native 开发的同学沟通,定义好协议,留下文档,方便后期维护。
对于混合开发来说,最核心的是数据交互:
- 小程序端:在不影响用户体验的情况下,直接或间接的使用现有规则进行开发。
- Native端:使用了目前市面上成熟的
WebViewJavaScriptBridge
开源库。在Android
端踩了一些坑,iOS
端没出现太大的问题。
项目完成了,抽时间学习下WebViewJavaScriptBridge
源码,顺便对这次Hybrid
开发做下总结。
一、 JavaScript 与 Native 交互的方式(以iOS为例)
1、JS 调用 Native 方法:
- Native 拦截 URL 跳转
window.location.href = 'Native自定义协议';
- 使用
WebKit
,苹果官方推荐使用这种方式
window.webkit.messagehandlers..postMessage('xxx');
2、Native
调用 JS
方法
前端将JS function
暴露到全局window
对象上, Native
在 webView
中注入 JS
代码执行。
以上方法如果单独使用,都比较麻烦,而且代码难以组织,Native
拦截 URL
跳转或使用 WebKit
更多时候是用在前端单向调用 Native
方法的场景,不支持 return
和 callback
,只能做 send
操作,做不了get
操作。
3、使用 WebViewJavaScriptBridge
开源库
iOS
和 Android
对应的代码开源地址:
- iOS WebViewJavascriptBridge
- Android JsBridge
二、WebViewJavaScriptBridge 实现机制
WebViewJavaScriptBridge 很好的解决了 JS
和 Native
通信的问题,并且使我们能更好的组织代码,其原理也是根据以上两种方法做了进一步封装。
JS
和 Native
需要互相调用,那么各自都需要做到两点:
1、注册好方法,供对方调用
2、调用对方已注册的方法
iOS(WKWebView)对外暴露的API:
- (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler;
- (void)removeHandler:(NSString*)handlerName;
- (void)callHandler:(NSString*)handlerName;
- (void)callHandler:(NSString*)handlerName data:(id)data;
- (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback;
- (void)reset;
- (void)setWebViewDelegate:(id)webViewDelegate;
- (void)disableJavscriptAlertBoxSafetyTimeout;
JavaScript 对外暴露的 API:
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
我们通常使用的也就是它们各自的 registerHandler
和 callHandler
方法。
三、WebViewJavaScriptBridge 目录结构
1、WebViewJavascriptBridgeBase
用来进行 bridge
初始化和消息处理的核心类,其保存了三个很重要的属性:
responseCallbacks
:用于保存 Objective-C
与javascript
环境相互调用的回调模块。通过 _uniqueId
加上时间戳来确定每个调用的回调。
messageHandlers:
用于保存 Objective-C
环境注册的方法,key
是方法名,value
是这个方法对应的回调 block
startupMessageQueue:
保存类实例化过程中需要发送给 JavaScirpt
环境的消息。
2、WebViewJavascriptBridge
bridge
入口类,判断当前 WebView
的类型是 UIWebView
或 WKWebView
,执行相应的逻辑。
3、WKWebViewJavascriptBridge
针对 WKWebView
做的一层封装,主要用来执行JS
代码,以及实现 WKWebView
的代理方法,并通过拦截 URL
来通知 WebViewJavascriptBridgeBase
做相应操作。本次源码学习,也是以 WKWebViewJavascriptBridge
为主,忽略 UIWebView
。
4、WebViewJavascriptBridge_JS
其代码会被注入到 WebView 中,用于 JavaScript 端的 register 和 call 操作。
加下来主要针对
-
iOS
初始化WebViewJavascriptBridge
-
JavaScript
初始化Bridge
-
JavaScript
主动调用iOS
方法 -
iOS
主动调用JavaScript
方法
这四个过程进行深入讲解
四、iOS 初始化 WebViewJavascriptBridge
在 ExampleWKWebViewController.m
文件中,进行brige的初始化
self.bridge = [WebViewJavascriptBridge bridgeForWebView:self.webview];
来到 WebViewJavascriptBridge
源码目录,打开 WebViewJavascriptBridge.m
文件,找到 bridgeForWebView
方法,相关代码如下:
// 类方法,用来初始化 bridge
+ (instancetype)bridgeForWebView:(id)webView {
return [self bridge:webView];
}
+ (instancetype)bridge:(id)webView {
// 通过 supportsWKWebView 判断是否支持 WKWebView
// 如果支持的话,执行 WKWebViewJavascriptBridge 类方法 bridgeForWebView
#if defined supportsWKWebView
if ([webView isKindOfClass:[WKWebView class]]) {
return (WebViewJavascriptBridge*) [WKWebViewJavascriptBridge bridgeForWebView:webView];
}
#endif
if ([webView isKindOfClass:[WVJB_WEBVIEW_TYPE class]]) {
WebViewJavascriptBridge* bridge = [[self alloc] init];
[bridge _platformSpecificSetup:webView];
return bridge;
}
[NSException raise:@"BadWebViewType" format:@"Unknown web view type."];
return nil;
}
打开 WKWebViewJavascriptBridge.m
文件,找到:
+ (instancetype)bridgeForWebView:(WKWebView*)webView {
WKWebViewJavascriptBridge* bridge = [[self alloc] init];
[bridge _setupInstance:webView];
[bridge reset];
return bridge;
}
实例化bridge
之后,依次执行 _setupInstance
和 reset
方法:
- (void) _setupInstance:(WKWebView*)webView {
// 使用成员变量 _webView 保存 webView
_webView = webView;
// 设置 navigationDelegate,很重要
// navigationDelegate 的类型是 WKNavigationDelegate,定义了很多 WKWebView 运行过程中的代理方法
// 后面要使用其拦截 URL 的功能
_webView.navigationDelegate = self;
// 实例化 WebViewJavascriptBridgeBase
_base = [[WebViewJavascriptBridgeBase alloc] init];
// 设置 WebViewJavascriptBridgeBase 的 delegate,方便调用 _evaluateJavascript
_base.delegate = self;
}
- (void)reset {
// 调用 WebViewJavascriptBridgeBase 的 reset 方法
[_base reset];
}
- (void)reset {
// 重置三个很重要的成员变量:startupMessageQueue 、responseCallbacks 和 _uniqueId
// 记住它们的数据类型
// responseCallbacks:用于保存 Objective-C 与javascript环境相互调用的回调模块。通过 _uniqueId加上时间戳来确定每个调用的回调。
// startupMessageQueue:保存类实例化过程中需要发送给 JavaScirpt 环境的消息。
self.startupMessageQueue = [NSMutableArray array]; // 数组
self.responseCallbacks = [NSMutableDictionary dictionary]; // 字典
_uniqueId = 0;
}
因为 webview
的 navigationDelegate
属性指向了 WKWebViewJavascriptBridge
实例,那么 webview
就有权执行 WKWebViewJavascriptBridge
实例的某些已实现的方法,如:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
// 如果不是指定的 webView 则不做任何处理
if (webView != _webView) { return; }
NSURL *url = navigationAction.request.URL;
__strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
// 判断是否是前端和Native协商好的协议格式
if ([_base isWebViewJavascriptBridgeURL:url]) {
// 是否是 webView loaded 的url
if ([_base isBridgeLoadedURL:url]) {
// 注入 WebViewJavascriptBridge_JS.m 中的 JS 字符串
// JS 就能通过全局 window 对象访问 WebViewJavascriptBridge 了
[_base injectJavascriptFile];
// 是否是JS主动发送的 call 消息
} else if ([_base isQueueMessageURL:url]) {
// 刷新 MessageQueue 队列
[self WKFlushMessageQueue];
} else {
// 未知消息
[_base logUnkownMessage:url];
}
// 取消执行正常的http请求流程,Native 自己处理剩余逻辑
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
// 如果不是前端和Native协商好的URL格式,说明是正常的http请求
if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
[_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
} else {
decisionHandler(WKNavigationActionPolicyAllow);
}
}
从以上代码可知,Native
获取JS
发送的消息的方式,就是拦截URL
请求。
五、JavaScript 初始化 Bridge
前端使用全局 window
对象下的 WebViewJavascriptBridge
,来源于 Native
注入 的WebViewJavascriptBridge_JS.m
中的 JS
字符串。
前端需要使用 WebViewJavascriptBridge
对象做一些初始化的工作,每次 call Native
端的方法,都需要主动改变 iframe
的 src
属性,达到使用 url
请求的效果,然后 Native
端便可以拦截到这个请求。具体示例代码如下,可作为参考。
const u = window.navigator.userAgent;
const isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1;
const isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/);
if (isAndroid) {
initAndroidBridge(bridge => {
bridge.init((message, responseCallback) => {
responseCallback();
});
// 如果是页面初始化完成后需要立即调用原生的方法
// 那么需要在对应的页面先订阅『页面初始化完成的消息』
// 然后在这里主动触发
// 例如 Vue 的 EventBus 等。
});
}
function initAndroidBridge(callback) {
if (window.WebViewJavascriptBridge) {
callback(window.WebViewJavascriptBridge);
} else {
document.addEventListener('WebViewJavascriptBridgeReady', () => {
callback(window.WebViewJavascriptBridge);
}, false);
}
}
function initIOSBridge(callback) {
if (window.WebViewJavascriptBridge) {
return callback(window.WebViewJavascriptBridge);
}
if (window.WVJBCallbacks) {
return window.WVJBCallbacks.push(callback);
}
window.WVJBCallbacks = [callback];
const WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(() => {
document.documentElement.removeChild(WVJBIframe);
}, 0);
}
const call = (function () {
if (isiOS) {
return function (name, data, callback) {
initIOSBridge(bridge => bridge.callHandler(name, data, callback));
}
}
return function (name, data, callback) {
window.WebViewJavascriptBridge.callHandler(name, data, callback);
}
})();
const register = (function () {
if (isiOS) {
return function (name, callback) {
initIOSBridge(bridge => bridge.registerHandler(name, callback));
}
}
return function (name, callback) {
window.WebViewJavascriptBridge.registerHandler(name, callback);
}
})();
export default {
call,
register
}
六、JavaScript调用iOS
JS
主动调用 Native
,首先Native
要注册好对应的方法,所以从 Native
使用register
注册方法开始,然后 JS
再使用call
调用 Native
注册好的方法。
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"Response from testObjcCallback");
}];
registerHandler
方法在 WebViewJavascriptBridge.m
文件中
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
// 给 messageHandlers 字典赋值
// handlerName 作为键名
// handler 拷贝作为键值
_base.messageHandlers[handlerName] = [handler copy];
}
注册完成,前端可以调用了:
window.WebViewJavascriptBridge.callHandler(handlerName, data, callback)
在前面提到过,Native
执行[_base injectJavascriptFile]
方法将 WebViewJavascriptBridge_JS
代码注入到webview
中,所以window
全局对象上就有了 WebViewJavascriptBridge
,即:
window.WebViewJavascriptBridge = {
callHandler,
registerHandler,
// ... 其他方法
};
所以看下 WebViewJavascriptBridge_JS.m
文件,找到 callHandler
方法:
function callHandler(handlerName, data, responseCallback) {
// 参数重载,data可传可不传
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
// 执行 _doSend 方法
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}
看下 _doSend
方法做了哪些事情:
function _doSend(message, responseCallback) {
// responseCallback 也是可选的,有这样的场景:
// 前端 call Native 方法之后,并不需要 Native 回应,也就是单向通信
if (responseCallback) {
// 前端 call 时,callbackId 的组成:cb前缀、uniqueId、时间戳
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
// responseCallbacks 对象保存 callbackId 对应的responseCallback
responseCallbacks[callbackId] = responseCallback;
// 给 message 添加 callbackId属性,值为 calbackId
message['callbackId'] = callbackId;
}
// 将此次 message 放到 sendMessageQueue 队列中
sendMessageQueue.push(message);
// Native 接收前端消息的核心:改变 iframe 的 src 属性,Native 会拦截到
// Native 判断 scheme 是否是与前端约定好的,做出具体处理
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
好了,Native
注册好了方法,前端也调用了,这时候,Native
会拦截到 URL
请求,看代码:
- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id)listener {
if (webView != _webView) { return; }
NSURL *url = [request URL];
if ([_base isWebViewJavascriptBridgeURL:url]) {
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
} else if ([_base isQueueMessageURL:url]) {
// Native 拦截到了前端 call 来的消息
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
[_base flushMessageQueue:messageQueueString];
} else {
[_base logUnkownMessage:url];
}
[listener ignore];
} else if (_webViewDelegate && [_webViewDelegate respondsToSelector:@selector
// ... 省略
} else {
// ... 省略
}
}
上面的代码,之前笔记有学习过,现在主要关注Native
拦截到了前端 call
来的消息,即:
// 执行 _evaluateJavascript 方法
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
[_base flushMessageQueue:messageQueueString];
首先获取到 "WebViewJavascriptBridge._fetchQueue();"
这个字符串, 通过调用
[_base webViewJavascriptFetchQueyCommand]:
方法
- (NSString *)webViewJavascriptFetchQueyCommand {
return @"WebViewJavascriptBridge._fetchQueue();";
}
然后交给 _evaluateJavascript
去处理,其实就是执行 JS 字符串代码
- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand {
return [_webView stringByEvaluatingJavaScriptFromString:javascriptCommand];
}
接着看下那段 JS
字符串代码,在 WebViewJavascriptBridge_JS.m
文件中找到:
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
sendMessageQueue
是个数组字符串,其元素格式如下:
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
[
{
handlerName,
data,
callbackId
}
]
// 别忘了 responseCallbacks 对象保存 callbackId 对应的responseCallback
responseCallbacks[callbackId] = responseCallback;
下面将 _fetchQueue
的执行结果 messageQueueString
作为参数传入并执行 [_base flushMessageQueue]
:
- (void)flushMessageQueue:(NSString *)messageQueueString{
// messageQueueString 必须是一个合格的可被解析的字符串数组
if (messageQueueString == nil || messageQueueString.length == 0) {
NSLog(@"WebViewJavascriptBridge: WARNING: ObjC got nil while fetching the message queue JSON from webview. This can happen if the WebViewJavascriptBridge JS is not currently present in the webview, e.g if the webview just loaded a new page.");
return;
}
// 将前端的字符串对象解析为数组
id messages = [self _deserializeMessageJSON:messageQueueString];
for (WVJBMessage* message in messages) {
// 数组元素的每一项 message 必须是合格的字典结构
if (![message isKindOfClass:[WVJBMessage class]]) {
NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
continue;
}
[self _log:@"RCVD" json:message];
NSString* responseId = message[@"responseId"];
// 前端主动 call Native 方法,暂时没有 responseId,直接走 else 分支
if (responseId) {
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
} else {
// responseCallback 是一个 block
WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
if (callbackId) {
// responseCallback
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
};
} else {
responseCallback = ^(id ignoreResponseData) {
// Do nothing
};
}
// 还记得吗?Native 注册方法时,执行了:
// _base.messageHandlers[handlerName] = [handler copy];
// 现在要根据与前端约定好的 handlerName 取出对应的 handler
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
if (!handler) {
// 走到这里,说明前端调用了Native未注册的方法
NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
continue;
}
// 传入对应的参数,执行 handler
// 也就是说,Native 收到了前端 call 来的消息,要执行自己的逻辑了
handler(message[@"data"], responseCallback);
}
}
}
上面代码的 handler
就是Native
注册的方法:
self.bridge registerHandler:@"login" handler:^(id data, WVJBResponseCallback responseCallback) {
NSString *response = @"this is a responsive message from native";
// 这里的 responseCallback 就是 上面代码判断 responseId 不存在时定义的 block
// 将需要返回给前端的参数 response 传入
responseCallback(response);
}];
那么 responseCallback
做了什么呢?再 copy
下代码,方便查看:
responseCallback = ^(id responseData) {
if (responseData == nil) {
// responseData 不存在的时候返回空
responseData = [NSNull null];
}
// 重新组装下 msg,执行 self(base) 的 _queueMessage 方法
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
};
- (void)_queueMessage:(WVJBMessage*)message {
// WKWebview 初始化完成,执行 injectJavascriptFile 时已将startupMessageQueue 置为 nil
// self.startupMessageQueue = nil;
if (self.startupMessageQueue) {
[self.startupMessageQueue addObject:message];
} else {
[self _dispatchMessage:message];
}
}
下面具体看_dispatchMessage
的逻辑,别忘了 msg
的结构:
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
- (void)_dispatchMessage:(WVJBMessage*)message {
// 将字段转为字符串
NSString *messageJSON = [self _serializeMessage:message pretty:NO];
[self _log:@"SEND" json:messageJSON];
// 省略一系列的转义代码..
// 使用 stringWithFormat 方法拼接字符串
// 最后执行的js代码相当于:WebViewJavascriptBridge._handleMessageFromObjC(message)
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
if ([[NSThread currentThread] isMainThread]) {
[self _evaluateJavascript:javascriptCommand];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self _evaluateJavascript:javascriptCommand];
});
}
}
在 WebViewJavascriptBridge_JS.m
文件中找到 _handleMessageFromObjC
方法:
function _handleMessageFromObjC(messageJSON) {
// _handleMessageFromObjC 做了个中转,实际是执行 _dispatchMessageFromObjC 方法
_dispatchMessageFromObjC(messageJSON);
}
到这里,说明Native 正式把控制权交给 JS 了
,执行 _dispatchMessageFromObjC
方法:
function _dispatchMessageFromObjC(messageJSON) {
if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
}
function _doDispatchMessageFromObjC() {
// 转成 JS 熟悉的 JSON
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;
// 当前 messageJSON 中存在 responseId
// 再把 msg 结构 copy 过来:
// WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
if (message.responseId) {
// 还记得吗?前端 call Native 方法的时候,执行了 _doSend 方法
// 使用responseCallbacks 对象保存 callbackId 对应的responseCallback
// responseCallbacks[callbackId] = responseCallback;
// 现在根据 responId ,也就是当时的 callbackId 取出 callback 执行
// 即:Native 的逻辑走完了,要开始自己的回调了
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
// 这个 responseCallback 就是我们 call 时写的 callback,现在传入 message.responseData执行
responseCallback(message.responseData);
// 调用完成后,删除 callback 引用
delete responseCallbacks[message.responseId];
} else {
// ... 省略当前用不到的代码...
}
}
}
七、iOS主动调用JavaScript方法
Native 主动调用 JS,首先 JS 要注册好对应的方法,所以本节笔记从 JS 使用 register 注册方法开始,然后 Native 再使用 call 调用 JS 注册好的方法。
window.WebViewJavascriptBridge.registerHandler(handlerName, callback)
registerHandler 方法在 WebViewJavascriptBridge_JS.m 文件中:
function registerHandler(handlerName, handler) {
// 与 Native 注册方法一模一样
// 给 messageHandlers 对象赋值
// handlerName 作为键名
// handler 拷贝作为键值
messageHandlers[handlerName] = handler;
}
注册完成,Native 可以调用了:
[self.bridge callHandler:@"handlerName" data:@"event from native" responseCallback:^(id responseData) {
NSLog(@"message from JavaScript: %@", responseData);
}];
在 WKWebViewJavascriptBridge.m 中找到 callHandler 方法:
- (void)callHandler:(NSString *)handlerName {
[self callHandler:handlerName data:nil responseCallback:nil];
}
- (void)callHandler:(NSString *)handlerName data:(id)data {
[self callHandler:handlerName data:data responseCallback:nil];
}
- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
[_base sendData:data responseCallback:responseCallback handlerName:handlerName];
}
作为示例,我们传入全部的三个参数:handlerName、data 和 responseCallback,也就是执行上面第三个方法:
[_base sendData:data responseCallback:responseCallback handlerName:handlerName];
在 WebViewJavascriptBridgeBase.m 文件中找到对应的方法:
- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
// 定义一个 message 字典
NSMutableDictionary* message = [NSMutableDictionary dictionary];
// 给 message 字典赋值 data
if (data) {
message[@"data"] = data;
}
if (responseCallback) {
// 这个与 JS call Native 方法一样了
// 拼接好唯一的 callbackId
// 将 responseCallback 放入 responseCallbacks 字典中
NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
self.responseCallbacks[callbackId] = [responseCallback copy];
message[@"callbackId"] = callbackId;
}
if (handlerName) {
// message 字典添加 handlerName
message[@"handlerName"] = handlerName;
}
// 将组装好的 message 传入 _queueMessage 方法并执行
[self _queueMessage:message];
}
先记住 message 字典的结构:
NSMutableDictionary* message = {
@"data": data, // 如果有 data 的话
@"callbackId": @"objc_cb_唯一的uniqueId",
@"handlerName": handlerName
};
另外在 responseCallbacks 字典中也添加了 callbackId,其值为 responseCallback 的拷贝。
self.responseCallbacks[callbackId] = [responseCallback copy];
下面具体看下 _queueMessage 方法,
- (void)_queueMessage:(WVJBMessage*)message {
if (self.startupMessageQueue) {
[self.startupMessageQueue addObject:message];
} else {
[self _dispatchMessage:message];
}
}
_dispatchMessage
方法:
- (void)_dispatchMessage:(WVJBMessage*)message {
// 将 message 字典转成对象字符串
NSString *messageJSON = [self _serializeMessage:message pretty:NO];
[self _log:@"SEND" json:messageJSON];
// ... 省略一系列转义代码
// 和 JS 调用 Native 一样,获取到可执行的 javascriptCommand
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
if ([[NSThread currentThread] isMainThread]) {
[self _evaluateJavascript:javascriptCommand];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self _evaluateJavascript:javascriptCommand];
});
}
}
WebViewJavascriptBridge._handleMessageFromObjC
方法:还记得 message
的结构吗?
NSMutableDictionary* message = {
@"data": data, // 如果有 data 的话
@"callbackId": @"objc_cb_唯一的uniqueId",
@"handlerName": handlerName
};
将 message
转为字符串后传入 _dispatchMessageFromObjC
执行
function _dispatchMessageFromObjC(messageJSON) {
if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
}
function _doDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else { // 看到 message 的结构,此时依然走这个 else 分支
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
}
// JS 注册方法时,messageHandlers[handlerName] = handler;
// 这里根据 handlerName 取出来用
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
// 执行 handler
handler(message.data, responseCallback);
}
}
}
}
handler
就是 JS
注册的方法的回调:
window.WebViewJavascriptBridge.registerHandler(handlerName, function handler (data, responseCallback) {
// ... 走完 JS 逻辑之后,要回调 Native call JS 方法的第二个或第三个参数,也就是 Native 的 callback,即:
var dataFromJs = {
name: 'zhaoyiming',
age: 18
};
responseCallback(dataFromJs);
// - (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
// [_base sendData:data responseCallback:responseCallback handlerName:handlerName];
// }
})
但是这里有个问题, Native
能直接执行 JS
方法,JS
不能直接执行 Native 的方法。
那么这里的 responseCallback
其实就是上文 else
分支中自定义的 responseCallback
:
responseCallback = function(responseData) {
// 在自定义的 responseCallback 中执行 _doSend 方法,看看做了哪些操作,来达到 JS 『间接』调用 Native 方法的
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
_doSend
方法:
根据上文执行,这里的 message 结构为:
const message = {
handlerName,
responseId: callbackResponseId, // 重点在这个地方,这次 responseId 有值了
responseData
};
function _doSend(message, responseCallback) {
// 这次 responseCallback 没有传,是 undefined,所以不走 if 语句块内的逻辑
if (responseCallback) {
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message['callbackId'] = callbackId;
}
// 还记得 sendMessageQueue 的作用吗?上一节笔记学习 JS 调用 Native 方法时有用到
// 它是个数组,可以存放很多的 message
sendMessageQueue.push(message);
// *** 重点在这里 ***
// 前端修改 iframe 的 src 属性值,主动触发 Native 拦截(JS 调用 Native 方法的过程)
// 然后 Native 就可以拿到 sendMessageQueue 中的数据,并解析
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
这时,逻辑又回到 Native
拦截 URL
请求的那个方法中了。其实最终还是 webview
执行stringByEvaluatingJavaScriptFromString
方法并传入 sendMessageQueue
对象字符串。
然后来到 flushMessageQueue
方法(WebViewJavascriptBridgeBase),这个时候 responseId 有值了:
- (void)flushMessageQueue:(NSString *)messageQueueString{
// ... 省略判断
id messages = [self _deserializeMessageJSON:messageQueueString];
for (WVJBMessage* message in messages) {
// ...省略判断
NSString* responseId = message[@"responseId"];
if (responseId) {
// 执行 sendData 方法时,已将 Native 主动调用 JS 方法的回调放入了 responseCallbacks 中
// self.responseCallbacks[callbackId] = [responseCallback copy];
// 现在取出来用
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
// responseCallback 执行了,JS 成功间接调用 Native 方法了
responseCallback(message[@"responseData"]);
// 调用成功之后,删除多余引用
[self.responseCallbacks removeObjectForKey:responseId];
} else {
// ... 这次走上面的 if 分支,responseId 有值
}
}
}
至此,Native 主动调用 JS 方法的整个逻辑就走完了。