在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的解析及調用了。
需要解決的問題
- webAPI集中管理及通用性
- 以上的方式十分的散亂,不利于管理。如果在其他的頁面也要調用到這個webAPi,會需要再copy一遍。所以,我們需要在某個集中的地方處理這些webAPI的調用,并且可以復用,write once,can be called anywhere。
- 要實現(xiàn)通用性,在native實現(xiàn)webAPI的參數應該是固定的,很簡單,NSDictionary搞定,(⊙o⊙)…,或許還需要callback,下面會說到。
- web調用的封裝性及通用性
- web通過iframe調用oc的方式也需要封裝起來,而不是在每個調用的地方都寫上一段創(chuàng)建iframe的代碼。只需引入一個js文件,調用其中已經封裝好的callNative方法就好。
- callNative需要定義一套通用規(guī)范,適合所有的場景。
- 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實例,來調用。只不過我覺得注冊會使得映射關系比較清晰,并且不依賴于類名。

通用性
- 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
- 解析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執(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函數調用。
最后附上一張總的調用圖。

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