如何設計通用WebAPI之Swift實現(xiàn)(一)

在App開發(fā)中,Web和Native的協(xié)作可謂是密不可分,因為web能快速迭代,更新,試錯。在這之中,除了純展示的頁面,其他的幾乎都會涉及到與Native的通信,調用native的功能實現(xiàn)業(yè)務需求。

常用做法

業(yè)界常用的做法就是web起個iframe,iframe設置src = 'xxx',通過自定義scheme,傳入方法名,參數,來調起native的方法。比如要調起App的登錄頁,可能是這樣寫:

var i = document.createElement('iframe');
i.style.display = 'none';
i.src = 'myApp://gotoLogin?p={}';
document.body.appendChild(i);
   
if (i && i.parentNode) {
  //destory the iframe
  i.parentNode.removeChild(i);
}     

然后在webView的回調中(一般是寫在vc中),做處理:

func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
 if (request.url.scheme == "myApp") {
    // 解析request.url,解出方法名及參數 
    // ...
  // 調用perfomSelector/NSInvocation來觸發(fā)方法
  // ...
 }
}

對,這樣就調起了登錄頁。但是如果web需要調起支付頁面呢?這還不簡單,iframe設置新的src,然后在oc中加個gotoPay的接口不就得了。

myApp://gotoPay

又有新接口,好,再加...

然后,在vc中包含webView的頁面就會充斥著各種處理web call oc的url解析,及方法定義,就像下面這樣。

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

#pragma mark - webAPi
- (void)gotoPay {}
- (void)webAPI1 {}

在web調用native的接口不多的時候,這樣寫都不是事。但是大體量的app,webAPI接口很多的時候,會有股濃濃的憂傷。這時候就需要考慮如何抽離出webAPI的解析及調用了。

需要解決的問題

  1. webAPI集中管理及通用性
  • 以上的方式十分的散亂,不利于管理。如果在其他的頁面也要調用到這個webAPi,會需要再copy一遍。所以,我們需要在某個集中的地方處理這些webAPI的調用,并且可以復用,write once,can be called anywhere。
  • 要實現(xiàn)通用性,在native實現(xiàn)webAPI的參數應該是固定的,很簡單,NSDictionary搞定,(⊙o⊙)…,或許還需要callback,下面會說到。
  1. web調用的封裝性及通用性
  • web通過iframe調用oc的方式也需要封裝起來,而不是在每個調用的地方都寫上一段創(chuàng)建iframe的代碼。只需引入一個js文件,調用其中已經封裝好的callNative方法就好。
  • callNative需要定義一套通用規(guī)范,適合所有的場景。
  1. callNative的回調
    有時候,js在調用native方法后,會等待native的結果返回后,執(zhí)行自己的處理。那么如何在oc中回調到js?

其實2,3在WebViewJavascriptBridge中都有實現(xiàn),不過我還是想用swift給實現(xiàn)一遍。

解決方案

webAPI集中管理

在yy中,webAPI非常多。根據單一職責,需要單獨的類來管理相關的接口,每個webAPI類會對應一個module。舉個栗子,DataWebAPI,module=data,專門處理跟數據相關的東西,比如獲取userId,獲取appVersion等;UIWebAPI,module=ui,處理UI相關的,如跳轉登錄頁,直播間,設置活動條frame等。

有了這些WebAPI,需要管理起來。這里采用注冊的方式,將module與webAPI instance以key-value方式存儲起來。在需要調用某個api時,找到對應module的WebAPI實例,進行分發(fā)。

func registerWebAPI(_ module: String, _ api: WebAPIProtocol) {}

當然,不注冊也行,根據web傳過來的module=UIWebAPI,利用runtime生成UIWebAPI實例,來調用。只不過我覺得注冊會使得映射關系比較清晰,并且不依賴于類名。


webAPIManager.png
通用性
  • web調用接口的通用性
    web調用webAPI只需要按一個固定模式調用,傳入相應的參數。上面提到了module的概念,所以web這邊調用,只需傳入module,method,param,callback就好。這里的callback其實是指的callbackId,下面會說到。
invokeClientMethod: function(module, name, parameters, callback) {}
  • native調用js接口通用性
invokeWebMethod: function(callback, params) {}
  • webAPI實現(xiàn)的通用性
    在native層實現(xiàn)的webAPI,也需要通用性,參數固定??紤]到有回調,所以webAPI接口參數是params+callback。
typealias SLCallback = (_ parameter: [String: Any]?) -> Void
func test(_ params:[String: AnyObject]?, callback: SLCallback?) {}
關于web調用的封裝性

這里的封裝性,比較簡單。上面說到,只需要引入js文件(名字定為bridge.js),就可以調用webAPI。所以,只需要把對應的功能函數放到bridge.js文件就好。

如何調用callback

web調用native之后的回調,就是在調完webAPI后,native這邊執(zhí)行js function。因為沒法把callback通過url的方式,傳給native。所以做法是生成全局callbackId,將callbackId與function映射起來。然后傳給native,native會生成一個block,參數為NSDictionary。執(zhí)行完webAPI,通過invokeWebMethod回調js方法的時候,再把callbackId傳回來,js這邊找到對應的function執(zhí)行。

js生成callbackId:

createGlobalFuncForCallback: function(callback){
   if (callback) {
       var name = '__GLOBAL_CALLBACK__' + (SLWebBridge.__GLOBAL_FUNC_INDEX__++);
       window[name] = function(){
           var args = arguments;
           var func = (typeof callback == "function") ? callback : window[callback];
           //we need to use setimeout here to avoid ui thread being frezzen
           setTimeout(function(){ func.apply(null, args); }, 0);
       };
       return name;
   }
   return null;
},

Native生成callback:

//cb,在js端是個id,根據id找到對應的function
let callbackId = url.objectForKey("cb")
if let callbackId = callbackId {
  // 生成callback
  callback = { result in
      guard let result = result else {
          return
      }
      
      // 將result-->string
      do {
        // 參數序列化成json
          let jsonData = try JSONSerialization.data(withJSONObject: result as Any, options: JSONSerialization.WritingOptions.prettyPrinted)
          
          var jsonString = String(data: jsonData, encoding: String.Encoding.utf8)
          
          jsonString = jsonString ?? "{}"
          
          let script = String(format: "SLWebBridge.invokeWebMethod(%@,%@);", callbackId, jsonString!)
          
          // 執(zhí)行js方法
          webView.stringByEvaluatingJavaScript(from: script)
      } catch {
          print("json to string error")
      }
  }
}
自定義scheme

根據調用native的參數,module,method,param,callback可得到如下url。

myApp://module/method?p={\"a\":2}&cb='xxxx'

最終,web在調用invokeClientMethod的時候,將parameter轉成string并encode,會生成callbackId。拼成url。

invokeClientMethod: function(module, name, parameters, callback) {
   var url = 'slwebbridge://' + module + '/' + name + '?p=' + encodeURIComponent(JSON.stringify(parameters || {}));
   
   if (callback) {
       var name;
       if (typeof callback == "function") {
         // 生成全局callbackId
           name = SLWebBridge.createGlobalFuncForCallback(callback);
       } else {
           name = callback;
       }
       
       url = url + '&cb=' + name;
   }
   console.log('[API]' + url);
   var r = SLWebBridge._openURL(url);
   return r ? r.result : null;
}

調用如下:

invokeClientMethod('ui','gotoLogin',{},function(params) {
 alert(params);
});

Native層的解析

webView回調方法中,如果判斷是我們定義的scheme,則進行解析處理。
url的規(guī)范定義是scheme://host:port/path?query

  1. 解析module
    module對應起來就是host。
let module = url.host
  1. 解析method
    method對應為path??赏ㄟ^pathComponent取出。
let pathComponents = url.pathComponents

pathComponents得到的是["/","path"],取pathComponents[1]即為method。

  1. 解析parameter
    parameter+callback對應為query。我寫了個NSURL的extension,返回dict,可取出url query中的任意參數。
func scanParameters() -> [String: String]? {
       guard !self.isFileURL else {
           return nil
       }

       let scanner = Scanner(string: self.absoluteString)
       scanner.charactersToBeSkipped = CharacterSet(charactersIn:"&?")
       scanner.scanUpTo("?", into: nil)
       
       var dict = [String: String]()
       
       var temp: NSString?

       while scanner.scanUpTo("&", into: &temp) {
           let array = temp?.components(separatedBy: "=")
           if let array = array, array.count >= 2 {
               let key = array[0].removingPercentEncoding
               let value = array[1].removingPercentEncoding
               
               dict[key!] = value
           }
       }
       
       return dict
   }

parameter可以這樣直接取。然后將jsonString轉換成dict,便可得到具體的參數。

let jsonString = url.objectForKey("p")
  1. 解析callbackId
    由上一步的extension,可以取出callbackId。
let callbackId = url.objectForKey("cb")

若callbackId存在,在會在native端生成個SLCallback,傳入到webAPI的callback參數中。在webAPI執(zhí)行完后,將要返回給js的參數傳入callback,再執(zhí)行。

WebAPI的調用

在url中解析出了module,method,parameter,callbackId,如何調用呢?很顯然,借助runtime,一切都解決了。在swift中,是沒有runtime能力的,需要借助oc,所以這里我們定義的webAPI的類都是繼承于NSObject。

上面我們說過,webAPI的module名和instance一一對應。只要通過module名,可取到webAPI的instance,然后得到方法的函數指針,傳入參數進行調用即可。

取出webAPI instance:

//MARK: get webAPI
func webAPI(_ module: String) -> WebAPIProtocol? {
   let obj = apiDict[module]
   return obj
}

函數調用:

func callNativeMethod(name: String, parameter: [String: AnyObject]?, callback: SLCallback?) {
   let sel = name + ":callback:"
   let seletor = NSSelectorFromString(sel)
   
   guard self.responds(to: seletor) else {
       print("\(self) not responds \(sel)")
       return
   }
   
   let imp = self.method(for: seletor)
   
   if let imp = imp {
     // 定義函數類型
       typealias function = @convention(c) (AnyObject, Selector, [String: AnyObject]?, SLCallback?) -> Void
       
       // 轉換類型
       let call = unsafeBitCast(imp, to: function.self)
       
       // 函數調用
       call(self, seletor, parameter, callback)
   }
   
   if let callback = callback {
       callback(nil)
   }
}

@convention(c),修飾函數類型,它指出了函數調用的約定,聲明這是個c函數調用。

最后附上一張總的調用圖。

bridge.png

End

至此,主要的流程就說完了。下一篇將細說下webAPI的定義,及這套方案如何融合到webView中。

github地址:SLWebBridge

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容