OC与JS交互之 WebViewJavascriptBridge
导语
本文将讲解 UIWebView 和 WKWebView 通过 WebViewJavascriptBridge 三方库进行通信的原理;梳理OC -- > JS, JS --> OC 调动交互的流程;和OC方、JS方各自做了具体什么操作才使Native与H5有了交互的功能。本文并不会去讲解 WebViewJavascriptBridge 库的使用和方法的讲解,你可以通过以下参考内容去了解。
- 使用WKWebView替换UIWebView
https://www.jianshu.com/p/6ba2507445e4 - WebViewJavascriptBridge浅析
http://www.cnblogs.com/LeeGof/p/8143408.html - WKWebView 那些坑
https://mp.weixin.qq.com/s/rhYKLIbXOsUJC_n6dt9UfA
OC 调用 JS
oc 调用 jsOC 相关代码: 调用与JS商量好的 oc_to_js_js’registerFunctionName 函数
[_webViewBridge callHandler:@“oc_to_js_js’registerFunctionName” data:@"oc传给js的数据” responseCallback:^(id responseData) {
NSLog(@"JS执行完回调给OC的数据:%@“,responseData);
}];
JS 相关代码: 注册一个叫 oc_to_js_js’registerFunctionName 的函数
setupWebViewJavascriptBridge(function(bridge) {
bridge.registerHandler('oc_to_js_js’registerFunctionName', function(data, responseCallback) {
alert(‘oc成功调用js,并打印数据data:’+data);
responseCallback(‘js执行完回调给OC的数据');
})
})
如上OC和JS代码可以看出,OC需要调用JS注册的‘ oc_to_js_js’registerFunctionName’ 方法,并传递数据data给JS;JS在收到OC的消息后alert了传递过来的数据,并回传了一份数据给OC。
那么这个 oc--data--js--data--oc 的流程到底是怎样的呢
大致步骤:
- 步骤一 (oc环境) : OC先声明一段JS代码,在第一次进入此webView页的时候注入到 webView中,之后webView上下文中就有了这些js函数。
注入JS代码
- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener {
...
if ([_base isQueueMessageURL:navigationAction.request.URL]) {
[webView stringByEvaluatingJavaScriptFromString:@“js代码”]
}
...
}
- 步骤二 (js环境) : 注册声明 js 会被 oc 调用的方法存放在 js 环境的字典中;
保存js注册的函数
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
- 步骤三 (oc环境) :oc 发起调用,生成一个message字典,三个参数(handlerName 方法名,data数据, callbackId 回调方法)
- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
NSMutableDictionary* message = [NSMutableDictionary dictionary];
if (data) {
message[@"data"] = data;
}
if (responseCallback) {
NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
self.responseCallbacks[callbackId] = [responseCallback copy];
message[@"callbackId"] = callbackId;
}
if (handlerName) {
message[@"handlerName"] = handlerName;
}
[self _queueMessage:message];
}
- 步骤四 (oc环境) : 把message字典转成json字符串,处理字符串里面的\、\、\r、\n 等特殊字符。生成新的字符串并加上
WebViewJavascriptBridge._handleMessageFromObjC(‘messageJSON‘),形成一段字符串形式的js代码(既通过提前注入js中的函数_handleMessageFromObjC去处理 messageJSON 参数)。
- (void)_dispatchMessage:(WVJBMessage*)message {
NSString *messageJSON = [self _serializeMessage:message pretty:NO];
[self _log:@"SEND" json:messageJSON];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"];
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
}
- 步骤五 (oc环境) : 通过调用oc系统的 stringByEvaluatingJavaScriptFromString 方法让webView执行上述js代码。 注: WKWebView 为 evaluateJavaScript 方法.
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
dispatch_sync(dispatch_get_main_queue(), ^{
[webView stringByEvaluatingJavaScriptFromString:javascriptCommand]
});
- 步骤六 (js环境) : 通过 oc 传入过来的json数据,解析后取出 functionName 和 callbackId,从事先注册好的缓存中获取对应handler函数和callback函数, 并执行。
取出 callback 函数和 handler 函数执行调用,在handler中执行 callback 数据回传操作。
responseCallback = responseCallbacks[message.responseId];
var handler = messageHandlers[message.handlerName];
if (handler) {
handler(message.data, responseCallback);
}
- 步骤七 (js环境) :在js的 callback 函数执行完后,需要从js环境发生数据给oc环境,这时候js会改变html根节点下隐性的<iframe>标签的src,当iframe的src改变时会发起Request,这时候oc就能监听到js发生了Request操作 。 这个环节的具体操作也就跟下面即将要讲的 js 调用 oc 的流程类似了。
JS 调用 OC
js 调用 ocJS调用OC 一般会有三种方式
- iOS7 引入了 JavaScriptCore,可以初始化一个 JSContext 对象,然后约定好一个方法名就好了。
- 特殊的一个 Scheme 。客户端这边会拦截到这种指令格式的 URL 需求,实现一个 JS 到 Native 传递消息的一个过程
- 轮询,对于 JS 他需要把给 Native 传递的消息,转化成一个 JSON ,客户端这边一般会开一个线程,每隔一段时间会调 JS 的方法,从这个方法里面把 JS 需要给 Native 传递的消息全部取出来,取出来之后再去做相应的操作,客户端开销比较大。
本文只要讲解第二种方式,通过Scheme去实现一个 JS 到 Native 传递消息的一个过程。
OC 代码 : 注册一个叫 js_to_oc_oc’registerFunctionName 的方法
[webViewBridge registerHandler:@"js_to_oc_oc’registerFunctionName” handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@“js成功调用oc, 并获取数据data”);
responseCallback(@“oc回调给js的数据 如:支付失败”);
}];
JS 代码 : 调用与OC商量好的 js_to_oc_oc’registerFunctionName 方法
WebViewJavascriptBridge.callHandler('js_to_oc_oc’registerFunctionName', "{\"content\" : \"js传递给oc的数据内容\”}”, function(response) {
alert('oc成功被调用,并回传给js的回调数据:’ + response);
document.getElementById("returnValue").value = response;
});
如上OC和JS方法可以看出, JS需要调用OC注册的‘ js_to_oc_oc’registerFunctionName’ 方法,并传递数据json字符串给OC;OC在收到JS的消息后log了传递过来的数据,并回传了一份数据给JS。
大致步骤:
-
步骤一 (oc环境) : OC先声明一段 js 通用代码,在第一次进入此webView页的时候注入到 webView中,之后webView上下文中就有了这些js方法(如果已注入过忽略此步)。
-
步骤二 (oc环境) : 注册oc方法, 存储一个message字典,以 handlerName 为key, handler 为value 。
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}
- 步骤三 (js环境) : js 发起调用方法,传入handlerName,data,callBack 参数, 生成message字典,保存在js环境中。
function callHandler(handlerName, data, responseCallback) {
...
_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);
}
- 步骤四 (js环境) :js发起调用时,会对当前html中的<iframe> 标签的src重新赋值一个scheme, <iframe> 标签会在src改变时自动发起Request跳转, 因此 oc 才会收到消息(下一步)
最开始注入的js代码中,会对当前html添加一个<iframe>, 并设置为隐性
iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(iframe);
重新赋值iframe的src
iframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
- 步骤五 (oc环境) :WKWebview 和 UIWebview 都会有个 decidePolicyForNavigationAction 的代理方法可以拦截Request的url, 对url的scheme进行判断是普通跳转还是JS跳转。
- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener {
...
if ([_base isCorrectProcotocolScheme:url]) {
...
// js 跳转,拦截处理
decisionHandler(WKNavigationActionPolicyCancel);
}else {
decisionHandler(WKNavigationActionPolicyAllow);
}
}
- 步骤六 (oc环境) :如果是js跳转, 通过系统的 stringByEvaluatingJavaScriptFromString 方法调用 ‘WebViewJavascriptBridge._fetchQueue()’ 这段注入过的js代码, 获取此方法的返回值, 既获取步骤3的 message。 注: WKWebView 为 evaluateJavaScript 方法获取.
[webView evaluateJavaScript:@"WebViewJavascriptBridge._fetchQueue();" completionHandler:^(NSString* result, NSError* error) {
// result 为获取到的函数信息(以 handlerName、data、callback) 为键的json字符串
}];
js 函数
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
- 步骤七 (oc环境) :根据从js获取到的函数信息message ,以 handlerName 为key ,从之前 oc 中保存的方法字典中取出对应方法,并执行。
WVJBResponseCallback responseCallback = _responseCallbacks[message[@"responseId"]]; // 获取存储的callback函数
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]]; // 获取存储的handler函数
if (!handler) {
NSLog(@"oc环境中没有注册过此方法");
continue;
}
handler(message[@"data"], responseCallback);
总结
-
OC —>JS : js注册好 functionNameIdentitify , oc 组装好 {functionNameIdentitify, data, callback} 字典,再转化成字符串后,通过系统方法执行js代码,js函数里去解析传入的json串,获取functionName等数据,从缓存中取出对应函数。
-
JS —> OC : 原理类似,oc 注册好 functionNameIdentitify 后 , 当html的<iframe>request后, 系统方法 decidePolicyForNavigationAction 会被执行,获取到当前操作的request.url。 拦截到是js发起的操作后, 通过系统方法stringByEvaluatingJavaScriptFromString 和 js方法._fetchQueue() 获取对应的js数据; 从数据中获取functionName 或 callbackId, 最后在oc缓存的字典中取出对应方法执行。
-
不管是OC —>JS, 还是JS —> OC , 最终都是通过系统的stringByEvaluatingJavaScriptFromString 或者 evaluateJavaScript 方法建立的桥梁,去获取js数据或者传入数据给js。
关于 js 与 oc 交互的更多细节,可以自己查看WebViewJavascriptBridge的源码 https://github.com/marcuswestin/WebViewJavascriptBridge
最后附上一段一开始被注入到webView中的JS代码
if (window.WebViewJavascriptBridge) {
return;
}
if (!window.onerror) {
window.onerror = function(msg, url, line) {
console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line);
}
}
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
var messagingIframe;
var sendMessageQueue = [];
var messageHandlers = {};
var CUSTOM_PROTOCOL_SCHEME = 'wvjbscheme';
var QUEUE_HAS_MESSAGE = '__WVJB_QUEUE_MESSAGE__';
var responseCallbacks = {};
var uniqueId = 1;
var dispatchMessagesWithTimeoutSafety = true;
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({
handlerName: handlerName,
data: data
}, responseCallback);
}
function disableJavscriptAlertBoxSafetyTimeout() {
dispatchMessagesWithTimeoutSafety = false;
}
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message['callbackId'] = callbackId;
}
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
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 {
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);
}
}
}
}
function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}
messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(messagingIframe);
registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);
setTimeout(_callWVJBCallbacks, 0);
function _callWVJBCallbacks() {
var callbacks = window.WVJBCallbacks;
delete window.WVJBCallbacks;
for (var i = 0; i < callbacks.length; i++) {
callbacks[i](WebViewJavascriptBridge);
}
}
}