iOS与H5交互: WebViewJavascriptBridge 解析

由于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;
    }
}

这两个地方技术实现看似完全不一样,但是都实现了对外暴露最少的接口,模块间尽可能解耦。

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

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

推荐阅读更多精彩内容