iOS Native和H5交互《WebViewJavascriptBridge》原理

image.png

原生和H5的交互,需要原生webview层面的支持:

  1. 原生UIWebView直接通过stringByEvaluatingJavaScriptFromString
    WKWebview对应evaluateJavaScript:completionHandler:执行JS代码

  2. webview中发出的所用网络请求都能被Native拦截到。通过拦截自定义URL Scheme调用Native方法。
    UIWebView对应的拦截方法:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;

WKWebview对应的拦截方法:

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;

WebViewJavascriptBridge简单介绍

包含的文件:

WebViewJavascriptBridge/WKWebViewJavascriptBridge分别对应UIWebView/WKWebView的接口
WebViewJavascriptBridge_JSJS 的实现
WebViewJavascriptBridgeBasebridge的核心实现

集成方式:

iOS 通过cocoapods集成

pod ‘WebViewJavascriptBridge’

H5中需要粘贴这段代码:

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 = 'https://__bridge_loaded__';
    document.documentElement.appendChild(WVJBIframe);
    setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}

使用:

  1. iOS中如下方式初始化:
[WebViewJavascriptBridge bridgeForWebView:webView];
  1. 简单看一下WebViewJavascriptBridge_JS中的方法:
    WebViewJavascriptBridge_js会在执行后创建一个WebViewJavascriptBridge对象,以及
    var messagingIframe;
    var sendMessageQueue = [];
    var messageHandlers = {};

JS中注册和call方法:

    // register
    function registerHandler(handlerName, handler) {
        messageHandlers[handlerName] = handler;
    }
    // call handle
    function callHandler(handlerName, data, responseCallback) {
        if (arguments.length == 2 && typeof data == 'function') {
            responseCallback = data;
            data = null;
        }
        _doSend({ handlerName:handlerName, data:data }, responseCallback);
    }
    // _doSend
    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;
    }
    

Native->js:

native调JS,需要JS先注册对应的方法;

  function registerHandler(handlerName, handler) {
      messageHandlers[handlerName] = handler;
  }

native通过callHandler:data:responseCallback,内部是实现的sendData:responseCallback:handlerName:

  • 封装一个message字典,用于传递给JS
  • 判断是否有responseCallback,如果有就产生callbackId,并且保存responseCallback到responseCallbacks中
  • 保存callbackId到message中,调用_queueMessage
- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {

  //封装一个message字典
  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];
}

_queueMessage:会根据startupMessageQueue是否为nil判断,如果不空表示webview还未加在完成,进而保存到startupMessageQueue中,等到webview中

    - (void)_queueMessage:(WVJBMessage*)message {
        if (self.startupMessageQueue) {
            [self.startupMessageQueue addObject:message];
        } else {
            [self _dispatchMessage:message];
        }
    }

webview加载完会执行(WKWebview)decidePolicyForNavigationAction:decisionHandler,走isBridgeLoadedURL分支,
执行injectJavascriptFile,执行JS的初始化代码

// 相关宏定义
#define kOldProtocolScheme @"wvjbscheme"
#define kNewProtocolScheme @"https"
#define kQueueHasMessage   @"__wvjb_queue_message__"
#define kBridgeLoaded      @"__bridge_loaded__

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    if (webView != _webView) { return; }
    NSURL *url = navigationAction.request.URL;
    __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;

    if ([_base isWebViewJavascriptBridgeURL:url]) {
    
        if ([_base isBridgeLoadedURL:url]) {
            [_base injectJavascriptFile];
        } else if ([_base isQueueMessageURL:url]) {
            [self WKFlushMessageQueue];
        } else {
            [_base logUnkownMessage:url];
        }
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    // ...
}

_dispatchMessage:最终通过_evaluateJavascript执行JS代码

  • message转成json字符串
  • 最终调用webview具体的执行JS方法
- (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];
    if ([[NSThread currentThread] isMainThread]) {
        [self _evaluateJavascript:javascriptCommand];

    } else {
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self _evaluateJavascript:javascriptCommand];
        });
    }
}

webView中的WebViewJavascriptBridge._handleMessageFromObjC,Native调用JS的核心方法。简单看下:

  • 解析Native传来的字符串,字符串转对象
  • 如果responseId存在,为js调用Native的回调,执行并且结束流程
  • callbackId不为空,则说明Native有回调,创建responseCallback,保存callbackId到responseCallback中
  • 根据handlerName从messageHandlers中取出对应的方法,然后执行
  • responseCallback最后通过_doSend回传callbackId和参数
    function _dispatchMessageFromObjC(messageJSON) {
        if (dispatchMessagesWithTimeoutSafety) {
            setTimeout(_doDispatchMessageFromObjC);
        } else {
             _doDispatchMessageFromObjC();
        }
        
        // 解析messageJSON,json字符串转对象
        function _doDispatchMessageFromObjC() {
            var message = JSON.parse(messageJSON);
            var messageHandler;
            var responseCallback;
            //responseId 存在,js调用Native的回调,执行并且结束
            if (message.responseId) {
                responseCallback = responseCallbacks[message.responseId];
                if (!responseCallback) {
                    return;
                }
                responseCallback(message.responseData);
                delete responseCallbacks[message.responseId];
            } else {
            // callbackId是Native带过来的,如果存在则创建responseCallback
                if (message.callbackId) {
                
                //responseCallback 用作回调,并回传callbackId到Native
                    var callbackResponseId = message.callbackId;
                    responseCallback = function(responseData) {
                        _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
                    };
                }
                
                // 从messageHandlers中取出对应的方法,然后执行
                var handler = messageHandlers[message.handlerName];
                if (!handler) {
                    console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
                } else {
                    handler(message.data, responseCallback);
                }
            }
        }
    }
    

_doSend会通过iframe发送request到Native,Native根据callbackId取出最初保存在messageHandlers中的handle并执行,整个过程执行完成。

JS->native

js调用native,原生需要先注册相应的方法(注册实际上是保存起来)

- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
    _base.messageHandlers[handlerName] = [handler copy];
}

js通过callHandler调用原生,进而执行_doSend:

  • 判断是否有回调responseCallback,如果有产生callbackId
  • 保存responseCallback到responseCallbacks中
  • 保存callbackId到sendMessageQueue队列中
  • 通过messagingIframe发起request,scheme包含wvjb_queue_message
    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 = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    }
    

request会被原生的decidePolicyForNavigationAction拦截(WKWebview),这次会走isQueueMessageURL为true的情况。
然后执行WKFlushMessageQueue

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    if (webView != _webView) { return; }
    NSURL *url = navigationAction.request.URL;
    __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;

    if ([_base isWebViewJavascriptBridgeURL:url]) {
        if ([_base isBridgeLoadedURL:url]) {
            [_base injectJavascriptFile];
        } else if ([_base isQueueMessageURL:url]) {
            [self WKFlushMessageQueue];
        } else {
            [_base logUnkownMessage:url];
        }
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    // ... 
}

WKFlushMessageQueue会执行一段JS代码:”WebViewJavascriptBridge._fetchQueue()“,看一眼js中的_fetchQueue方法:
该方法返回sendMessageQueue中的内容,并且清空队列;

    function _fetchQueue() {
        var messageQueueString = JSON.stringify(sendMessageQueue);
        sendMessageQueue = [];
        return messageQueueString;
    }

原生这里,evaluateJavaScript获取到sendMessageQueue中的内容,紧接着执行flushMessageQueue,这是最核心的方法了

- (void)WKFlushMessageQueue {
    [_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];
    }];
}

flushMessageQueue简化后如下:

  • json转字典,遍历每个message对象
  • 如果responseId存在,从_responseCallbacks中找出对应的responseCallback并执行,然后结束
  • 如果responseId不存在,则message就是js callhandle原生方法。根据js传递来的callbackId来决定是否创建responseCallback,带上callbackId。
  • 根据js传递的handlerName从原生messageHandlers中取出相应的方法handler
  • 执行handler,handler的具体实现中会执行创建的responseCallback
    最后,responseCallback会在JS中被执行,JS会根据最初创建的callbackId,在_dispatchMessageFromObjC中完成最后的处理,整个JS->Native过程结束
- (void)flushMessageQueue:(NSString *)messageQueueString{
    // ... 此处省略判断的代码
    // json字符串转数组
    id messages = [self _deserializeMessageJSON:messageQueueString];
    // 遍历每个message,从中判断responseId
    for (WVJBMessage* message in messages) {
        NSString* responseId = message[@"responseId"];
        // 如果有responseId,为Native->JS 的回调,执行responseCallback后结束
        if (responseId) {
            WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
            responseCallback(message[@"responseData"]);
            [self.responseCallbacks removeObjectForKey:responseId];
        } else {
            // 如果无responseId,才是JS->Native的流程
            //如果callbackId存在则需要回调JS,创建responseCallback,并在responseCallback中传递最初创建的callbackId,以及其他参数responseData
            // 最后到_queueMessage中完成回调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 };
                    [self _queueMessage:msg];
                };
            } else {
                responseCallback = ^(id ignoreResponseData) {
                    // Do nothing
                };
            }
            
            // 根据message中handlerName,从Native的messageHandlers取出方法并执行
            
            WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
            
            if (!handler) {
                NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
                continue;
            }
            // responseCallback会在具体handler中执行
            handler(message[@"data"], responseCallback);
        }
    }
}

最后附上源码地址:WebViewJavascriptBridge

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容