iOS WKWebView 与 JS 交互

2020-01-03  本文已影响0人  ZT_Story

应对苹果公司的号召,2020年还是要把之前老项目的UIWebView都替换成WKWebView。
单纯换View倒也不难,除了代理方法有点区别之外,加载网页的使用方式都是类似的。
但现在越来越多应用都采用混合开发的模式,所以就需要Native与JS之间进行良好的通信。
之前UIWebView与JS通信是采用JavaScriptCore来实现的
通过给JSContext动态注入OC对象来实现JS调用Native
由于以后苹果不允许使用UIWebView我这里也就不多讲述其实现了
主要还是说一说WKWebView是如何与JS之间交互的!

开讲之前

今天我们从两个方面来讲述OC与JS交互:
1、通过原生的WKWebViewConfiguration
2、通过使用WebViewJavascriptBridge

通过Native调用JS的方式,没有任何争议,几乎都是用WebView提供的

[webView evaluateJavaScript:@"" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
        
}];

这里我们重点讨论JS如何调用Native

WKWebViewConfiguration、WKUserContentController

image.png

WKWebViewConfiguration对象有一个很关键的属性参数userContentController
我们可以把它当做内容交互控制器,可以自己注入JS代码及JS调用原生方法注册

- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;

addScript需要与remove成对出现,在dealloc时需要注意调用

- (void)removeScriptMessageHandlerForName:(NSString *)name;

在JS里我们可以通过

window.webkit.messageHandlers.<注册的方法名>.postMessage(<需要传递的参数>);

的方式来调用我们在Native端注册的方法
同时Native会通过WKScriptMessageHandler代理方法

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;

接收JS传递过来的参数,参数内容都封装在WKScriptMessage对象中
其中,name为注册的方法名,body为传递的参数对象
由于只能接收一个参数,所以JS通常采取Object转JSON字符串的形式传递多参数,Native这边用NSDictionary接收

WebViewJavascriptBridge

这是github上非常火的一个桥接库https://github.com/marcuswestin/WebViewJavascriptBridge有12.9k的Star
看了看他的源码,整个处理的非常优雅
主要是利用iframe设置src的方式通知Native完成通信过程
所以从UIWebView到WKWebView的改动几乎很小,并且还支持相互的通信之间的回调,可以说是非常全面的Bridge了
我们在这里简单的分析一下这个库是如何实现通信的

简易流程图
开始之前我们先看一下这个库的文件结构
image.png
整个库总共也就八个文件,算是十分精简也是十分清晰了
核心代码在WebViewJavascriptBridge_JSWebViewJavascriptBridgeBase
前者负责实现注入JS的通信解析对象
后者负责实现Native解析JS信息的对象
其余两个类文件是用来扩展到UIWebView和WKWebView的,主要是实现对应的协议方法,用作拦截URL
JS调用Native

打开WebView页面时,默认通过iframe设置

WVJBIframe.src = 'https://__bridge_loaded__';

客户端在拦截到初始化的指令时会通过这两步进行预设好的JS注入

if ([_base isBridgeLoadedURL:url]) {
    [_base injectJavascriptFile];
}
......
- (void)injectJavascriptFile {
    NSString *js = WebViewJavascriptBridge_js();
    [self _evaluateJavascript:js];
}

初始化工作做好之后,按照流程就是Native端的注册方法

[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
     NSLog(@"testObjcCallback called: %@", data);
     responseCallback(@"Response from testObjcCallback");
}];

等待JS端的调用
JS通过bridge对象的callHandler方法

var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
     callbackButton.innerHTML = 'Fire testObjcCallback'
     callbackButton.onclick = function(e) {
     bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
          log('JS got response', response)
     })
}
......

function callHandler(handlerName, data, responseCallback) {
     if (arguments.length == 2 && typeof data == 'function') {
            responseCallback = data;
            data = null;
     }
     _doSend({ handlerName:handlerName, data:data }, responseCallback);
}  
function _doSend(message, responseCallback) {
     if (responseCallback) {
            var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
            responseCallbacks[callbackId] = responseCallback;
            message['callbackId'] = callbackId;
     }
     sendMessageQueue.push(message);
     messagingIframe.src = 'https://__wvjb_queue_message__/';
}

拦截URL变化主要是通过WebView的delegate获取

/// UIWebView 通过
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
/// WKWebView 通过
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;

设置了src为https://__wvjb_queue_message__/,Native拦截之后通过获取sendMessageQueue中的message完成方法名的提取和参数的提取

[_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];
}];

通过messageJSONString中的信息匹配到之前注册的方法名和block,调用执行。
如果Native注册的方法中有返回responseCallBack信息,则将返回的信息参数与JS提供的callBackId组成新的对象,通知JS解析,JS通过callBackId找到对应方法并传递参数执行

WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];

以上就是完整的JS调用Native流程

Native调用JS

反过来,Native调用JS,其实双方注册方法和调用方法都是类似的,整个流程反一下就可以了,具体我这里就不细说了,只要前面的理解了,后面就很容易想明白了,可以参考源码Demo。

总结一下

1、JS通知Native信息变化通过

iframe.src='https://__bridge_loaded__/'                  // 初始化
iframe.src='https://__wvjb_queue_message__/'            // 发送消息

2、Native通知JS传递参数通过

[webView evaluateJavaScript:@"" completionHandler:^(id _Nullable result, NSError * _Nullable error) {}];

整个交互过程依赖双方存储注册方法信息,并通过方法名和参数来完成跨界调用

这个第三方库考验开发者要有一些基础的前端开发经验,否则对于里面近半数的JS源码可能不太理解,重点是设置iframe.src

以上就是本次我想分享的关于WKWebView与JS交互的内容
ps:内容建议结合源码Demo一起食用,更容易理清思路
希望对你有帮助

上一篇下一篇

猜你喜欢

热点阅读