WebViewJavascriptBridge的使用和实现原理

在HTML添加交互代码

<!-- script 嵌入JS代码 -->
 window.onerror = function(err) {
   log('window.onerror: ' + err)
 }
 
 /*这段代码是固定的,必须要放到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';
   //src是js交互的标识码
   WVJBIframe.src = 'kadios://__KAD_BRIDGE_LOADED__';
   document.documentElement.appendChild(WVJBIframe);
   //加载这个方法后就删除自定义的src 让后面重定向url
   //把iframe一起干掉,既然改变src不会刷新页面,重新创建一个iframe 就会刷新
   setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
 }

 /*与OC交互的所有JS方法都要放在此处注册,才能调用通过JS调用OC或者让OC调用这里的JS*/
 setupWebViewJavascriptBridge(function(bridge) {
   //JS 调用 OC方法 callHandler
   document.getElementById("showButton").onclick = function(e){
       bridge.callHandler('goHome:', {'name': 'lemon' + Math.random()}, function(response) {

       })
   }
     
   /*JS给ObjC提供公开的API,在ObjC端可以手动调用JS的这个API。接收ObjC传过来的参数,且可以回调ObjC*/
   //OC 调用JS registerHandler
   bridge.registerHandler('getUserInfos', function(data, responseCallback) {
     document.getElementById("changeTitle").innerHTML = data;
     //回调给ObjC
     responseCallback({'userId': '123456', 'title': 'Hello World!'})
   })
                              
 })  
     

app中使用WebViewJavascriptBridge的代码

//创建WebViewJavascriptBridge做为属性
@property (nonatomic, strong) WebViewJavascriptBridge *bridge;
//给webView建立JS与OC桥梁
self.bridge = [WebViewJavascriptBridge bridgeForWebView:self.webView];
//设置代理
[self.bridge setWebViewDelegate:self ];

// 1.JS主动调用OjbC的方法
// 这是JS会调用getUserIdFromObjC方法,这是OC注册给JS调用的
// JS需要回调,当然JS也可以传参数过来。data就是JS所传的参数,不一定需要传
// OC端通过responseCallback回调JS端,JS就可以得到所需要的数据
    
[self.bridge registerHandler:@"goHome:" handler:^(id data, WVJBResponseCallback responseCallback) {
   self.showDataLB.text = [NSString stringWithFormat:@"%@",data];
   [self.showDataLB sizeToFit];
   NSLog(@"goHome:%@",data);
}];


-(void) clickShowButton{
    //OC调用了js方法
    [self.bridge callHandler:@"getUserInfos" data:@"我点击了showButton" responseCallback:^(id responseData) {
        //data是js的回调数据
        NSLog(@"%@",responseData);
    }];

}

上面的代码是WebViewJavascriptBridge的基本使用。

下面是关于WebViewJavascriptBridge的原理

WebViewJavascriptBridge类的作用是绑定webView,在该类中处理WebView的代理。

js调用OC的原理

+ (instancetype)bridgeForWebView:(WVJB_WEBVIEW_TYPE*)webView {
    WebViewJavascriptBridge* bridge = [[self alloc] init];
    [bridge _platformSpecificSetup:webView];
    return bridge;
}

- (void) _platformSpecificSetup:(WVJB_WEBVIEW_TYPE*)webView {
    _webView = webView;
    _webView.delegate = self;
    _base = [[WebViewJavascriptBridgeBase alloc] init];
    //base实例会把该注册事件放进到base的一个消息池子(负责接受多个OC注册事件)中,方便后续处理
    _base.delegate = self;
}

webView

//每次在重新定向URL的时候,这个方法就会被触发,通常情况,我们会在这里做一些拦截完成js和本地的间接交互什么的
- (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;
    //拦截URL看是不是 判断是否是自定义的路径
    //在html中我们设置了 (WVJBIframe.src = 'kadios://__KAD_BRIDGE_LOADED__';)
    //isCorrectProcotocolScheme 方法判断 scheme 是否等于 kadios
    //isBridgeLoadedURL 方法 判断 host 是否等于 __KAD_BRIDGE_LOADED__
    if ([_base isCorrectProcotocolScheme:url]) {
        if ([_base isBridgeLoadedURL:url]) {
            //oc 调用 js
            //此时执行在工程里面放置的JS文件
            [_base injectJavascriptFile];
            
            //由于js函数中一进来便主动触发了registerHandler,所以url变成了 kadios://__KAD_QUEUE_MESSAGE__
            //文件执行完毕 然后走下面else if
        } else if ([_base isQueueMessageURL:url]) {
            //url所以变成了 kadios://__KAD_QUEUE_MESSAGE__
            // evaluateJavascript:@"WebViewJavascriptBridge._fetchQueue();"
            //_fetchQueue() 取出消息字典里的内容 在js里面把 字典变成字符串
            //🌰 [webView stringByEvaluatingJavaScriptFromString:@"document.location.href"]    //获取当前页面的url。
            //如果不是js调用的 messageQueueString = [];
            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;
    }
}

[_base injectJavascriptFile]加载的js文件

//js这边 先把方法名字、参数、处理方法保存成一个字典在转成json字符串,在通过UIWebview调用js中某个方法把这个json字符串传到Native中去,同时把这个处理的方法以key-value形式放到一个js的字典中。
// UIWebView在收到这个json之后,进行数据处理、还有js的回掉的处理方法(就是那个callbackId)处理完成后也会拼成一个key-value字典通过调用js传回去(可以直接调用js)。
// js在接到这个json后,根据responseId读取responseCallbacks中处理方法进行处理Native code返回的数据。

;(function() {
    if (window.WebViewJavascriptBridge) { return }
    var messagingIframe
    //发送的消息队列
    var sendMessageQueue = []
    //接收消息队列
    var receiveMessageQueue = []
    //回调映射
    var messageHandlers = {}
    //改变 wvjbscheme 和 __WVJB_QUEUE_MESSAGE__ 这连个组合标识 给webview 的 delegate判断用的
    var CUSTOM_PROTOCOL_SCHEME = 'kadios'
    var QUEUE_HAS_MESSAGE = '__KAD_QUEUE_MESSAGE__'
    //js 调用 OC 如果有回调会加入这里
    var responseCallbacks = {}
    var uniqueId = 1
    //获取iframe 
    function _createQueueReadyIframe(doc) {
        messagingIframe = doc.createElement('iframe')
        messagingIframe.style.display = 'none'
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE
        //在iframe 最加 src(路径)属性
        doc.documentElement.appendChild(messagingIframe)
    }

    function init(messageHandler) {
        if (WebViewJavascriptBridge._messageHandler) { throw new Error('WebViewJavascriptBridge.init called twice') }
        WebViewJavascriptBridge._messageHandler = messageHandler
        var receivedMessages = receiveMessageQueue
        receiveMessageQueue = null
        for (var i=0; i<receivedMessages.length; i++) {
            _dispatchMessageFromObjC(receivedMessages[i])
        }
    }

    function send(data, responseCallback) {
        _doSend({ data:data }, responseCallback)
    }
    //oc 调用 js
    function registerHandler(handlerName, handler) {
        messageHandlers[handlerName] = handler
    }
    //js调用oc
    function callHandler(handlerName, data, responseCallback) {
        _doSend({ handlerName:handlerName, data:data }, responseCallback)
    }
    //js需要触发oc必须调用该方法
    function _doSend(message, responseCallback) {
        if (responseCallback) {
            //callbackId 该事件的id  把回调方法与id绑定
            var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime()
            responseCallbacks[callbackId] = responseCallback
            message['callbackId'] = callbackId
        }
        //把字典放到消息队列中
        sendMessageQueue.push(message)
        //产生一个url 执行URL重定向
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE
    }

    //sendMessageQueue 里面是消息字典  内容有 handlerName data  callbackId
    //这方法是取出 sendMessageQueue的内容
    function _fetchQueue() {
        var messageQueueString = JSON.stringify(sendMessageQueue)
        //获取完后吧发送消息队列清空 防止重复调用
        sendMessageQueue = []
        return messageQueueString
    }
    //js中处理oc的消息 判断oc获取到js端的消息
    function _dispatchMessageFromObjC(messageJSON) {
        setTimeout(function _timeoutDispatchMessageFromObjC() {
            var message = JSON.parse(messageJSON)
            var messageHandler
            var responseCallback
            //判断oc的回调是 response 还是 callback 
            if (message.responseId) {
                responseCallback = responseCallbacks[message.responseId]
                if (!responseCallback) { return; }
                responseCallback(message.responseData)
                delete responseCallbacks[message.responseId]
            } else {
                //js 调用 oc后  oc返回数据
                if (message.callbackId) {
                    var callbackResponseId = message.callbackId
                    responseCallback = function(responseData) {
                        _doSend({ responseId:callbackResponseId, responseData:responseData })
                    }
                }
                //这段代码 触发js调用 responseCallback
                var handler = WebViewJavascriptBridge._messageHandler
                if (message.handlerName) {
                    handler = messageHandlers[message.handlerName]
                }
                
                try {
                    handler(message.data, responseCallback)
                } catch(exception) {
                    if (typeof console != 'undefined') {
                        console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception)
                    }
                }
            }
        })
    }
    //OC调用js的方法通过这方法
    function _handleMessageFromObjC(messageJSON) {
        if (receiveMessageQueue) {
            receiveMessageQueue.push(messageJSON)
        } else {
            _dispatchMessageFromObjC(messageJSON)
        }
    }
    //js定义 WebViewJavascriptBridge对象
    window.WebViewJavascriptBridge = {
        init: init,
        send: send,
        registerHandler: registerHandler,
        callHandler: callHandler,
        _fetchQueue: _fetchQueue,
        _handleMessageFromObjC: _handleMessageFromObjC
    }

    var doc = document
    _createQueueReadyIframe(doc)
    var readyEvent = doc.createEvent('Events')
    readyEvent.initEvent('WebViewJavascriptBridgeReady')
    readyEvent.bridge = WebViewJavascriptBridge
    doc.dispatchEvent(readyEvent)
})();

OC调用JS的原理

OC调用JS方法使用callHandler方法,该方法把需要的(data,handlerName,callbackId) 封装成字典,然后把字典转为String,然后把该String写入到webView中,实现与js交互。

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


//webView 写入js  OC调用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"];
    //调用js中的_handleMessageFromObjC方法
    NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
    if ([[NSThread currentThread] isMainThread]) {
        [self _evaluateJavascript:javascriptCommand];

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

推荐阅读更多精彩内容