OC與JS交互之 WebViewJavascriptBridge 實現(xiàn)原理

導(dǎo)語

本文將講解 UIWebView 和 WKWebView 通過 WebViewJavascriptBridge 三方庫進(jìn)行通信的原理;梳理OC -- > JS凤粗, JS --> OC 調(diào)動交互的流程辅柴;和OC方薯鳍、JS方各自做了具體什么操作才使Native與H5有了交互的功能沉衣。本文并不會去講解 WebViewJavascriptBridge 庫的使用和方法的講解盔然,你可以通過以下參考內(nèi)容去了解愉烙。

OC 調(diào)用 JS

oc 調(diào)用 js

OC 相關(guān)代碼: 調(diào)用與JS商量好的 oc_to_js_js’registerFunctionName 函數(shù)

[_webViewBridge callHandler:@“oc_to_js_js’registerFunctionName” data:@"oc傳給js的數(shù)據(jù)” responseCallback:^(id responseData) {
        NSLog(@"JS執(zhí)行完回調(diào)給OC的數(shù)據(jù):%@“,responseData);
    }];

JS 相關(guān)代碼: 注冊一個叫 oc_to_js_js’registerFunctionName 的函數(shù)

setupWebViewJavascriptBridge(function(bridge) {
                                 bridge.registerHandler('oc_to_js_js’registerFunctionName', function(data, responseCallback) {
                                                        alert(‘oc成功調(diào)用js讨盒,并打印數(shù)據(jù)data:’+data);
                                                        responseCallback(‘js執(zhí)行完回調(diào)給OC的數(shù)據(jù)');
                                                        })
                                 })

如上OC和JS代碼可以看出,OC需要調(diào)用JS注冊的‘ oc_to_js_js’registerFunctionName’ 方法步责,并傳遞數(shù)據(jù)data給JS返顺;JS在收到OC的消息后alert了傳遞過來的數(shù)據(jù),并回傳了一份數(shù)據(jù)給OC蔓肯。
那么這個 oc--data--js--data--oc 的流程到底是怎樣的呢
大致步驟:

  • 步驟一 (oc環(huán)境) : OC先聲明一段JS代碼遂鹊,在第一次進(jìn)入此webView頁的時候注入到 webView中,之后webView上下文中就有了這些js函數(shù)蔗包。
注入JS代碼
- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener {
   ...
   if ([_base isQueueMessageURL:navigationAction.request.URL]) {
         [webView stringByEvaluatingJavaScriptFromString:@“js代碼”]
    } 
   ...  
}
  • 步驟二 (js環(huán)境) : 注冊聲明 js 會被 oc 調(diào)用的方法存放在 js 環(huán)境的字典中秉扑;
保存js注冊的函數(shù)
function registerHandler(handlerName, handler) {
   messageHandlers[handlerName] = handler;
}
  • 步驟三 (oc環(huán)境) :oc 發(fā)起調(diào)用,生成一個message字典调限,三個參數(shù)(handlerName 方法名舟陆,data數(shù)據(jù), callbackId 回調(diào)方法)
- (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];
}
  • 步驟四 (oc環(huán)境) : 把message字典轉(zhuǎn)成json字符串旧噪,處理字符串里面的\吨娜、\、\r淘钟、\n 等特殊字符宦赠。生成新的字符串并加上
    WebViewJavascriptBridge._handleMessageFromObjC(‘messageJSON‘)陪毡,形成一段字符串形式的js代碼(既通過提前注入js中的函數(shù)_handleMessageFromObjC去處理 messageJSON 參數(shù))。
- (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];
}
  • 步驟五 (oc環(huán)境) : 通過調(diào)用oc系統(tǒng)的 stringByEvaluatingJavaScriptFromString 方法讓webView執(zhí)行上述js代碼勾扭。 注: WKWebView 為 evaluateJavaScript 方法.
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
dispatch_sync(dispatch_get_main_queue(), ^{
           [webView stringByEvaluatingJavaScriptFromString:javascriptCommand]
        });
  • 步驟六 (js環(huán)境) : 通過 oc 傳入過來的json數(shù)據(jù)毡琉,解析后取出 functionName 和 callbackId,從事先注冊好的緩存中獲取對應(yīng)handler函數(shù)和callback函數(shù)妙色, 并執(zhí)行桅滋。
取出 callback 函數(shù)和 handler 函數(shù)執(zhí)行調(diào)用,在handler中執(zhí)行 callback 數(shù)據(jù)回傳操作身辨。
responseCallback = responseCallbacks[message.responseId];
var handler = messageHandlers[message.handlerName];
if (handler)  {
    handler(message.data, responseCallback);
}
  • 步驟七 (js環(huán)境) :在js的 callback 函數(shù)執(zhí)行完后丐谋,需要從js環(huán)境發(fā)生數(shù)據(jù)給oc環(huán)境,這時候js會改變html根節(jié)點下隱性的<iframe>標(biāo)簽的src煌珊,當(dāng)iframe的src改變時會發(fā)起Request号俐,這時候oc就能監(jiān)聽到j(luò)s發(fā)生了Request操作 。 這個環(huán)節(jié)的具體操作也就跟下面即將要講的 js 調(diào)用 oc 的流程類似了定庵。

JS 調(diào)用 OC

js 調(diào)用 oc

JS調(diào)用OC 一般會有三種方式

  • iOS7 引入了 JavaScriptCore吏饿,可以初始化一個 JSContext 對象,然后約定好一個方法名就好了蔬浙。
  • 特殊的一個 Scheme 猪落。客戶端這邊會攔截到這種指令格式的 URL 需求畴博,實現(xiàn)一個 JS 到 Native 傳遞消息的一個過程
  • 輪詢笨忌,對于 JS 他需要把給 Native 傳遞的消息,轉(zhuǎn)化成一個 JSON 绎晃,客戶端這邊一般會開一個線程蜜唾,每隔一段時間會調(diào) JS 的方法,從這個方法里面把 JS 需要給 Native 傳遞的消息全部取出來庶艾,取出來之后再去做相應(yīng)的操作袁余,客戶端開銷比較大。

本文只要講解第二種方式咱揍,通過Scheme去實現(xiàn)一個 JS 到 Native 傳遞消息的一個過程颖榜。

OC 代碼 : 注冊一個叫 js_to_oc_oc’registerFunctionName 的方法

[webViewBridge registerHandler:@"js_to_oc_oc’registerFunctionName” handler:^(id data, WVJBResponseCallback responseCallback) {
        NSLog(@“js成功調(diào)用oc, 并獲取數(shù)據(jù)data”);
        responseCallback(@“oc回調(diào)給js的數(shù)據(jù) 如:支付失敗”);
    }];

JS 代碼 : 調(diào)用與OC商量好的 js_to_oc_oc’registerFunctionName 方法

WebViewJavascriptBridge.callHandler('js_to_oc_oc’registerFunctionName', "{\"content\" : \"js傳遞給oc的數(shù)據(jù)內(nèi)容\”}”, function(response) {
                                            alert('oc成功被調(diào)用煤裙,并回傳給js的回調(diào)數(shù)據(jù):’ + response);
                                            document.getElementById("returnValue").value = response;
                                            });

如上OC和JS方法可以看出掩完, JS需要調(diào)用OC注冊的‘ js_to_oc_oc’registerFunctionName’ 方法,并傳遞數(shù)據(jù)json字符串給OC硼砰;OC在收到JS的消息后log了傳遞過來的數(shù)據(jù)且蓬,并回傳了一份數(shù)據(jù)給JS。
大致步驟:

  • 步驟一 (oc環(huán)境) : OC先聲明一段 js 通用代碼题翰,在第一次進(jìn)入此webView頁的時候注入到 webView中恶阴,之后webView上下文中就有了這些js方法(如果已注入過忽略此步)诈胜。

  • 步驟二 (oc環(huán)境) : 注冊oc方法, 存儲一個message字典冯事,以 handlerName 為key焦匈, handler 為value 。

- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
    _base.messageHandlers[handlerName] = [handler copy];
}
  • 步驟三 (js環(huán)境) : js 發(fā)起調(diào)用方法昵仅,傳入handlerName缓熟,data,callBack 參數(shù)摔笤, 生成message字典够滑,保存在js環(huán)境中。
function callHandler(handlerName, data, responseCallback) {
    ...
    _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);
}
  • 步驟四 (js環(huán)境) :js發(fā)起調(diào)用時籍茧,會對當(dāng)前html中的<iframe> 標(biāo)簽的src重新賦值一個scheme版述, <iframe> 標(biāo)簽會在src改變時自動發(fā)起Request跳轉(zhuǎn), 因此 oc 才會收到消息(下一步)
最開始注入的js代碼中,會對當(dāng)前html添加一個<iframe>, 并設(shè)置為隱性
iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(iframe);
重新賦值iframe的src
iframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
  • 步驟五 (oc環(huán)境) :WKWebview 和 UIWebview 都會有個 decidePolicyForNavigationAction 的代理方法可以攔截Request的url寞冯, 對url的scheme進(jìn)行判斷是普通跳轉(zhuǎn)還是JS跳轉(zhuǎn)。
- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener {
    ...  
    if ([_base isCorrectProcotocolScheme:url]) {
         ...
         // js 跳轉(zhuǎn)晚伙,攔截處理
        decisionHandler(WKNavigationActionPolicyCancel); 
    }else {
        decisionHandler(WKNavigationActionPolicyAllow); 
    }
}
  • 步驟六 (oc環(huán)境) :如果是js跳轉(zhuǎn)吮龄, 通過系統(tǒng)的 stringByEvaluatingJavaScriptFromString 方法調(diào)用 ‘WebViewJavascriptBridge._fetchQueue()’ 這段注入過的js代碼, 獲取此方法的返回值咆疗, 既獲取步驟3的 message漓帚。 注: WKWebView 為 evaluateJavaScript 方法獲取.
[webView evaluateJavaScript:@"WebViewJavascriptBridge._fetchQueue();" completionHandler:^(NSString* result, NSError* error) {
   // result 為獲取到的函數(shù)信息(以 handlerName、data午磁、callback) 為鍵的json字符串
}];
js 函數(shù)
function _fetchQueue() {
    var messageQueueString = JSON.stringify(sendMessageQueue);
    sendMessageQueue = [];
    return messageQueueString;
}
  • 步驟七 (oc環(huán)境) :根據(jù)從js獲取到的函數(shù)信息message 尝抖,以 handlerName 為key ,從之前 oc 中保存的方法字典中取出對應(yīng)方法迅皇,并執(zhí)行昧辽。
WVJBResponseCallback responseCallback = _responseCallbacks[message[@"responseId"]]; // 獲取存儲的callback函數(shù)
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];  // 獲取存儲的handler函數(shù)
 if (!handler) {
     NSLog(@"oc環(huán)境中沒有注冊過此方法");
    continue;
 }     
handler(message[@"data"], responseCallback);

總結(jié)

  • OC —>JS : js注冊好 functionNameIdentitify , oc 組裝好 {functionNameIdentitify, data登颓, callback} 字典搅荞,再轉(zhuǎn)化成字符串后,通過系統(tǒng)方法執(zhí)行js代碼框咙,js函數(shù)里去解析傳入的json串咕痛,獲取functionName等數(shù)據(jù),從緩存中取出對應(yīng)函數(shù)喇嘱。

  • JS —> OC : 原理類似茉贡,oc 注冊好 functionNameIdentitify 后 , 當(dāng)html的<iframe>request后者铜, 系統(tǒng)方法 decidePolicyForNavigationAction 會被執(zhí)行腔丧,獲取到當(dāng)前操作的request.url放椰。 攔截到是js發(fā)起的操作后, 通過系統(tǒng)方法stringByEvaluatingJavaScriptFromString 和 js方法._fetchQueue() 獲取對應(yīng)的js數(shù)據(jù)悔据; 從數(shù)據(jù)中獲取functionName 或 callbackId庄敛, 最后在oc緩存的字典中取出對應(yīng)方法執(zhí)行。

  • 不管是OC —>JS, 還是JS —> OC 科汗, 最終都是通過系統(tǒng)的stringByEvaluatingJavaScriptFromString 或者 evaluateJavaScript 方法建立的橋梁藻烤,去獲取js數(shù)據(jù)或者傳入數(shù)據(jù)給js。

關(guān)于 js 與 oc 交互的更多細(xì)節(jié)头滔,可以自己查看WebViewJavascriptBridge的源碼 https://github.com/marcuswestin/WebViewJavascriptBridge
最后附上一段一開始被注入到webView中的JS代碼
if (window.WebViewJavascriptBridge) {
    return;
}

if (!window.onerror) {
    window.onerror = function(msg, url, line) {
        console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line);
    }
}
window.WebViewJavascriptBridge = {
    registerHandler: registerHandler,
    callHandler: callHandler,
    disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
    _fetchQueue: _fetchQueue,
    _handleMessageFromObjC: _handleMessageFromObjC
};

var messagingIframe;
var sendMessageQueue = [];
var messageHandlers = {};

var CUSTOM_PROTOCOL_SCHEME = 'wvjbscheme';
var QUEUE_HAS_MESSAGE = '__WVJB_QUEUE_MESSAGE__';

var responseCallbacks = {};
var uniqueId = 1;
var dispatchMessagesWithTimeoutSafety = true;

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

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

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

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

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

function _handleMessageFromObjC(messageJSON) {
    _dispatchMessageFromObjC(messageJSON);
}

messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(messagingIframe);

registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);

setTimeout(_callWVJBCallbacks, 0);

function _callWVJBCallbacks() {
    var callbacks = window.WVJBCallbacks;
    delete window.WVJBCallbacks;
    for (var i = 0; i < callbacks.length; i++) {
        callbacks[i](WebViewJavascriptBridge);
    }
}
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末怖亭,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子坤检,更是在濱河造成了極大的恐慌兴猩,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件早歇,死亡現(xiàn)場離奇詭異倾芝,居然都是意外死亡,警方通過查閱死者的電腦和手機箭跳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進(jìn)店門晨另,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人谱姓,你說我怎么就攤上這事借尿。” “怎么了屉来?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵路翻,是天一觀的道長。 經(jīng)常有香客問我茄靠,道長茂契,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任嘹黔,我火速辦了婚禮账嚎,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘儡蔓。我一直安慰自己郭蕉,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布喂江。 她就那樣靜靜地躺著召锈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪获询。 梳的紋絲不亂的頭發(fā)上涨岁,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天拐袜,我揣著相機與錄音,去河邊找鬼梢薪。 笑死蹬铺,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的秉撇。 我是一名探鬼主播甜攀,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼琐馆!你這毒婦竟也來了规阀?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤瘦麸,失蹤者是張志新(化名)和其女友劉穎谁撼,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體滋饲,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡厉碟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了屠缭。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片墨榄。...
    茶點故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖勿她,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情阵翎,我是刑警寧澤逢并,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布身堡,位于F島的核電站螃概,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏桨嫁。R本人自食惡果不足惜贰军,卻給世界環(huán)境...
    茶點故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一玻蝌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧词疼,春花似錦俯树、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至舵盈,卻和暖如春陋率,著一層夾襖步出監(jiān)牢的瞬間球化,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工瓦糟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留筒愚,地道東北人。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓菩浙,卻偏偏與公主長得像巢掺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子芍耘,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,585評論 2 359

推薦閱讀更多精彩內(nèi)容