在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的解析及调用了。
需要解决的问题
- webAPI集中管理及通用性
- 以上的方式十分的散乱,不利于管理。如果在其他的页面也要调用到这个webAPi,会需要再copy一遍。所以,我们需要在某个集中的地方处理这些webAPI的调用,并且可以复用,write once,can be called anywhere。
- 要实现通用性,在native实现webAPI的参数应该是固定的,很简单,NSDictionary搞定,(⊙o⊙)…,或许还需要callback,下面会说到。
- web调用的封装性及通用性
- web通过iframe调用oc的方式也需要封装起来,而不是在每个调用的地方都写上一段创建iframe的代码。只需引入一个js文件,调用其中已经封装好的callNative方法就好。
- callNative需要定义一套通用规范,适合所有的场景。
- 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实例,来调用。只不过我觉得注册会使得映射关系比较清晰,并且不依赖于类名。
通用性
- web调用接口的通用性
web调用webAPI只需要按一个固定模式调用,传入相应的参数。上面提到了module的概念,所以web这边调用,只需传入module,method,param,callback就好。这里的callback其实是指的callbackId,下面会说到。
invokeClientMethod: function(module, name, parameters, callback) {}
- native调用js接口通用性
invokeWebMethod: function(callback, params) {}
- webAPI实现的通用性
在native层实现的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这边执行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
- 解析module
module对应起来就是host。
let module = url.host
- 解析method
method对应为path。可通过pathComponent取出。
let pathComponents = url.pathComponents
pathComponents得到的是["/","path"],取pathComponents[1]即为method。
- 解析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")
- 解析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函数调用。
最后附上一张总的调用图。
End
至此,主要的流程就说完了。下一篇将细说下webAPI的定义,及这套方案如何融合到webView中。
github地址:SLWebBridge。