iOS 與 Web 互調

前提

蘋果推出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代碼。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容