美文网首页
iOS与H5交互: WebViewJavascriptBridg

iOS与H5交互: WebViewJavascriptBridg

作者: uubird | 来源:发表于2019-06-28 01:13 被阅读0次

    由于app发版更新的限制,为了快速上线,很多app会嵌入h5页面,使用h5页面就绕不ios和h5的交互问题。WebViewJavascriptBridge是一个很好的解决方案。

    基本技术实现原理:

    • js向iOS通信:不能直接调用oc的方法,只能通过原生的url拦截实现。
    • iOS向js通信:直接调用系统的evaluateJavaScript方法来执行js代码。

    WebViewJavascriptBridge源码:

    image.png
    关系:
    未命名文件-2.png
    WebViewJavascriptBridge(WKWebViewJavascriptBridge):桥接的入口,针对不同类型的 WebView (UIWebView、WKWebView、WebView)进行分发;执行 JS 代码,实现不同WebView的代理方法,并通过拦截 URL 来通知 WebViewJavascriptBridgeBase 做相应操作
    WebViewJavascriptBridgeBase:用来进行 bridge 初始化和消息处理的核心类;WKWebView出现后独立出来的
    WebViewJavascriptBridge_JS:一堆字符串,用于给js注入,JS 端负责“收发消息”的代码

    具体实现:

    1、初始化

    //初始化,根据传入的参数不同返回不同类型的bridge(UI/WK)
    + (instancetype)bridgeForWebView:(id)webView {
        return [self bridge:webView];
    }
    + (instancetype)bridge:(id)webView {
    #if defined supportsWKWebView
        if ([webView isKindOfClass:[WKWebView class]]) {
            return (WebViewJavascriptBridge*) [WKWebViewJavascriptBridge bridgeForWebView:webView];
        }
    #endif
        if ([webView isKindOfClass:[WVJB_WEBVIEW_TYPE class]]) {
            WebViewJavascriptBridge* bridge = [[self alloc] init];
            [bridge _platformSpecificSetup:webView];
            return bridge;
        }
        [NSException raise:@"BadWebViewType" format:@"Unknown web view type."];
        return nil;
    }
    
    //base初始化
    //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;
    }
    

    js中初始化和注册方法

    //初始化 这段代码的意思就是执行加载WebViewJavascriptBridge_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函数,并且这个函数传入的callback也是一个函数。callback函数中有我们在javascript环境中注册的OC调用JS提供的方法方法。
    setupWebViewJavascriptBridge(function(bridge) {
                                       
           /*JS给ObjC提供的API,在ObjC端可以手动调用JS的这个API。接收ObjC传过来的参数,且可以回调ObjC*/
           bridge.registerHandler('getUserInfo', function(data, responseCallback) {
             showMsg("从OC传过来的参数: ", data)
             responseCallback({'userId': '123456', 'name': 'huiwang227'})
           })
                                       
           document.getElementById('clickBtn').onclick = function (e) {
             bridge.callHandler('getResultObjC', {'toOC': 'xxxxxx'}, function(response) {
                              showMsg('OC的返回值', response)
                              })
           }
         })
    

    2、OC中注册

    [self.bridge registerHandler:@"getResultObjC" handler:^(id data, WVJBResponseCallback responseCallback) {
            NSDictionary *dic = (NSDictionary *)data;
            NSString *msg = [dic objectForKey:@"toOC"];
            NSLog(@"---------toOC--------%@",msg);
            if (responseCallback) {
                // 反馈给JS
                responseCallback(@{@"result": @"oc返回的结果"});
            }
        }];
    //注册一个OC方法OC提供方法给JS调用给javascript调用,并且把他的回调实现保存在messageHandlers中。
    - (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
        _base.messageHandlers[handlerName] = [handler copy];
    }
    

    3、js调用原生:

    //js方法
    bridge.callHandler('getResultObjC', {'toOC': 'xxxxxx'}, function(response) {
                              showMsg('OC的返回值', response)
                              })
    //WebViewJavascriptBridge_js 中代码
    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;
            }
    //message为要传递的业务数据,QUEUE_HAS_MESSAGE为oc中url拦截的标志
            sendMessageQueue.push(message);
            messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
        }
    
    //拦截url oc中的代码  通过[_base isWebViewJavascriptBridgeURL:url]来判断是否是普通的跳转还是webViewjavascriptBridege的跳转。
    //如果是__bridge_loaded__表示是初始化javascript环境的消息,如果是__wvjb_queue_message__则表示是发送javascript消息。
    - (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 isWebViewJavascriptBridgeURL:url]) {
            if ([_base isBridgeLoadedURL:url]) {
                [_base injectJavascriptFile];
            } else if ([_base isQueueMessageURL:url]) {
                NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
                [_base flushMessageQueue:messageQueueString];
            } else {
                [_base logUnkownMessage:url];
            }
            return NO;
        } else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
            return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
        } else {
            return YES;
        }
    }
    
    //通过handler寻找注册过的oc方法并执行
    - (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) {
                    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);
            }
        }
    }
    

    4、原生调js

    [self.bridge callHandler:@"getUserInfo" data:@{@"userID": @"12345"} responseCallback:^(id responseData) {
            NSLog(@"from js: %@", responseData);
        }];
    
    - (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
        [_base sendData:data responseCallback:responseCallback handlerName:handlerName];
    }
    
    - (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];
    }
    
    - (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"];
        //在js中加入WebViewJavascriptBridge方法
        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];
            });
        }
    }
    //WebViewJavascriptBridge_JS中代码 根据handler找到该执行的js
    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);
                    }
                }
            }
        }
    
    

    总结和思考:

    1、bridge分别对(UIWebView、WKWebView、WebView)三种webview进行管理和分发,但是对外界只提供一个方法,把这三个的不同处理隐藏在自己的实现内部。符合设计模式迪米特法则。

    迪米特法则定义:
    一个对象应该对其他对象有最少的了解,通俗的说,就是一个类应该对外暴露尽量少的公共接口,如有必要,可以把对象之间的耦合度降到最低。
    迪米特法则的优点:
    1.一个类暴露的公用接口越少,那么后期修改时涉及的面就越小,由于修改造成的风险也会降到最低。
    2.类之间解耦了,独立性也会相应的提升。那么类的复用率就会大大提高。

    2、无论是js调用原生还是原生调用js,都需要在bridge中预先注册自己的方法,提供给别人调用。所以说每一个js和每一个oc方法都要进行一次注册。真实项目中如果交互很多的话,会产生大量的注册。而且这个注册是强依赖的,注册和调用的地方必须一致。这样的话oc和h5这两个系统紧密耦合在一起,不符合设计模式中要求的低耦合性。

    怎么办呢?
    可以参考我们工程中的做法。只注册一个handler。把具体的方法名当做参数传。对JS参数进行解析,并使用Runtime分发

    //调用的入口
    [self.bridge registerHandler:@"WebViewJavascriptBridgeRun" handler:^(id data, WVJBResponseCallback responseCallback) {
            HDFAppLog(@"$$$$$ Javascript传递数据: %@", data);
            [weakObject p_disposeJSCallWithData:data callBack:responseCallback];
        }];
    
    /**
     对JS参数进行解析,并使用Runtime分发
    
     @param data 参数数据
     @param responseCallback 回调Block
     */
    - (void)p_disposeJSCallWithData:(id)data callBack:(WVJBResponseCallback)responseCallback {
        if (kIsInvalidDict(data)) {  //参数缺失
            if (responseCallback) {
                responseCallback([self hdf_returnMessageWithCode:@"411" message:@"参数缺失" data:nil]);
            }
            return;
        }
        
        NSString *actionName = [NSString stringWithFormat:@"hdf_%@:", [data hdf_safeObjectForKey:@"nativeMethod"]];
    
        //版本不支持
        if (kIsEmptyString(actionName)) {
            if (responseCallback) {
                responseCallback([self hdf_returnMessageWithCode:@"410" message:@"版本不支持" data:nil]);
            }
            return;
        }
        
        NSMutableDictionary *params = [NSMutableDictionary dictionary];
        [params hdf_setSafeObject:[data hdf_safeObjectForKey:@"data"] forKey:@"data"];
        [params hdf_setSafeObject:responseCallback forKey:@"retBlock"];
        
        SEL action = NSSelectorFromString(actionName);
        if ([self respondsToSelector:action])
        {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            [self performSelector:action withObject:params];
            return;
    #pragma clang diagnostic pop
        }
        else  //无响应,跳转到一个公用错误页面/返回nil
        {
            if (responseCallback) {
                responseCallback([self hdf_returnMessageWithCode:@"410" message:nil data:nil]);
            }
            return;
        }
    }
    

    这两个地方技术实现看似完全不一样,但是都实现了对外暴露最少的接口,模块间尽可能解耦。特别是我们的js和原生交互方案使用这么久,非常方便。高峰的设计确实高!

    我们在开发特别是模块化改造抽离过程中也要多多思考,不要着急下手,选取最好的实现方案。

    相关文章

      网友评论

          本文标题:iOS与H5交互: WebViewJavascriptBridg

          本文链接:https://www.haomeiwen.com/subject/catujftx.html