summer的ios小记iOS Developer

如何设计通用WebAPI之Swift实现(一)

2017-03-26  本文已影响97人  微微笑的蜗牛

在App开发中,Web和Native的协作可谓是密不可分,因为web能快速迭代,更新,试错。在这之中,除了纯展示的页面,其他的几乎都会涉及到与Native的通信,调用native的功能实现业务需求。

常用做法

业界常用的做法就是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来触发方法
  // ...
 }
}

对,这样就调起了登录页。但是如果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集中管理及通用性
  1. web调用的封装性及通用性
  1. callNative的回调
    有时候,js在调用native方法后,会等待native的结果返回后,执行自己的处理。那么如何在oc中回调到js?

其实2,3在WebViewJavascriptBridge中都有实现,不过我还是想用swift给实现一遍。

解决方案

webAPI集中管理

在yy中,webAPI非常多。根据单一职责,需要单独的类来管理相关的接口,每个webAPI类会对应一个module。举个栗子,DataWebAPI,module=data,专门处理跟数据相关的东西,比如获取userId,获取appVersion等;UIWebAPI,module=ui,处理UI相关的,如跳转登录页,直播间,设置活动条frame等。

有了这些WebAPI,需要管理起来。这里采用注册的方式,将module与webAPI instance以key-value方式存储起来。在需要调用某个api时,找到对应module的WebAPI实例,进行分发。

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

当然,不注册也行,根据web传过来的module=UIWebAPI,利用runtime生成UIWebAPI实例,来调用。只不过我觉得注册会使得映射关系比较清晰,并且不依赖于类名。


webAPIManager.png
通用性
invokeClientMethod: function(module, name, parameters, callback) {}
invokeWebMethod: function(callback, params) {}
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这边执行js function。因为没法把callback通过url的方式,传给native。所以做法是生成全局callbackId,将callbackId与function映射起来。然后传给native,native会生成一个block,参数为NSDictionary。执行完webAPI,通过invokeWebMethod回调js方法的时候,再把callbackId传回来,js这边找到对应的function执行。

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!)
          
          // 执行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的规范定义是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执行完后,将要返回给js的参数传入callback,再执行。

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

上一篇 下一篇

猜你喜欢

热点阅读