大家好,今天笔者分享一下自己在项目开发中,对iOS和H5之间的通信的一些知识点。主要是针对于苹果APP混合开发中的一点点小知识。然后是对WKWebViewJavascriptBridge,这个iOS原生OC代码和H5之间的通信桥接第三方包的一些分析。
首先在这里,先说明,这篇文章iOS这块的控件用的是WKWebview。因为再iOS12之后,UIWebview因为性能不足和内存占有过大等一系列问题,Apple公司最终决定将它慢慢移出iOS的舞台。
先来大致看下WKWebview一些常规用到的代理方法:
// --------------- WKNavigationDelegate -----------------
// 请求之间,决定是否跳转网页:一般用户在点击一个链接时跳转到下一个页面之间会调用这个方法
// 重点的代理方法,拦截webView 中url的方法
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
// 获取响应数据之后,决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
// 开始加载页面时调用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
// 主机地址被重定向时调用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
// 页面加载失败时调用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
// 页面加载完毕时调用
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;
// 跳转失败时调用
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;
// 证书验证步骤:如果需要证书验证,与使用AFN进行HTTPS证书验证是一样的
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler;
// ------- WKUIDelegate方面一个比较重要的代理 ---------------
// 在webview中使用alert函数时,需要执行这个代理,否则将无法打开
// 坏处在于,需要我们硬性执行代理方法,好处在于,可以在我们原生iOS这边自定义弹出框
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
接下来笔者说一下,最基础的iOS和js之间的通信方式,在WKWebview这边主要的方法:
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;
这里我们和UIWebview中执行通信代码的方法比较一下:
- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;
可以看到,WKWebview这边的通信方法相对而言比较的人性化,因为iOS传递给js代码之后,得到的结果不一定是NSString类型,在很多情况下我们会需要得到一个对象、数组等等数据。
接下来,顺便提一下一个iOS和H5之间通信的架包:JavaScriptCore.framework。这个架包是苹果自带支持的架包,主要目的就是让开发人员更加简便的去操作iOS和H5之间的通信代码。导入方式是直接从build phases中导入。如下图:
接下来,我们正式进入,对WKWebViewJavascriptBridge这个第三方包的使用和源码的分析。首先,笔者这边给出了一个简单的使用WKWebViewJavascriptBridge的demo,代码地址: https://github.com/IBIgLiang/WKWebView-WebViewJavascriptBridge
。而WKWebViewJavascriptBridge这个第三方包的GitHub地址如下: https://github.com/marcuswestin/WebViewJavascriptBridge。里面已经很明确的指出的使用流程和注意事项。
这里笔者用一张图来总结一下这个过程,当然这些代码在demo中都有,大家可以照着图看下:
接下来,我们重点来看看这个第三方的源代码,我们这里通过流程图,大致看下整个过程。先说明,笔者这里暂时不考虑UIWebview的操作,因为笔者已经说了它将退出。作为iOS开发,我们就从iOS这边开始整个流程,先从ViewController开始:
从上图中可以看出,一般来说,VC中所作的基本上都是准备工作,无论是注册通信还是响应通信,除了当响应通信时,没有需要传递的参数, 因为WebViewJavascriptBridge本身所有的保存的参数的消息队列是统一处理的。
接下来我们来看看,上面说过的一个重要的代理:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
这个代理主要是做请求拦截操作,WebViewJavascriptBridge作者在拦截的过程中,加载WebViewJavascriptBridge_JS文件,然后是处理消息队列中的数据,具体我们还是一起看流程图:
这张图的发起点是在H5的JS端:
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
// 创建一个看不见的iframe,发起一个请求,这个请求用来加载WebViewJavascriptBridge_JS.m中的js代码
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
调用setupWebViewJavascriptBridge这个方法之后,通过iFrame发送https://bridge_loaded请求,进入decidePolicyForNavigationAction代理中,然后走上面流程图中的流程。当地址是https://bridge_loaded时,此时直接进入[_base injectJavascriptFile];代码段。具体内容如下:
//TODO:WebViewJavascriptBridge桥接js文件的导入,并且发送iOS端业务数据给js
- (void)injectJavascriptFile {
NSString *js = WebViewJavascriptBridge_js();
[self _evaluateJavascript:js];
//导入桥接文件之后,将iOS端需要发送的数据发送给js端
//self.startupMessageQueue中放置了iOS发起给js的数据
if (self.startupMessageQueue) {
NSArray* queue = self.startupMessageQueue;
self.startupMessageQueue = nil;
for (id queuedMessage in queue) {
// 发送iOS端数据到JS端
[self _dispatchMessage:queuedMessage];
}
}
}
主要做的就是导入js桥接问题,然后就是发送iOS端的业务参数,这个参数的来源已经在上面ViewController准备工作的流程图中写明。
除了JS端的iFrame发送https://bridge_loaded请求之外,还有WebViewJavascriptBridge_js中的https://wvjb_queue_message,这个请求发起的主要作用就是处理消息队列中的信息。也就是[self WKFlushMessageQueue];这个步骤。这个消息队列的处理包含了JS端发送的数据。代码如下:
//TODO:处理消息队列中的数据
- (void)WKFlushMessageQueue {
// 读取js发送的数据
[_webView evaluateJavaScript:[_base webViewJavascriptFetchQueyCommand] completionHandler:^(NSString* result, NSError* error) {
if (error != nil) {
NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
}
// 处理消息队列中的数据
[_base flushMessageQueue:result];
}];
}
[_base flushMessageQueue:result];这个方法中包含了JS端发送的数据和iOS处理完数据之后回调给JS端的数据的处理,包括两个部分:一个是iOS响应JS端通信后JS端回调给iOS端的消息内容,key值包含callbackId;另一个是JS响应iOS端的通信的消息内容,key值包含responseId。主要代码段:
NSString* responseId = message[@"responseId"];
if (responseId) {
// 如果是js注册函数中回调回来的数据,就直接回到iOS的callHandler的block中
// js注册registerHandler -> iOS发送相应业务数据callHandler -> js中注册函数的block回调到iOS中
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
} else {
// iOS注册函数, js相应数据后,回到iOS注册函数registerHandler中的block,然后回调给js
//iOS注册registerHandler -> js发送相应业务数据callHandler -> 进入iOS的registerHandler的block中 -> responseCallback回调给js
WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
if (callbackId) {
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
// 将js发送给iOS之后,iOS处理完业务,重新将业务数据发送给js端
[self _queueMessage:msg];
};
} else {
responseCallback = ^(id ignoreResponseData) {
// Do nothing
};
}
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
if (!handler) {
NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
continue;
}
handler(message[@"data"], responseCallback);
到此,除了桥接js文件没有涉及讲解之外,其余重要步骤都已讲解完毕。
============================================================
接下来就是最后的WebViewJavascriptBridge_JS文件。这个js文件的内容其实很好理解,就是为iOS和JS两端的通信创建一个通用的WebViewJavascriptBridge对象:
// 定义一个webview和js之间的桥
window.WebViewJavascriptBridge = {
// 用于JS端注册通信
registerHandler: registerHandler,
// 用于存储JS端相应iOS端的通信的参数,放入_fetchQueue中
callHandler: callHandler,
// 请求超时字段
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
// 消息队列,用于放置JS端发送给iOS端的数据
_fetchQueue: _fetchQueue,
// 处理iOS端发送给JS端的数据
_handleMessageFromObjC: _handleMessageFromObjC
};
这个文件里面的方法中,需要重点说明一下的就是_dispatchMessageFromObjC这个方法中的这个代码段:
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
}
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
handler(message.data, responseCallback);
}
}
其实大家可以发现,这个代码段和笔者上面贴出来flushMessageQueue部分的那个代码段是相对应的,它就是JS端处理iOS发送过来的数据的过程。这里笔者就不具体展开了。
由于篇幅长度原因,关于iOS中的WKWebView和H5之间通信及WKWebViewJavascriptBridge的源码分析就到此为止了。
笔者将在下一篇文章WKURLSchemeHandler在WKWebView的拦截请求中的使用中指出NSURLProtocol拦截这WKWebView无效的问题以及原因(scheme底层注册有关),并且使用WKURLSchemeHandler拦截请求,当前有个前提是这个请求的scheme是自定义的,有兴趣的同学可以瞄一眼!