作為一名合格的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
存在就入棧callback
到WVJBCallbacks
并返回
第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ù), 而這個request
的URL
正是'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)建一個messagingIframe
的iframe
, 并改變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)行交互.