在介紹我實現的Hybrid容器以前,建議先了解一下,經常使用的JavaScript和Native相互通訊的方式到底有多少種?html
建議閱讀一下這篇文章: 從零收拾一個hybrid框架(一)-- 從選擇JS通訊方案開始前端
如下,假設你已對JS和Native通訊方式有了基本的瞭解。web
經常使用的三方庫WebViewJavascriptBridge
,爲了兼容UIWebView,繼續採用了假跳轉攔截Request的方式。其實,你也能夠不用攔截的方式,而是使用WKWebView自身提供的API。數組
以WebViewJavascriptBridge
源碼分析。它加載了本地的ExampleApp.html
文件。ExampleApp.html
加載時,運行了一個js方法:setupWebViewJavascriptBridge(callback)
.bash
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 = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
setupWebViewJavascriptBridge(function(bridge) {
var uniqueId = 1
function log(message, data) {
var log = document.getElementById('log')
var el = document.createElement('div')
el.className = 'logLine'
el.innerHTML = uniqueId++ + '. ' + message + ':<br/>' + JSON.stringify(data)
if (log.children.length) { log.insertBefore(el, log.children[0]) }
else { log.appendChild(el) }
}
bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
log('ObjC called testJavascriptHandler with', data)
var responseData = { 'Javascript Says':'Right back atcha!' }
log('JS responding with', responseData)
responseCallback(responseData)
})
document.body.appendChild(document.createElement('br'))
var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
callbackButton.innerHTML = 'Fire testObjcCallback'
callbackButton.onclick = function(e) {
e.preventDefault()
log('JS calling handler "testObjcCallback"')
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
log('JS got response', response)
})
}
})
複製代碼
剛開始,window
對象上的WebViewJavascriptBridge
和WVJBCallbacks
變量還沒值;而後,定義了WVJBCallbacks
數組,這是一個方法數組,存放了註冊事件的操做。而後,聲明瞭一個iframe
,它的src
是一個假地址(這也是爲何稱呼它叫假跳轉)。簡單的說,html中的iframe
就是打開一個網頁。表現到webView上,就是跳轉了一個新的連接。對於這個假連接,客戶端固然不會作跳轉,而是去注入了JS腳本文件。 JS文件主要用於處理web和native的通訊,是用隊列的方式去接收和發送事件。注入的JS文件作了什麼?拋開一大堆的聲明,能夠看到以下的調用:app
messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(messagingIframe);
registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);
setTimeout(_callWVJBCallbacks, 0);
function _callWVJBCallbacks() {
var callbacks = window.WVJBCallbacks;
delete window.WVJBCallbacks;
for (var i=0; i<callbacks.length; i++) {
callbacks[i](WebViewJavascriptBridge);
}
}
複製代碼
此處又建立了一個iframe
,它的src
是特定格式的:https://__wvjb_queue_message__
。咱們還看到,_callWVJBCallbacks
方法中遍歷了WVJBCallbacks
數組,而且傳遞了WebViewJavascriptBridge
。回到上文看數組中的方法,你會發現,這個時候:交互事件被註冊。再回到客戶端,這裏的src
也不是跳轉連接,而是web和native的事件交互「觸發器」。框架
總結:WebViewJavascriptBridge
會觸發2類假跳轉,第一類用於客戶端向webView注入js,第二類用於web和native事件交互。原理就是跳轉的過程當中攔截了請求,對請求作了特定的處理。ide
本文的主題是不攔截Request,若是真的這麼作,從上文的流程,咱們能夠看出2個問題。函數
解決問題2: Apple提供了JavaScriptCore以後,JS在iOS中的使用如魚得水。Webkit中有一個關鍵的類:WKUserContentController
。看它的介紹:源碼分析
A WKUserContentController object provides a way for JavaScript to post messages to a web view. The user content controller associated with a web view is specified by its web view configuration.
簡單的說,咱們能夠經過註冊事件的方式實現WebView的JS和Native交互。(代碼示例我就不提供了,看API文檔)
解決問題1: Webkit還提供了一個類:WKUserScript
。它只提供一個公有方法,用於注入script文件。有2種可選時機,一種是頁面加載完成,一種是頁面開始加載。
其實到這裏,這個方案的初步輪廓已經完成了。簡單的說就是用WKWebView的API。可是,還有優化的地方。
咱們在WebViewJavascriptBridge
的ExampleApp.html文件中能夠看到,每個交互的事件都須要單獨的註冊。可否用一個事件去處理呢?這是徹底能夠的。咱們知道,事件交互時,會傳遞數據,咱們能夠:把須要調用的方法名做爲參數傳遞給客戶端,客戶端用NSMethodSignature
類生成函數簽名,最後經過runtime去調用對應的方法。
例如:在注入的js文件中,咱們能夠這麼作:
// ...其餘處理
function _on(event, callback) {
//...略
_event_hook_map[event] = callback;
}
function _handleMessageFromApp(message) {
//...略
switch(message) {
case 'event': {//...}
case 'init' : {
var ret = _event_hook_map[xxx];
}
}
}
function _setDefaultEventHandlers() {
_on('sys:init',function(ses){
if (window.RbJSBridge._hasInit) {
console.log('hasInit, no need to init again');
return;
}else{
console.log('init event');
}
window.RbJSBridge._hasInit = true;
// bridge ready
var readyEvent = doc.createEvent('Events');
readyEvent.initEvent('RbJSBridge');
doc.dispatchEvent(readyEvent);
});
}
var doc = document;
_setDefaultEventHandlers();
複製代碼
1.webView加載時,注入js,js會調用_setDefaultEventHandlers();
方法。_event_hook_map
中存放了註冊事件的方法,可是它要等待頁面加載成功才能註冊。
2.當客戶端頁面加載成功後,客戶端在webView的didFinish代理方法中主動調用_handleMessageFromApp()
方法,告訴web開始註冊事件。
當客戶端收到web的交互事件時,咱們須要作的是把方法名「翻譯」成函數。這裏須要一套統一的規則。即咱們規定(根據自身須要,兩端人員制定):調用的全部客戶端方法都只有2個參數,一是字典參數,而是block回調。而後,將其生成方法,以下:
+ (id)ur_performSelectorWithTargetName:(NSString *)targetName selector:(SEL)aSelector withObjects:(NSArray *)objects {
URWebWidgetManager *manager = [URWebWidgetManager shareInstance];
id realInstance = [manager.widgets objectForKey:targetName];
if ([realInstance respondsToSelector:aSelector]) {
NSMethodSignature *signature = [realInstance methodSignatureForSelector:aSelector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:realInstance];
[invocation setSelector:aSelector];
NSUInteger i = 1;
for (id object in objects) {
id tempObject = object;
[invocation setArgument:&tempObject atIndex:++i];
}
[invocation invoke]; //方法被執行
if ([signature methodReturnLength]) {
id data;
[invocation getReturnValue:&data];
return data;
}
}
return nil;
}
複製代碼
這樣,咱們就不須要每次都在html和客戶端中註冊交互事件。而是前端規定好調用的客戶端方法名,客戶端提供對應的實現就行了。
不攔截Request只是一種實現方式,我並無去檢測這麼作會不會比攔截的性能更高。但是,我以爲提供惟一的事件註冊,並把這個工做放在注入的js文件中去作,能夠極大的減小兩端開發人員要作的事情。web端調用客戶端時,提供方法名和參數便可。客戶端只要實現對應的方法名函數便可。 目前,基於這種方案實現的H5容器已經在咱們公司的線上產品中使用。我尚未單獨整理出demo,整理出來後第一時間更新。