Native接H5框架WKWebViewJavascriptBridge原理总结

篇头语

  • 本篇参考文章在章尾,博客写的很不错,但是需要反复详读加上自己在头脑中绘制逻辑结构才能比较好的理解,不然“东一块、西一块”很难联系到一起,所以我结合了较多部分上述文章以外单独又重新总结了一遍,加上了一些自己的理解和细节上的处理,也做了一些方便理解的结构图,方便自己“一步到位”的复习,也方便其他人理解。
  • 至于为什么要选择这个框架进行解析呢,是因为目前H5和Native的连接脱落不了“对于网址跳转拦截”这一个过程,此框架使用广泛,写法成熟,研读此框架不仅是为了了解这个框架,更多意义上是为什么理解Native接H5的原理,也方便了解各个框架对这方面的处理的差异化和优势体现在哪里

基本构成

对于整个框架来说分为三个部分

框架包含的文件
  • oc部分,包括oc处理暴露给js接口的类:WKWebViewJavascriptBridge.m/.h
  • js部分,包括js处理暴露给oc接口的文件: ExampleApp.html
  • bridge处理部分,这个部分对于oc和js都各有一个文件,oc是类:WebViewJavascriptBridgeBase.m/.h,js则是:WebViewJavascriptBridge_JS.m,虽然是.m文件,但是其中是js代码,点开就知道了。
  • oc和js部分的作用就是声明给对方调用的方法,以及提供供自身使用可以调用对方的一个接口,但是具体如何调用的js、如果调用的oc或者说如何注册给js、如何注册给oc使用的方法,这些逻辑都放在两端的bridge部分进行处理。

H5端的处理过程

H5端初始化流程

处理过程其实是指对于bridge的处理过程,oc端的处理都在Base.m中,流程和js的初始化原理大致相同,但是js端的过程比较繁琐,因为初始化过程直接就来了一个和oc的交互。为什么会有这么一个交互过程呢?因为对于js的初始化代码被存放到了移动端的文件中,这一点应该是出于尽可能的减少双端工作量的考量吧,这样存放虽然加大了js端初始化的复杂性,但是这对于开发者是黑盒的,所以也不能说是一个缺点。这个处理过程是一个在js端初始化一个bridge并达到通过这个bridge可以跟oc交互的目的的过程。先看一下植入WKWebViewJavascriptBridge框架的过程中,在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)
      }
    
      /*与OC交互的所有JS方法都要放在此处注册,才能调用通过JS调用OC或者让OC调用这里的JS*/
      setupWebViewJavascriptBridge(function(bridge) {
                                   
                                   
       var uniqueId = 1
       function log(message, data) {
         var log = document.getElementById('log')
         var el = document.createElement('div')
         el.className = 'logLine'
         el.innerHTML = uniqueId++ + '. ' + message + ':<br/>' + JSON.stringify(data)
         if (log.children.length) {
            log.insertBefore(el, log.children[0])
         } else {
           log.appendChild(el)
         }
       }
       //register、call代码
       bridge.registerHandler('getUserInfos', function(data, responseCallback) {
         log("这是在H5中的getUserInfos 接收的从ObjC传过来的参数", data)
         responseCallback({'userId': '123456', 'blog': '标哥的技术博客'})
       })
                                   
       /*JS给ObjC提供公开的API,ObjC端通过注册,就可以在JS端调用此API时,得到回调。ObjC端可以在处理完成后,反馈给JS,这样写就是在载入页面完成时就先调用*/
       bridge.callHandler('getUserIdFromObjC', function(responseData) {
         log("这是在H5中的getUserIdFromObjC方法对应的参数responseData的值", responseData)
       })
       })

下面是对以上代码进行一个简单的简化,解耦出一个方法callback,但是可以方便理解。所以主要看下面的代码即可

function setupWebViewJavascriptBridge(callback) {
     //第一次调用这个方法的时候,为false
    if (window.WebViewJavascriptBridge) {
        var result = callback(WebViewJavascriptBridge);
        return result;
    }
    //第一次调用的时候,也是false
    if (window.WVJBCallbacks) {
        var result = window.WVJBCallbacks.push(callback);
        return result;
    }
    //把callback对象赋值给对象。
    window.WVJBCallbacks = [callback];
    //这段代码的意思就是执行加载WebViewJavascriptBridge_JS.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执行的时候传入的参数,这是一个方法。
function callback(bridge) {
    var uniqueId = 1
    //把WEB中要注册的方法注册到bridge里面
    bridge.registerHandler('OC调用JS提供的方法', function(data, responseCallback) {
        log('OC调用JS方法成功', data)
        var responseData = { 'JS给OC调用的回调':'回调值!' }
        log('OC调用JS的返回值', responseData)
        responseCallback(responseData)
    })
};
//驱动所有hander的初始化
setupWebViewJavascriptBridge(callback);
  • 这里直接调用了setupWebViewJavascriptBridge这个方法,但是实际上调用这个方法前两个语句都不会执行。因为没有初始化赋值的情况下,window的属性都是空的,但是其他的语句是会被执行的,就是下面的代码。
  • 单看这两个方法setupWebViewJavascriptBridge以及callback来说的话,callback的参数bridge一直没有被赋值,不过无所谓,在第一次调用的时候callback方法作为setupWebViewJavascriptBridge方法的参数,也没有被调用,所以此时参数为空也无所谓。
  • 下面这段代码的主要目的如下面注释所说,但是细节上也需要了解一下,(�js实在是没什么基础,看起来比较吃力)代码实际上创建了一个iframe,这个是一个web中再打开一个web页面的这么一个组件,把这个组件加到页面又移走,设置为不可见,然后这个iframe指向了一个地址为“https://bridge_loaded”的东西,其实是为了制造webview的url跳转,从而触发oc的回调方法。
    _iframe可以理解为webview中的窗口,当我们改变iframe的src属性的时候,相当于我们浏览器实现了链接的跳转。比如从www.baidu.com跳转到www.google.com。下面这段代码的目的就是实现一个到https://bridge_loaded的跳转。触发oc回调的目的是因为为js端初始化bridge的代码需要从oc端的回调方法中触发,从而达到初始化javascript环境的bridge的作用。
//这段代码的意思就是执行加载WebViewJavascriptBridge_JS.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);

为js端初始化bridge的代码是一个js文件,这个文件实现了为上述代码中的callback方法中的bridge赋值。通过上述代码在oc端被执行在当前webview,至于怎么通过这个“跳转”触发js文件的执行的呢?这就取决于oc这边的方法回调了,实现了WKNavigationDelegate协议的类会在内部WKWebView发生地址跳转的时候触发以下这个方法:

- (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;
    //如果是WebViewJavascriptBridge发送或者接受的消息,则特殊处理。否则按照正常流程处理。
    if ([_base isWebViewJavascriptBridgeURL:url]) {
        //第一次注入JS代码,也就是说只有在初始化的时候才会被调用一次
        //是通过判断url的scheme和host字段是不是预置的特殊含义的字段
        if ([_base isBridgeLoadedURL:url]) {
            [_base injectJavascriptFile];
        //此处判断出消息是从WEB发过来的,所有从web发来的消息都走此口,无论是“js调用oc的方法”消息,还是“oc调用js方法,js执行完毕发回来的消息(目的是告诉oc我执行完了,你可以执行你设置的回调函数了!)”
        } else if ([_base isQueueMessageURL:url]) {
            [self WKFlushMessageQueue];
        } else {
            [_base logUnkownMessage:url];
        }
        decisionHandler(WKNavigationActionPolicyCancel);
    }
    //下面是webview的正常代理执行流程,不用管。
    if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
        [_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
    } else {
        decisionHandler(WKNavigationActionPolicyAllow);
    }
}

注意上面的代码,很多是调用base的方法执行,这是解耦思想呦!和bridge相关的具体操作细节要移步到BridgeBase类实现。下面看一下[base injectJavascriptFile];这个方法的作用就是把WebViewJavascriptBridge_JS.js中的方法注入到webview中并且执行,从而达到初始化javascript环境的brige的作用。至于其他部分用到的时候再看。

//初始化的是否注入WebViewJavascriptBridge_JS.js
- (void)injectJavascriptFile {
    NSString *js = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"WebViewJavascriptBridge_JS.js" ofType:nil] encoding:NSUTF8StringEncoding error:nil];
    //把javascript代码注入webview中执行,这里执行具体的注入操作。
    [self _evaluateJavascript:js];
    //如果javascript环境初始化完成以后,有startupMessageQueue消息。则立即发送消息。
    //startupMessageQueue置为nil,不reset情况下将不会在被赋值
    if (self.startupMessageQueue) {
        NSArray* queue = self.startupMessageQueue;
        self.startupMessageQueue = nil;
        for (id queuedMessage in queue) {
            [self _dispatchMessage:queuedMessage];
        }
    }
}
//把javascript代码写入webview
- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand {
    [_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
    return NULL;
}

上面代码注释已经很清晰了,其实就是拼接一条js指令然后通过oc提供的方式把他丢到webview中去执行,然后看一下要执行的这个文件,其实在下面的这段代码目的执行的js文件对于bridge进行了赋值,下面来看一下这个WebViewJavascriptBridgeBase_JS.js文件,也就是js端的bridge文件

;(function() {
    //如果已经初始化了,则返回。
    if (window.WebViewJavascriptBridge) {
        return;
    }
    if (!window.onerror) {
        window.onerror = function(msg, url, line) {
            console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line);
        }
    }
    //初始化一些属性。
    var messagingIframe;
    //用于存储消息列表
    var sendMessageQueue = [];
    //用于存储消息
    var messageHandlers = {};
    //通过下面两个协议组合来确定是否是特定的消息,然后拦击。
    var CUSTOM_PROTOCOL_SCHEME = 'https';
    var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
    //oc调用js的回调
    var responseCallbacks = {};
    //消息对应的id
    var uniqueId = 1;
    //是否设置消息超时
    var dispatchMessagesWithTimeoutSafety = true;
    //web端注册一个消息方法
    function registerHandler(handlerName, handler) {
        messageHandlers[handlerName] = handler;
    }
    //web端调用一个OC注册的消息
    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;
    }
        //把消息转换成JSON字符串返回
    function _fetchQueue() {
        var messageQueueString = JSON.stringify(sendMessageQueue);
        sendMessageQueue = [];
        return messageQueueString;
    }
    //OC调用JS的入口方法
    function _handleMessageFromObjC(messageJSON) {
        _dispatchMessageFromObjC(messageJSON);
    }

    //初始化桥接对象,OC可以通过WebViewJavascriptBridge来调用JS里面的各种方法。
    window.WebViewJavascriptBridge = {
        registerHandler: registerHandler,
        callHandler: callHandler,
        disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
        _fetchQueue: _fetchQueue,
        _handleMessageFromObjC: _handleMessageFromObjC
    };


    //处理从OC返回的消息。
    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 });
                    };
                }
                //获取JS注册的函数
                var handler = messageHandlers[message.handlerName];
                if (!handler) {
                    console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
                } else {
                    //调用JS中的对应函数处理
                    handler(message.data, responseCallback);
                }
            }
        }
    }
    //把消息从JS发送到OC,执行具体的发送操作。
    function _doSend(message, responseCallback) {
        if (responseCallback) {
            var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
            //存储消息的回调ID
            responseCallbacks[callbackId] = responseCallback;
            //把消息对应的回调ID和消息一起发送,以供消息返回以后使用。
            message['callbackId'] = callbackId;
        }
        //把消息放入消息列表
        sendMessageQueue.push(message);
        //下面这句话会出发JS对OC的调用
        //让webview执行跳转操作,从而可以在
        //webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 中拦截到JS发给OC的消息
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    }


    messagingIframe = document.createElement('iframe');
    messagingIframe.style.display = 'none';
    //messagingIframe.body.style.backgroundColor="#0000ff";
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    document.documentElement.appendChild(messagingIframe);


    //注册_disableJavascriptAlertBoxSafetyTimeout方法,让OC可以关闭回调超时,默认是开启的。
    registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);
    //执行_callWVJBCallbacks方法
    setTimeout(_callWVJBCallbacks, 0);

    //初始化WEB中注册的方法。这个方法会把WEB中的hander注册到bridge中。
    //下面的代码其实就是执行WEB中的callback函数。
    function _callWVJBCallbacks() {
        var callbacks = window.WVJBCallbacks;
        delete window.WVJBCallbacks;
        for (var i = 0; i < callbacks.length; i++) {
            callbacks[i](WebViewJavascriptBridge);
        }
    }
})();

上面代码比较复杂,但是却比较容易看,主要分为以下几个功能:

  • 三个属性:
    • sendMessageQueue 用于存储消息列表
    • messageHandlers 用于存储注册给oc调用的方法
    • responseCallbacks 用于存储oc在调用js方法时需要调用的js提供的回调方法
  • 实现了registerHandler方法,原理是把方法名和方法实现存储在messageHandlers队列中
  • 实现了callHandler方法,原理是生成一个callback的id,和callback的实现作为对用的key、value一同存储在responseCallbacks队列中,与此同时,把方法名和参数和callback的id存储在一个字典中,然后吧这个字典加入到sendMessageQueue队列中,最后变化网址,形成跳转,触发oc回调方法,实现一个信息交流,至于是怎么把参数和方法名称带过去的呢?
  • disableJavscriptAlertBoxSafetyTimeout等相关的一些列作用暂时没研究?
  • 实现了接收oc消息的方法,原理是解析收到的json值,从中解析出需要的回调函数的信息,然后触发回调函数,之后从回调函数的队列responseCallbacks中删除该回调方法,如果说oc返回的信息中没有这个回调方法,是不是说oc端没有主动回调这个js的回调?则将会通过json数据获取到之前存储的message的callbackid来查找对应的回调函数来进行调用,之后的步骤和callHandler同理,也就是说这个回调函数是一定会被调用的?
  • 通过同样的方式制造跳转,目的是把初始化好之后把js要发给oc的消息一下发出去
  • 立即执行的函数,把WebViewJavascriptBridge注入到之前文件的callbacks方法中作为参数,实现register方法的真正触发,(扣题)

oc环境初始化

在bridge.m中其实工作很简单,他只是一个对于oc项目连接H5的入口,提供一个单例,搞定自己的webview和base就好了。

//初始化一个OC环境的桥WKWebViewJavascriptBridge并且初始化。
+ (instancetype)bridgeForWebView:(WKWebView*)webView {
    WKWebViewJavascriptBridge* bridge = [[self alloc] init];
    //调用下面那个方法
    [bridge _setupInstance:webView];
    [bridge reset];
    return bridge;
}
//初始化
- (void) _setupInstance:(WKWebView*)webView {
    _webView = webView;
    _webView.navigationDelegate = self;
    _base = [[WebViewJavascriptBridgeBase alloc] init];
    _base.delegate = self;
}

剩余的初始化工作在BridgeBase.m

//messageHandlers用于保存OC环境注册的方法,key是方法名,value是这个方法对应的回调block
//startupMessageQueue用于保存是实话过程中需要发送给javascirpt环境的消息。
//responseCallbacks用于保存OC于javascript环境相互调用的回调模块。通过_uniqueId加上时间戳来确定每个调用的回调。
- (id)init {
    if (self = [super init]) {
        self.messageHandlers = [NSMutableDictionary dictionary];
        self.startupMessageQueue = [NSMutableArray array];
        self.responseCallbacks = [NSMutableDictionary dictionary];
        _uniqueId = 0;
    }
    return self;
}

所有与javascript之间交互的信息都存储在messageHandlers和responseCallbacks中。这两个属性记录了OC环境与javascript交互的信息。

OC发消息给WEB的过程梳理

oc向web发消息流程梳理图

对于oc调用js的方法,走下面这个流程:

- (void)callHandler:(id)sender {
    id data = @{ @"OC调用JS方法": @"OC调用JS方法的参数" };
    [_bridge callHandler:@"OC调用JS提供的方法" data:data responseCallback:^(id response) {
       // NSLog(@"testJavascriptHandler responded: %@", response);
    }];
}
/*
    handerName:OC调用JS提供的方法
    data:{@"OC调用JS方法的参数":@"OC调用JS方法"}
    responseCallback:回调block
 */
- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
    [_base sendData:data responseCallback:responseCallback handlerName:handlerName];
}

把所有信息存入一个名字为message的字典中。里面拼装好参数data、回调IDcallbackId(现场生成)、要调用的js方法名字handlerName。于此同时,把刚刚生成的callbackID和callback方法存入自身的callback队列中,目的是js执行回来的时候再调用,具体如下,其实整个过程处理和js的bridge文件的处理方式一样,

- (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;
    }
    //注意是要调用的js方法的名字
    if (handlerName) {
        message[@"handlerName"] = handlerName;
    }
    [self _queueMessage:message];
}

其次把封装好的信息[参数data、回调IDcallbackId(现场生成)、要调用的js方法名字handlerName
]发送出去,把OC消息序列化、并且转化为javascript环境的格式。然后在主线程中调用_evaluateJavascript。

- (void)_queueMessage:(WVJBMessage *)message {
    //啥时候有啥时候没有??这里好像是只会存储js初始化文件执行之前的消息,由于js未初始化,所以消息都加到队列中,初始化之后以后一次集体发送,但是初始化之后队列被置为nil,将不会再走if,都是有消息直接就发送出去
    if (self.startupMessageQueue) {
     //加完了就完事了吗??
        [self.startupMessageQueue addObject:message];
    } else {
        [self _dispatchMessage:message];
    }
}

- (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];
        });
    }
}

- (NSString *)_serializeMessage:(id)message pretty:(BOOL)pretty {
    return [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:message options:(NSJSONWritingOptions)(pretty ? NSJSONWritingPrettyPrinted : 0) error:nil] encoding:NSUTF8StringEncoding];
}

整个流程对消息进行了一系列处理,最后的处理通过一句代码进行发送

NSString *javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];

其实最终形成的字符串是

WebViewJavascriptBridge._handleMessageFromObjC('{\"callbackId\":\"objc_cb_1\",\"data\":{\"OC调用JS方法\":\"OC调用JS方法的参数\"},\"handlerName\":\"OC调用JS提供的方法\"}');

其实就是通过javascript环境中的Bridge对象的_handleMessageFromObjC方法。下面我们去WebViewJavascriptBridge_JS.js中看_handleMessageFromObjC的处理过程。

//OC调用JS的入口方法
//messsgeJSON:{\"callbackId\":\"objc_cb_1\",\"data\":{\"OC调用JS方法\":\"OC调用JS方法的参数\"},\"handlerName\":\"OC调用JS提供的方法\"}
    function _handleMessageFromObjC(messageJSON) {
        _dispatchMessageFromObjC(messageJSON);
    }
    //处理从OC返回的消息。
function _dispatchMessageFromObjC(messageJSON) {
    if (dispatchMessagesWithTimeoutSafety) {
        setTimeout(_doDispatchMessageFromObjC);
    } else {
        _doDispatchMessageFromObjC();
    }

    function _doDispatchMessageFromObjC() {
        var message = JSON.parse(messageJSON);
        var messageHandler;
        var responseCallback;
        //很明显在此过程中responseId)是没有的,所以跳过,message有三个key:data、callbackId、handleName
        //这一部分是oc执行完js的调用返回信息调用js回调的逻辑
        if (message.responseId) {
            responseCallback = responseCallbacks[message.responseId];
            if (!responseCallback) {
                return;
            }
            responseCallback(message.responseData);
            delete responseCallbacks[message.responseId];
        } else {
        //这一部分是处理oc调用js方法的逻辑
        //这里为什么要获取oc的callbackID是因为要在执行完之后给oc发送消息时附带
            if (message.callbackId) {
                var callbackResponseId = message.callbackId;
                //准备好方法结束后执行的“通知oc可以执行回调函数”的方法
                responseCallback = function(responseData) {
                   _doSend({ handlerName: message.handlerName, responseId: callbackResponseId, responseData: responseData });
                };
            }
            //获取JS注册的函数
            var handler = messageHandlers[message.handlerName];
            if (!handler) {
                console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
            } else {
                //调用JS中的对应函数处理,附带上刚刚拼接好的回调函数
                handler(message.data, responseCallback);
            }
        }
    }
}

上面这段代码很容易理解,而且我已经把注释写的非常清晰了,其实就是通过判断消息中是否有responseId来判断这是一个oc的回调消息,还是说oc想要调用js的方法。js处理完方法调用之后直接调用_doSend方法把信息返回OC。下面我们看看_doSend的具体实现:

//把消息从JS发送到OC,执行具体的发送操作。
function _doSend(message, responseCallback) {
//在这一步骤中if语句是不执行的
    if (responseCallback) {
        var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
        //存储消息的回调ID
        responseCallbacks[callbackId] = responseCallback;
        //把消息对应的回调ID和消息一起发送,以供消息返回以后使用。
        message['callbackId'] = callbackId;
    }
    //把消息放入消息列表
    sendMessageQueue.push(message);
    //下面这句话会出发JS对OC的调用
    //让webview执行跳转操作,从而可以在
    //webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 中拦截到JS发给OC的消息
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

在这一过程中,并没有向_doSend传入responseCallback参数,因为这是一个“js执行完方法,要发送消息给oc”的过程,而不是要发送“调用oc方法的消息”,所以不需要第二个参数。也是就在if语句不执行的情况下,直接通过页面跳转把消息发走,但是之前封装的message并没有随着这个网址一起发走,它被加入到了自身的sendMessageQueue队列中,等oc收到消息后会自己来取。
其中最重要还是最后面的通过改变iframe的messagingIframe.src。从而触发webview的代理方法webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler从而在OC中处理javascript环境触发过来的回调。具体如下:

if ([_base isWebViewJavascriptBridgeURL:url]) {
    //第一次注入JS代码
    if ([_base isBridgeLoadedURL:url]) {
        [_base injectJavascriptFile];
    //处理WEB发过来的消息
    } else if ([_base isQueueMessageURL:url]) {
        [self WKFlushMessageQueue];
    } else {
        [_base logUnkownMessage:url];
    }
    decisionHandler(WKNavigationActionPolicyCancel);
}

这里会走[self WKFlushMessageQueue];方法。然后通过调用WebViewJavascriptBridge._fetchQueue()来获取javascript给OC的回调信息。

//获取WEB消息的JSON字符串
- (NSString *)webViewJavascriptFetchQueyCommand {
    return @"WebViewJavascriptBridge._fetchQueue();";
}
////把消息或者WEB回调从OC发送到OC
- (void)WKFlushMessageQueue {
    NSString *js = [_base webViewJavascriptFetchQueyCommand];
    [_webView evaluateJavaScript:js completionHandler:^(NSString* result, NSError* error) {
        if (error != nil) {
            NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
        }
        //把消息或者WEB回调从OC发送到OC
        [_base flushMessageQueue:result];
    }];
}

获取到javascript给OC的回调消息以后,然后把javascript的bridge返回的信息加工处理成OC环境的bridge能识别的信息。从而找到具体的实现执行。

//把从WEB发送的消息返回。然后在这里处理
- (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;
    }
   //此方法在上面有贴,用于解析json数据
    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) {
                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);
        }
    }
}

其实就是依然是通过判断消息中是否有responseId来判断这是一个“js给oc的回调消息”,还是说这是“js想调用oc的消息”。注意此时拿到的message是上一步在js中封装的,其中包括data,callbackId和handleName

WEB发消息给OC

基于框架的作用,无论在oc端调用H5或者是在H5端调用oc都仅需含简单的几行代码

bridge.callHandler('OC提供方法给JS调用',params, function(response) {
    log('JS调用OC的返回值', response)
})

此处调用的事BridgeBase.js中的方法

//web端调用一个OC注册的消息
function callHandler(handlerName, data, responseCallback) {
    if (arguments.length == 2 && typeof data == 'function') {
        responseCallback = data;
        data = null;
    }
    _doSend({ handlerName: handlerName, data: data }, responseCallback);
}
//把消息从JS发送到OC,执行具体的发送操作。
function _doSend(message, responseCallback) {
    if (responseCallback) {
        var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
        //存储消息的回调ID
        responseCallbacks[callbackId] = responseCallback;
        //把消息对应的回调ID和消息一起发送,以供消息返回以后使用。
        message['callbackId'] = callbackId;
    }
    //把消息放入消息列表
    sendMessageQueue.push(message);
    //下面这句话会出发JS对OC的调用
    //让webview执行跳转操作,从而可以在
    //webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 中拦截到JS发给OC的消息
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

其实剩下的过程就很类似了,就不多说了

参考文章

https://juejin.im/entry/6844903472718938126

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