Hybrid开发之WebViewJavascriptBridge
对于APP开发,一方面需要快速迭代,一方面需要效果好性能优。这样Hybrid模式应运而生。
本文主要解析一下WebViewJavascriptBridge这个OC与JS交互框架的封装思路。
当前版本:v5.0.7
使用方式
OC端
- (void)setupWVJSBridge {
[WebViewJavascriptBridge enableLogging];
_bridge = [WebViewJavascriptBridge bridgeForWebView:_webView];
[_bridge setWebViewDelegate:self];
[_bridge registerHandler:@"Hybird" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"data %@ responseCallBack %@", data, responseCallback);
NSDictionary* dicResult = @{@"ret":@"OK"};
responseCallback(dicResult);
}];
[_bridge callHandler:@"InvokeJavascriptHandler" data:@{ @"say":@"Hello" } responseCallback:^(id responseData) {
NSLog(@"OC get call back from js when OC invoke %@", responseData);
}];
}
JS端
WebViewJavascriptBridge.registerHandler('InvokeJavascriptHandler', function(data, responseCallback) {
responseCallback({say : 'Hi'})
})
function test_Hybird(){
WebViewJavascriptBridge.callHandler('Hybird', {}, function(cb){
alert('js 收到回调 ' + cb.ret)
})
}
当然需要提前注入JS代码。
WebView提前注入JS
首先我们知道,OC可以在webView中方便的执行JS代码:
[webView stringByEvaluatingJavaScriptFromString:@"alert('Hi')"];
而JS直接调用OC没有提供直接的方法(当然了,iOS7 之后Apple提供了JavaScriptCore框架可以使用,本文不详细介绍),那么我们需要通过触发url重定向功能,在WebView的回调方法中捕获url,然后通过特定的url来执行我们需要的方法。
提前注入JS的方法
作者提供的方法是在JS中触发
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
setupWebViewJavascriptBridge()
前端同学需要在JS代码中,提前写入这段代码,才会有Hybrid交互的能力。
当执行上面这段代码的时候,会获取iframe这个属性,通过改变iframe的src来触发UIWebview的回调:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
这里注入的回调url是:
wvjbscheme://BRIDGE_LOADED
顺便说下执行JS的url是:
wvjbscheme://WVJB_QUEUE_MESSAGE
当然我们OC也可以在手动执行JS注入代码
简单点的方法,直接在修改库文件WebViewJavascriptBridge,把WebViewJavascriptBridgeBase *_base;拿到.h文件中,我们就可以方便的调用,需要在网页加载完回调中执行。
- (void)webViewDidFinishLoad:(UIWebView *)webView {
[_base performSelector:@selector(injectJavascriptFile)];
}
如果你不想改变三方库呢,可以用runtime的方法,获取这个私有的变量_base
- (void)injectJSBridgeBase {
unsigned int count = 0;
Ivar *members = class_copyIvarList([_bridge class], &count);
for (int i=0; i<count; i++) {
Ivar var = members[i];
const char * memberName = ivar_getName(var);
const char * memberType = ivar_getTypeEncoding(var);
NSString *strMemberType = [NSString stringWithFormat:@"%s", memberType];
if ([strMemberType rangeOfString:@"WebViewJavascriptBridgeBase"].location != NSNotFound) {
_base = object_getIvar(_bridge, var);
break;
}
}
free(members);
}
ok,完美。
UIWebview回调捕获URL处理
通过UIWebview的代理方法处理URL:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
if (webView != _webView) { return YES; }
NSURL *url = [request URL];
__strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;
if ([_base isCorrectProcotocolScheme:url]) { // 判断是不是sheme:wvjbscheme
if ([_base isBridgeLoadedURL:url]) { // 判断是不是:wvjbscheme://__BRIDGE_LOADED__
[_base injectJavascriptFile]; // 注入js
} else if ([_base isQueueMessageURL:url]) { // js注入后,触发新的url重定向 wvjbscheme://__WVJB_QUEUE_MESSAGE__
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]]; // 获取js中的缓存队列数组字符串,清空队列
[_base flushMessageQueue:messageQueueString]; // 执行js中的缓存队列数组,全部执行完
} else {
[_base logUnkownMessage:url]; // sheme:wvjbscheme 下其他的url 直接提示出错
}
return NO; // 只是执行js注入,不会执行加载操作
} else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) { // 如果设置delegate,则调用代理方法
return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
} else { // 什么也不是正常执行
return YES;
}
}
这里对sheme:wvjbscheme的处理主要是三部分:
- 注入JS
- 处理消息
- 其他错误Log提示
对其他的sheme,如果设置delegate:
- (void)setWebViewDelegate:(WVJB_WEBVIEW_DELEGATE_TYPE*)webViewDelegate;
那么执行设置设置代理的方法。如果没有设置,那么WebView将不执行delegate的回调方法。即使你在UIViewController里面设置webView.delegate = self,也是无济于事的。
下面分析一下重要的两个分支。
注入JS方法分支分析
这里通过isBridgeLoadedURL方法是否是注入的url,是的话执行注入JS代码injectJavascriptFile
- (void)injectJavascriptFile {
NSString *js = WebViewJavascriptBridge_js();
[self _evaluateJavascript:js];
if (self.startupMessageQueue) { // 一般是oc开始的注册handler方法
NSArray* queue = self.startupMessageQueue;
self.startupMessageQueue = nil;
for (id queuedMessage in queue) {
[self _dispatchMessage:queuedMessage];
}
}
}
- 这里会获取本地写好的JS代码,然后执行,顺便提一句_evaluateJavascript方法就是封装的stringByEvaluatingJavaScriptFromString。
- 如果有缓存队列self.startupMessageQueue,执行里面全部的消息,一般是OC提前调用- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler方法,提前注册JS调用OC的响应方法。
下面看下执行方法:_dispatchMessage:
- (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];
// 需要保证在主线程中执行js代码
if ([[NSThread currentThread] isMainThread]) {
[self _evaluateJavascript:javascriptCommand];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self _evaluateJavascript:javascriptCommand];
});
}
}
- 首先将字典(WVJBMessage)类型数据转换成String
- 做一些特殊字符的转换
- 调用JS的WebViewJavascriptBridge._handleMessageFromObjC('%@')方法,将消息传进去。
那么我们看一下JS的_handleMessageFromObjC()方法做了些什么
function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}
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);
}
}
}
}
- 首先做Json格式转换
- 判断message是否有responseId,有通过responseId执行本地的回调方法。
- 没有responseId,回调handler方法,将数据,回调方法传回去,本地的callHandler是通过message.handlerName来标示的。
如果有callbackId,就是本地支持回调,那么实现一个匿名函数接受回调。
在触发responseCallback的时候,会有三个参数:
1)handlerName:调用的名称
2)responseId:这里会把callbackId转变成responseId
3)responseData:回调的数据信息
注意,responseId跟callbackId的区别:
- callbackId: 用来标示回调的id,在触发回调的时候会把callbackId变成responseId
- responseId: 用来执行回调的id
最后看下这个doSend方法:
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message['callbackId'] = callbackId;
}
sendMessageQueue.push(message); // 添加message到数组中
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
- 这个方法执行消息的发送,如果有回调方法的话,这里会生成一个callbackId,然后存responseCallbacks中,message中新增callbackId的信息。
- 添加到sendMessageQueue队列中
- 改变iframe的src触发Webview的回调
这里会触发UIWebview的回调,此时的url为:
wvjbscheme://WVJB_QUEUE_MESSAGE
正好走回调的第二个分支,下面分析第二个分支
执行消息分支分析
else if ([_base isQueueMessageURL:url]) { // js注入后,触发新的url重定向 wvjbscheme://__WVJB_QUEUE_MESSAGE__
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]]; // 获取js中的缓存队列数组字符串,清空队列
[_base flushMessageQueue:messageQueueString]; // 执行js中的缓存队列数组,全部执行完
}
下面分两部分解析消息的处理
1) 消息的获取
这里会执行JS代码,通过webViewJavascriptFetchQueyCommand方法获取JS的messageQueueString,也就上上面的sendMessageQueue里面的信息。
-(NSString *)webViewJavascriptFetchQueyCommand {
return @"WebViewJavascriptBridge._fetchQueue();";
}
JS中_fetchQueue()方法:
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
可以看出是直接把sendMessageQueue转换成string。
2) 消息的执行
执行方法flushMessageQueue:
- (void)flushMessageQueue:(NSString *)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) {
if (![message isKindOfClass:[WVJBMessage class]]) {
NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
continue;
}
[self _log:@"RCVD" json:message];
NSString* responseId = message[@"responseId"];
if (responseId) {
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
} else {
WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
if (callbackId) { // 如果接收到js的callbackId,初始化一个block,里面变callbackId为responseId
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[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里面是相似的。
先将消息转换成JSON对象,然后便利所有的消息任务,如果有responseId,执行本地回调,并从self.responseCallbacks中移除执行过的回调方法。如果没有,通过message[@"handlerName"]执行self.messageHandlers中的回调,如果有callbackId,初始化一个responseCallback的block,并传入handler中,接收OC的再回调。
OC直接call JS
WebViewJavascriptBridge方法中提供方法:
typedef void (^WVJBHandler)(id data, WVJBResponseCallback responseCallback);
- (void)callHandler:(NSString*)handlerName;
- (void)callHandler:(NSString*)handlerName data:(id)data;
- (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback;
这里需要提供JS里面的对应的handlerName,然后参数data,最后是响应的回调,提供JS回调的数据,一个responseCallback的Block,可以再回调JS。当然如果你不需要回调,可以置为nil。
方法实现:
- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
[_base sendData:data responseCallback:responseCallback handlerName:handlerName];
}
// WebViewJavascriptBridgeBase中
- (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];
}
调用的是_base的sendData:方法,这里将参数放入字典message中,并把回调block放到self.responseCallbacks字典中。
- data:存储的数据
- callbackId:标示block在self.responseCallbacks中的位置
- handlerName:交互的名字
这样就解决了block回调在JS中不兼容的问题,两端分别实现,用callbackId来标示,用responseId来执行。思路不错。
_queueMessage:执行这条message。
- (void)_queueMessage:(WVJBMessage*)message {
if (self.startupMessageQueue) { // startupMessageQueue:未在webview中注入js之前,缓存之前的交互消息message
[self.startupMessageQueue addObject:message];
} else {
[self _dispatchMessage:message];
}
}
_dispatchMessage: 通过JS调用执行这条message,具体在前面已经解析过了,这里不再赘述。
OC注册方法,等待JS call OC
WebViewJavascriptBridge方法中提供方法:
- (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler;
方法实现:
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}
很简单,只是将hander放到_base(WebViewJavascriptBridgeBase的一个实例对象)的messageHandlers字典中,标示的key为handlerName。
这就结束了,然后等待是JS的调用触发,即:iframe.src设置触发UIWebView的Delegate回调。上面解析注入JS的时候解析过,这里不详细展开。
如果文中有什么错误,欢迎大家指正。
转载请注明出处:http://semyonxu.com