前提
蘋果推出JSCore 以前,iOS 調用JS 只能通過WebView 執行JSString 來實現。而WebView 沒法直接調用iOS,只能觸發特定鏈接,讓iOS在WebView代理方法中捕獲到這特定鏈接,從而執行相應操作,間接實現WebView 調 iOS。
原理
// iOS 調 JS
[_webView stringByEvaluatingJavaScriptFromString:jsString];
// JS 調 iOS
1:
web 將href 改為特定值
2:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSURL *url = [request URL];
if (url == 特定值) {call method}
}
iOS7之后蘋果推出JSCore,通過獲取web上下文環境,實現了iOS可以直接調JS方法,同時iOS 也能將block 賦值給JS的方法,實現了JS調用iOS并傳值。
// 獲取JS上下文
JSContext *jsContext;
-(void)webViewDidFinishLoad:(UIWebView *)webView {
jsContext = [_webview valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
}
// iOS 調 JS
JSValue *funcValue = jsContext[@"insertText"];
[funcValue callWithArguments:@[@"hello!!!!"]];
//JS 調 iOS
jsContext[@"callNative"] = ^(NSString*str){
NSLog(str);
};
三方庫WebViewJavascriptBridge
當然了不用三方庫,自己也是能實現,之所有在這介紹這個三方庫,還是覺得這個庫設計還是不錯,值得學習。
結構:
iOS端用法:
- iOS 端與 web端統一好handler 名。
- 初始化bridge
_bridge = [WebViewJavascriptBridge bridgeForWebView:_webView];
- iOS call Web,iOS 端要做的
// iOS 端直接callHandler
[_bridge callHandler:@"JSToDo" data:@{}];
- Web call iOS ,iOS 端要做的
// 注冊handler 等待被Web調用
[_bridge registerHandler:@"OCToDo" handler:^(id data, WVJBResponseCallback responseCallback) {
NSString *str = [NSString stringWithFormat:@"%@",data];
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"iOS alert" message:str preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"ok" style:UIAlertActionStyleDefault handler:nil];
[alert addAction:okAction];
[self presentViewController:alert animated:false completion:nil];
responseCallback(@"iOS receive data");
}];
用法對法iOS 端來說還是蠻簡單的!當然這些僅僅靠iOS 端還不夠,需要web 端配合,待分析了實現原理后再看web 端的工作。
關系、作用:
前面說過JSCore 以前 iOS調web 只能用webView 執行JSString, web 調 iOS 只能主動觸發特定url 讓iOS 端監聽到,實現間接調用。那這個三方庫的實現基礎正是如此。只不過在代碼層面通過“觀察者模式”加以封裝。
- Class: WebViewJavascriptBridge
1、 在初始化時成為webView 的真實代理
// 實例化
+ (instancetype)bridgeForWebView:(WVJB_WEBVIEW_TYPE*)webView {
WebViewJavascriptBridge* bridge = [[self alloc] init];
[bridge _platformSpecificSetup:webView];
return bridge;
}
// 成為代理、初始化WebVeiwJavascriptBridgeBase
- (void) _platformSpecificSetup:(WVJB_WEBVIEW_TYPE*)webView {
_webView = webView;
_webView.delegate = self;
_base = [[WebViewJavascriptBridgeBase alloc] init];
_base.delegate = self;
}
// 實現WebViewDelegate方法
...
2、暴露接口, 提供callHandler、registerHandler 方法
- (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler;
- (void)callHandler:(NSString*)handlerName;
- (void)callHandler:(NSString*)handlerName data:(id)data;
- (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback;
3、監聽Url變化
- (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]) {// wvjbscheme://__BRIDGE_LOADED__
[_base injectJavascriptFile]; // 重點:注入JSBridge
} else if ([_base isQueueMessageURL:url]) {
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]]; //從Web 頁面獲取JS 數據
[_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;
}
}
主要就是提供接口、監聽變化,具體實現都是交給WebViewJavascriptBridgeBase。
- Class: WebViewJavascriptBridgeBase
給WebViewJavascript 類充當業務層,實現CallHandler、registerHandler。通過操控WebView 執行JSString以及監聽WebView URL 變化 實現iOS 與 Web交互。
1、 RegisterHandler
// 將handlerName 保存起來
_base.messageHandlers[handlerName] = [handler copy];
2、CallHandler
message字段處理(需要回調時將responseCallback按唯一callbackId保存)—>message 轉json字符串—>WebView 執行JSString
3、消息處理
messageString 轉數組
遍歷
處理:取每條消息,看是否有responseId,有則是iOS端調Web端之后所需要的回調。沒有則就是Web主動調iOS所發送數據。
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);
}
- Class: WebViewJavascriptBridge_JS
一段字符串,同時也是真正的bridge, 通過WebView 執行JSString 將 全局變量WebViewJavascriptBridge放在Web 頁面的JS 中。
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
``
不管Web頁面怎么變,JSBridge 是不變的,僅僅提供幾個交互的方法就夠了,所以這一塊被提取出來,以string 的形式存放在三方庫的文件 WebViewJavascriptBridge_JS.m中,需要用時注入頁面就行。這也是這個三方庫架構設計很合理的地方。
- Class: WKWebViewJavascriptBridge
跟WebViewJavascriptBridge功能類似,僅僅為WKWebView 做些特殊處理。
### Web 端要做的
- 注入bridge
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)
}
當然web端沒有必要自己保存JSBridge,只是通過 “WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__'; ” 來觸發iOS 端將JSBridge 注入頁面。
- 如果需要被iOS 調用則registerHandler
bridge.registerHandler('JSToDo', function(data, responseCallback) {
var a = document.createElement("span");
a.innerText = "hello !!!!";
document.documentElement.appendChild(a);
responseCallback(data)
})
- 如果需要調iOS 則callHandler
window.WebViewJavascriptBridge.callHandler('OCToDo',{name:'carson'}, function responseCallback(responseData) {
console.log("JS received response:", responseData)
})
前面說了web 不能主動調iOS, 那web callHandler 時也是只能觸發一個特定url 請求,同時將數據放在頁面上,iOS監聽到url 則去獲取頁面數據。實現Web 調iOS。
##總結
其實重點很少,也提了很多次,也就是WebView 執行JSString 實現調web, web 觸發特定URL 讓原生監聽到,執行相應操作,實現web調iOS 。當然JSCore 出現之后就不再是這個原理了,比這直接很多,最前面的原理也提到了。
這個庫也還是有很多值得學習的地方,包括觀察者模式,web和原生都只管注冊和調用就夠了。還有將JSBridge 的抽象出來,放在三方庫中,簡化了很多web代碼。