前提
蘋果推出JSCore 以前捉片,iOS 調(diào)用JS 只能通過WebView 執(zhí)行JSString 來實(shí)現(xiàn)桃熄。而WebView 沒法直接調(diào)用iOS帮掉,只能觸發(fā)特定鏈接,讓iOS在WebView代理方法中捕獲到這特定鏈接矾湃,從而執(zhí)行相應(yīng)操作,間接實(shí)現(xiàn)WebView 調(diào) iOS堕澄。
原理
// iOS 調(diào) JS
[_webView stringByEvaluatingJavaScriptFromString:jsString];
// JS 調(diào) 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上下文環(huán)境,實(shí)現(xiàn)了iOS可以直接調(diào)JS方法蛙紫,同時(shí)iOS 也能將block 賦值給JS的方法坞嘀,實(shí)現(xiàn)了JS調(diào)用iOS并傳值。
// 獲取JS上下文
JSContext *jsContext;
-(void)webViewDidFinishLoad:(UIWebView *)webView {
jsContext = [_webview valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
}
// iOS 調(diào) JS
JSValue *funcValue = jsContext[@"insertText"];
[funcValue callWithArguments:@[@"hello!!!!"]];
//JS 調(diào) iOS
jsContext[@"callNative"] = ^(NSString*str){
NSLog(str);
};
三方庫WebViewJavascriptBridge
當(dāng)然了不用三方庫惊来,自己也是能實(shí)現(xiàn)丽涩,之所有在這介紹這個(gè)三方庫,還是覺得這個(gè)庫設(shè)計(jì)還是不錯(cuò)裁蚁,值得學(xué)習(xí)矢渊。
結(jié)構(gòu):
iOS端用法:
- iOS 端與 web端統(tǒng)一好handler 名。
- 初始化bridge
_bridge = [WebViewJavascriptBridge bridgeForWebView:_webView];
- iOS call Web枉证,iOS 端要做的
// iOS 端直接callHandler
[_bridge callHandler:@"JSToDo" data:@{}];
- Web call iOS 矮男,iOS 端要做的
// 注冊(cè)handler 等待被Web調(diào)用
[_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");
}];
用法對(duì)法iOS 端來說還是蠻簡單的!當(dāng)然這些僅僅靠iOS 端還不夠室谚,需要web 端配合毡鉴,待分析了實(shí)現(xiàn)原理后再看web 端的工作。
關(guān)系秒赤、作用:
前面說過JSCore 以前 iOS調(diào)web 只能用webView 執(zhí)行JSString猪瞬, web 調(diào) iOS 只能主動(dòng)觸發(fā)特定url 讓iOS 端監(jiān)聽到,實(shí)現(xiàn)間接調(diào)用入篮。那這個(gè)三方庫的實(shí)現(xiàn)基礎(chǔ)正是如此陈瘦。只不過在代碼層面通過“觀察者模式”加以封裝。
- Class: WebViewJavascriptBridge
1潮售、 在初始化時(shí)成為webView 的真實(shí)代理
// 實(shí)例化
+ (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;
}
// 實(shí)現(xiàn)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、監(jiān)聽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]; // 重點(diǎn):注入JSBridge
} else if ([_base isQueueMessageURL:url]) {
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]]; //從Web 頁面獲取JS 數(shù)據(jù)
[_base flushMessageQueue:messageQueueString]; // 數(shù)據(jù)處理
} 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;
}
}
主要就是提供接口肮帐、監(jiān)聽變化咖驮,具體實(shí)現(xiàn)都是交給WebViewJavascriptBridgeBase。
- Class: WebViewJavascriptBridgeBase
給WebViewJavascript 類充當(dāng)業(yè)務(wù)層,實(shí)現(xiàn)CallHandler游沿、registerHandler饰抒。通過操控WebView 執(zhí)行JSString以及監(jiān)聽WebView URL 變化 實(shí)現(xiàn)iOS 與 Web交互。
1诀黍、 RegisterHandler
// 將handlerName 保存起來
_base.messageHandlers[handlerName] = [handler copy];
2袋坑、CallHandler
message字段處理(需要回調(diào)時(shí)將responseCallback按唯一callbackId保存)—>message 轉(zhuǎn)json字符串—>WebView 執(zhí)行JSString
3、消息處理
messageString 轉(zhuǎn)數(shù)組
遍歷
處理:取每條消息眯勾,看是否有responseId枣宫,有則是iOS端調(diào)Web端之后所需要的回調(diào)。沒有則就是Web主動(dòng)調(diào)iOS所發(fā)送數(shù)據(jù)吃环。
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
一段字符串也颤,同時(shí)也是真正的bridge, 通過WebView 執(zhí)行JSString 將 全局變量WebViewJavascriptBridge放在Web 頁面的JS 中郁轻。
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
``
不管Web頁面怎么變翅娶,JSBridge 是不變的,僅僅提供幾個(gè)交互的方法就夠了好唯,所以這一塊被提取出來竭沫,以string 的形式存放在三方庫的文件 WebViewJavascriptBridge_JS.m中,需要用時(shí)注入頁面就行骑篙。這也是這個(gè)三方庫架構(gòu)設(shè)計(jì)很合理的地方蜕提。
- 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)
}
當(dāng)然web端沒有必要自己保存JSBridge谎势,只是通過 “WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__'; ” 來觸發(fā)iOS 端將JSBridge 注入頁面。
- 如果需要被iOS 調(diào)用則registerHandler
bridge.registerHandler('JSToDo', function(data, responseCallback) {
var a = document.createElement("span");
a.innerText = "hello !!!!";
document.documentElement.appendChild(a);
responseCallback(data)
})
- 如果需要調(diào)iOS 則callHandler
window.WebViewJavascriptBridge.callHandler('OCToDo',{name:'carson'}, function responseCallback(responseData) {
console.log("JS received response:", responseData)
})
前面說了web 不能主動(dòng)調(diào)iOS, 那web callHandler 時(shí)也是只能觸發(fā)一個(gè)特定url 請(qǐng)求杨名,同時(shí)將數(shù)據(jù)放在頁面上脏榆,iOS監(jiān)聽到url 則去獲取頁面數(shù)據(jù)。實(shí)現(xiàn)Web 調(diào)iOS镣煮。
##總結(jié)
其實(shí)重點(diǎn)很少姐霍,也提了很多次鄙麦,也就是WebView 執(zhí)行JSString 實(shí)現(xiàn)調(diào)web, web 觸發(fā)特定URL 讓原生監(jiān)聽到典唇,執(zhí)行相應(yīng)操作,實(shí)現(xiàn)web調(diào)iOS 胯府。當(dāng)然JSCore 出現(xiàn)之后就不再是這個(gè)原理了介衔,比這直接很多,最前面的原理也提到了骂因。
這個(gè)庫也還是有很多值得學(xué)習(xí)的地方炎咖,包括觀察者模式,web和原生都只管注冊(cè)和調(diào)用就夠了。還有將JSBridge 的抽象出來乘盼,放在三方庫中升熊,簡化了很多web代碼。