OC和JS交互(UIWebView)中級篇1

作為一名合格的iOS開發(fā), 光了解Native是不夠的, 在很多情況下, 我們都要和Web去做交互, 了解OC和Web交互的原理有助于我們更好的對底層框架進(jìn)行改動優(yōu)化.

還沒有了解OC和JS交互的基本原理的可以快速瀏覽下, 在這里還要繼續(xù)深入的探討下OC和JS交互.

WebViewJavascriptBridge

這是Github傳送門, 其實OC和JS交互有很多種, 其中一種主要的方式叫做JS注入, 這是一種比較傳統(tǒng)也比較經(jīng)典的方式, WebViewJavascriptBridge也是這種方式.

1 首先在你要加載的Web頁面里會有這樣的一段JS代碼,
// 這段代碼是固定的智绸,必須要放到j(luò)s中
1       function setupWebViewJavascriptBridge(callback) {
2            if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
3            if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
4            window.WVJBCallbacks = [callback];
5            var WVJBIframe = document.createElement('iframe');
6            WVJBIframe.style.display = 'none';
7            WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';
8            document.documentElement.appendChild(WVJBIframe);
9            setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
        }

連注釋都copy過來了, 這里是一個腳本方法, 下面的代碼直接調(diào)用了setupWebViewJavascriptBridge這個方法
第2行, 意思是window.WebViewJavascriptBridge存在就調(diào)用callback(WebViewJavascriptBridge)并返回
第3行, 意思是window.WVJBCallbacks存在就入棧callbackWVJBCallbacks并返回
第4行, 意思是將數(shù)組WVJBCallbacks初始化為[callback]
第5行, 意思是創(chuàng)建一個iframe對象WVJBIframe
第6行, 設(shè)置 WVJBIframe的display屬性
第7行, 設(shè)置 WVJBIframe的src屬性為'wvjbscheme://__BRIDGE_LOADED__', 這行代碼很關(guān)鍵, 這里相當(dāng)于改變了iframe的src, 導(dǎo)致了UIWebView的代理方法- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType被執(zhí)行,
第8行, 將WVJBIframet添加到document
第9行, 調(diào)用setTimeout(func, 0)方法, 傳入一個callback方法function() { document.documentElement.removeChild(WVJBIframe) }, 0) }

2 在ViewController中, 初始化bridge
VC
self.bridge = [WebViewJavascriptBridge bridgeForWebView:self.webView];
[self.bridge setWebViewDelegate:self];
WebViewJavascriptBridge
1 初始化bridge
+ (instancetype)bridgeForWebView:(WVJB_WEBVIEW_TYPE*)webView {
    WebViewJavascriptBridge* bridge = [[self alloc] init];
    [bridge _platformSpecificSetup:webView];
    return bridge;
}
2 給webView綁定代理, 并把base的代理設(shè)置成自己
- (void) _platformSpecificSetup:(WVJB_WEBVIEW_TYPE*)webView {
    _webView = webView;
    _webView.delegate = self;
    _base = [[WebViewJavascriptBridgeBase alloc] init];
    _base.delegate = self;
}
3 代理方法被執(zhí)行
- (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 isCorrectProcotocolScheme: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;
    }
}

上面的4段代碼顯示了, 一個web是如何傳入WebViewJavascriptBridge并進(jìn)行代理綁定的, 這里最終的目的是想讓所有web的代理在WebViewJavascriptBridge被執(zhí)行, 也就是我們要在WebViewJavascriptBridge里面進(jìn)行統(tǒng)一的攔截, shouldStartLoadWithRequest被執(zhí)行的時候傳了NSURLRequest類型參數(shù), 而這個requestURL正是'wvjbscheme://__BRIDGE_LOADED__', 也就是網(wǎng)頁內(nèi)容中設(shè)置的src的值. [_base isCorrectProcotocolScheme:url][_base isBridgeLoadedURL:url] 是判斷scheme和URL是否等于wvjbscheme__BRIDGE_LOADED__, 如果相等, 就執(zhí)行下面的代碼

WebViewJavascriptBridgeBase
- (void)injectJavascriptFile {
    NSString *js = WebViewJavascriptBridge_js();
    [self _evaluateJavascript:js];
    if (self.startupMessageQueue) {
        NSArray* queue = self.startupMessageQueue;
        self.startupMessageQueue = nil;
        for (id queuedMessage in queue) {
            [self _dispatchMessage:queuedMessage];
        }
    }
}

- (void) _evaluateJavascript:(NSString *)javascriptCommand {
    [self.delegate _evaluateJavascript:javascriptCommand];
}

這里是先從WebViewJavascriptBridge_js文件中取出js腳本并進(jìn)行執(zhí)行,

@protocol WebViewJavascriptBridgeBaseDelegate <NSObject>
- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand;
@end
WebViewJavascriptBridge
- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand
{
    return [_webView stringByEvaluatingJavaScriptFromString:javascriptCommand];
}

這段腳本最終被WebViewJavascriptBridge中的_webView執(zhí)行了, 這里可以看出比較強的設(shè)計思想, WebViewJavascriptBridgeBase中只是提供通用的方法, 而不保存_webView的實例, _webView的實例保存在WebViewJavascriptBridge, 這個UIWebView和JS交互用到的bridge, 并且請求的攔截也是在這里完成的, 至此就完成了JS的注入, 下面來看下, 到底注入了什么東西

// This file contains the source for the Javascript side of the
// WebViewJavascriptBridge. It is plaintext, but converted to an NSString
// via some preprocessor tricks.
//
// Previous implementations of WebViewJavascriptBridge loaded the javascript source
// from a resource. This worked fine for app developers, but library developers who
// included the bridge into their library, awkwardly had to ask consumers of their
// library to include the resource, violating their encapsulation. By including the
// Javascript as a string resource, the encapsulation of the library is maintained.

#import "WebViewJavascriptBridge_JS.h"

NSString * WebViewJavascriptBridge_js() {
    #define __wvjb_js_func__(x) #x
    
    // BEGIN preprocessorJSCode
    static NSString * preprocessorJSCode = @__wvjb_js_func__(
;(function() {
    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);
        }
    }
})();
    ); // END preprocessorJSCode

    #undef __wvjb_js_func__
    return preprocessorJSCode;
};

這是一段通用的注入代碼, 因為本人JS能力有限, 大致是創(chuàng)建一些JS對象, 并對一些后面需要用到的數(shù)據(jù)進(jìn)行初始化操作, 這里主要說幾個前面用到的, 下面有說錯的地方, 請大神們看到了輕拍

window.WebViewJavascriptBridge = {
        registerHandler: registerHandler,
        callHandler: callHandler,
        disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
        _fetchQueue: _fetchQueue,
        _handleMessageFromObjC: _handleMessageFromObjC
    };

定義了一個叫WebViewJavascriptBridge的作用域, 里面有幾個方法
registerHandler, callHandler等, 至于這些方法有什么用, 等后面再說. 這里先看代碼的結(jié)構(gòu).

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

這里還是和之前類似的, 創(chuàng)建一個messagingIframeiframe, 并改變src, 觸發(fā)UIWebView代理方法回調(diào).

registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);

注冊一個JS方法, 后面供OC調(diào)用

setTimeout(_callWVJBCallbacks, 0);
    function _callWVJBCallbacks() {
        var callbacks = window.WVJBCallbacks;
        delete window.WVJBCallbacks;
        for (var i=0; i<callbacks.length; i++) {
            callbacks[i](WebViewJavascriptBridge);
        }
    }

調(diào)用setTimeout方法, 并傳遞一個_callWVJBCallbacks函數(shù)指針, 給setTimeout, 至于這個setTimeout到底有什么作用, 小編暫時還沒搞清楚.

至此, 我們看下OC中最關(guān)鍵的一個方法

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType

攔截請求的方法, 這里一共回調(diào)了幾次
1 url = file:///Users/bigparis/Library/Developer/CoreSimulator/Devices/F2CEEE61-7EAC-43BA-8412-BB886AA1E4D4/data/Containers/Bundle/Application/D099E6F5-BE84-49BC-988F-AF6D6E9622F4/WebViewJSBridgeDemo.app/index.html這次是加載網(wǎng)頁的時候回調(diào)的.
2 url = url wvjbscheme://__BRIDGE_LOADED__ 這次是網(wǎng)頁內(nèi)容執(zhí)行到WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';這句的時候觸發(fā)的. 這里會導(dǎo)致把事先準(zhǔn)備好的文件WebViewJavascriptBridge_js中的內(nèi)容全部注入, 這里host已經(jīng)說明了, 是LOADED, 也就是加載.
3 url=wvjbscheme://__WVJB_QUEUE_MESSAGE__這次是在注入的過程中執(zhí)行到了messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;這句觸發(fā)的, 這里host已經(jīng)說明了, 是QUEUE_HAS_MESSAGE是因為有消息觸發(fā), 但是由于實際上這里只是在初始化注入, JS中的消息隊列sendMessageQueue中并沒有實際內(nèi)容, 所以沒有進(jìn)行后續(xù)的執(zhí)行了.

WebViewJavascriptBridgeBase.m
- (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;
    }
    // something else...
}

初始化注入到這里就截止了.

當(dāng)然, 對于不同的網(wǎng)頁, 可能不止這3次, 但是這3次是必要的, 本文至此就已經(jīng)詳細(xì)說明了如何對JS進(jìn)行注入. 下一篇將詳細(xì)說明下利用注入的JS, 如何進(jìn)行交互.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市访忿,隨后出現(xiàn)的幾起案子瞧栗,更是在濱河造成了極大的恐慌,老刑警劉巖海铆,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件迹恐,死亡現(xiàn)場離奇詭異,居然都是意外死亡卧斟,警方通過查閱死者的電腦和手機(jī)殴边,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進(jìn)店門憎茂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人锤岸,你說我怎么就攤上這事竖幔。” “怎么了是偷?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵拳氢,是天一觀的道長。 經(jīng)常有香客問我蛋铆,道長馋评,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任戒职,我火速辦了婚禮,結(jié)果婚禮上透乾,老公的妹妹穿的比我還像新娘洪燥。我一直安慰自己,他們只是感情好乳乌,可當(dāng)我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布捧韵。 她就那樣靜靜地躺著,像睡著了一般汉操。 火紅的嫁衣襯著肌膚如雪再来。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天磷瘤,我揣著相機(jī)與錄音芒篷,去河邊找鬼。 笑死采缚,一個胖子當(dāng)著我的面吹牛针炉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播扳抽,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼篡帕,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了贸呢?” 一聲冷哼從身側(cè)響起镰烧,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎楞陷,沒想到半個月后怔鳖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡固蛾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年败砂,在試婚紗的時候發(fā)現(xiàn)自己被綠了赌渣。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡昌犹,死狀恐怖坚芜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情斜姥,我是刑警寧澤鸿竖,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站铸敏,受9級特大地震影響缚忧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜杈笔,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一闪水、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蒙具,春花似錦球榆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至篱昔,卻和暖如春每强,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背州刽。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工空执, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人穗椅。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓脆烟,卻偏偏與公主長得像,于是被迫代替她去往敵國和親房待。 傳聞我的和親對象是個殘疾皇子邢羔,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,514評論 2 348

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