本文介紹quick hybrid
框架的核心JSBridge
的實現javascript
因爲在最新版本中,已經沒有考慮iOS7
等低版本,所以在選用方案時沒有采用url scheme
方式,而是直接基於WKWebView
實現前端
具體H5和Native的交互原理能夠參考前文的H5和Native交互原理
java
交互原理圖以下:ios
若是一步一步來分析,最後再看效果,可能會很枯燥,甚至還有點化簡爲繁的樣子。(感受直接看代碼應該是最簡單的,奈何每次寫成文章時都得加一大堆的描述)git
所以,先來看看最終完成後應該是什麼樣的。github
// 調用ui中alert的示例 callHandler({ // 模塊名,本文中的API劃分了模塊 module: 'ui', // 方法名 name: 'alert', // 須要傳遞給native的請求參數 data: { message: 'hello', }, callback: function(res) { /** * 調用後的回調,接收原生傳遞的回調數據 * alert若是成功,能夠點擊後再回調 { // 1成功/0失敗 code: 1, message: '描述', // 數據 data: {}, } */ } });
從頭開始實現一個JSBridge,很容易兩眼一抹黑,無從下手。web
所以咱們須要先從大方向上把功能交互肯定好,而後再開始構建細節,編碼實現json
根據核心架構,規劃須要實現的功能:api
H5橋接對象的設計(JSBridge)數組
原生橋接對象的設計
API的設計
接下來就是JSBridge的實現
最重要的,是先把H5和Native通訊時的幾個全局橋接對象肯定:
JSBridge
,H5端的橋接對象,對象中綁定了接收原生調用的方法_handleMessageFromNative
,以及內部有對回調函數等進行管理webkit.messageHandlers.WKWebViewJavascriptBridge.postMessage
,iOS端的橋接對象,這個方法接收H5的調用prompt
,Android端的橋接對象,爲了方便,直接重寫了WebChromeClient
中的onJsPrompt
// H5端的內部邏輯處理 window.JSBridge = {...} // 接收原生的調用,有回調以及主動調用兩種 JSBridge._handleMessageFromNative = function() {...}
// H5主動調用原生 if (os.ios) { // ios採用 window.webkit.messageHandlers.WKWebViewJavascriptBridge.postMessage(...); } else { window.top.prompt(...); }
H5就依靠這個對象與Native通訊,這裏僅介紹核心的邏輯
JSBridge = { // 本地註冊的方法集合,原生只能主動調用本地註冊的方法 messageHandlers: {}, // 短時間回調函數集合,在原生調用完對應的方法後會自動刪除回收 responseCallbacks: {}, // 長期存在的回調集合,能夠屢次調用 responseCallbacksLongTerm: {}, _handleMessageFromNative: function(messageJSON) { // 內部的處理: /** 若是是回調函數: 若是是短時間回調responseCallbacks中查詢回調id,並執行,執行後自動銷燬 若是是短時間回調responseCallbacksLongTerm中查詢回調id,並執行 */ /** 若是是Native的主動調用: 去本地註冊的方法池messageHandlers中搜索,並執行 */ }, callHandler: function(...) { // 底層分別調用Android或iOS的原生接收方法 // 若是是短時間回調,會將回調添加到responseCallbacks中 // 若是是長期回調,會將回調添加到responseCallbacksLongTerm中 // 省略若干邏輯 ... if (os.ios) { // ios採用 window.webkit.messageHandlers.WKWebViewJavascriptBridge.postMessage(...); } else { window.top.prompt(...); } }, registerHandler: function(handlerName, handler) { // H5在本地註冊可供原生調用的方法 }, ... };
Android中的核心就是JSBridge
,其他都是圍繞這個來的,如下是僞代碼,列舉主要的邏輯
public class JSBridge { // 緩存全部的API模塊(註冊時添加進去) static exposedAPIModlues = new HashMap<>(); static register(String apiModelName, Class<? extends IBridgeImpl> clazz) { // 註冊時會自動尋找全部的框架API模塊,而後添加到緩存exposedAPIModlues,每個模塊中能夠有若干API // 每個模塊都須要實現IBridgeImpl接口 ... } static callAPI(...) { // 首先會解析參數(H5中傳遞的),解析出調用了哪個API,傳遞了些什麼,解析結果包括以下 // port:H5傳遞的回調id,是responseCallbacks或responseCallbacksLongTerm中的key // moduleName:調用的API的模塊名,用來檢索exposedAPIModlues中註冊的模塊 // name:調用的API的方法名,在對於找到的模塊中去查找API // 其餘:包括傳遞的參數等等 // 而後會根據H5的回調端口號,生成一個回調對象(用來回調通知H5) Callback callback = new Callback(port); // 以後,根據解析的參數尋找API方法 // java.lang.reflect.Method; Method method = searchMethodBy(moduleName, name); // 沒有找到方法會回調對於錯誤信息 // 不然執行對於的method,傳遞解析出的參數 // 而且在method內部執行完畢後主動回調給H5對於信息 method.invoke(..., callback); } }
callback
類僞代碼以下:
public class Callback { apply(...) { // 先解析拼裝參數,而後將參數組裝成javascript代碼,參數中包含Callback對於的port值(回調id) ... String js = javascript:JSBridge._handleMessageFromNative(對於的json參數); callJS(js); } callHandler(...) { // 主動調用H5,封裝的參數中再也不是回調id,而是handleName ... callJS(js); } callJS(js) { // 底層經過loadUrl執行 ... webviewContext.loadUrl(js); } }
IBridgeImpl
接口是空的,只是一個抽象定義,如下以某個實現這個接口的API爲例
// 爲了清晰,以ui.alert爲例 public class xxxApi implements IBridgeImpl { // 定義一個註冊的模塊別名,方便查找,譬如ui static RegisterName = "ui"; // 模塊中的某個API,譬如alert public static void alert(..., Callback callback) { // 接下來就是在這個API中實現對於的邏輯 ... // 最後,經過觸發callback通知H5便可 callback.apply(...); } }
最後能夠看到,在webview
中,從新了WebChromeClient
的onJsPrompt
來接收H5的調用
而且在webview
加載時就會調用JSBridge
的register
public class XXXWebChromeClient extends WebChromeClient { @Override public boolean onJsPrompt(..., JsPromptResult result) { // 內部觸發JSBridge.callJava result.confirm(JSBridge.callJava(...)); return true; } }
以上幾個就是Andorid
中JSBridge核心實現,其餘的如長期回調,短時間回調,細節實現等優化不是核心邏輯,就列舉,詳情能夠參考最後的源碼
這裏仍然是OC
實現的,主要參考的marcuswestin/WebViewJavascriptBridge實現
核心仍然是WKWebViewJavascriptBridge
,其他一切都是經過它來分發代理
@implementation WKWebViewJavascriptBridge { // 內部基於一個WebViewJavascriptBridgeBase基類(基類中定義交互方法) WebViewJavascriptBridgeBase *_base; } /** * API */ - (void)callHandler:(NSString *)handlerName data:(id)data { // 主動調用H5的方法 // 底層調用_base的sendData,發送數據給H5 } - (void)registerModuleFrameAPI { // 註冊模塊API,模塊用到了別名代理 [self registerHandlersWithClassName:@"UIApi" moduleName:@"ui"]; // 其中registerHandlersWithClassName就是將模塊示例化註冊到全局中的做用,不贅述 } - (void)excuteMessage:(NSString *)message { // 內部執行API的實現,這裏會解析API解析出來的數據,如 // module.name,port(callbackid)等 ... // 而後底層調用_base的excuteMsg(它內部會根據註冊的API,找到相對應的,而後執行原生功能,最後經過回調通知H5) } #pragma mark - WKScriptMessageHandler其實就是一個遵循的協議,它能讓網頁經過JS把消息發送給OC - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { // 監聽到對於API調用時,底層會調用excuteMessage if ([message.name isEqualToString:@"WKWebViewJavascriptBridge"]) { [self excuteMessage:message.body]; } }
而後看看它基類WebViewJavascriptBridgeBase
的實現
@implementation WebViewJavascriptBridgeBase - (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName { // 底層將接收到的數據組裝成js代碼執行 ... NSString* javascriptCommand = [NSString stringWithFormat:@"JSBridge._handleMessageFromNative('%@');", messageJSON]; [_webView evaluateJavaScript:javascriptCommand completionHandler:nil]; } - (void)excuteMsg:(NSString *)messageQueueString moduleName:(NSString *)moduleName { // 底層根據對於的模塊,API名,找到註冊的handler ... // 而後建立一個回調對象 WVJBResponseCallback responseCallback = (經過sendData通知H5回調數據); // 而後執行這個handler handler(message[@"data"], responseCallback); }
接下來是API的定義
定義API模塊以前,須要先了解RegisterBaseClass
,全部模塊必須實現的基類,定義瞭如何註冊
@implementation RegisterBaseClass #pragma mark - 註冊api的統一方法 - (void)registerHandlers { // 子類重寫改方法實現自定義API註冊 } #pragma mark - handler存取 - (void)registerHandlerName:(NSString *)handleName handler:(WVJBHandler)handler { // 註冊某個模塊下的某個API } - (WVJBHandler)handler:(NSString *)handlerName { // 經過名稱獲取對應的API }
要定義一個API模塊,則需繼承RegisterBaseClass
而後重寫registerHandlers
(爲了清晰,以ui.alert爲例)
@implementation UIApi - (void)registerHandlers { [self registerHandlerName:@"alert" handler:^(id data, WVJBResponseCallback responseCallback) { // 一樣,在接收到數據,並處理後,經過responseCallback通知H5 ... responseCallback(...); } }
在webview
加載時就會調用WKWebViewJavascriptBridge
的registerModuleFrameAPI
,對於模塊名ui
與別名UIApi
,能夠在註冊時看到,它們之間是有一一對應關係的
而後在webview建立時,會進行監聽,userContentController
WKWebViewConfiguration * webConfig = [[WKWebViewConfiguration alloc] init]; WKUserContentController * userContentVC = [[WKUserContentController alloc] init]; webConfig.userContentController = userContentVC; WKWebView * wk = [[WKWebView alloc] initWithFrame: CGRectZero configuration: webConfig]; self.wv = wk; ... // 代理 self.bridge = [WKWebViewJavascriptBridge bridgeForWebView: self.wv]; [self.bridge setWebViewDelegate: self]; // 添加供js調用oc的橋樑。這裏的name對應WKScriptMessage中的name,多數狀況下咱們認爲它就是方法名。 [self.wv.configuration.userContentController addScriptMessageHandler: self.bridge name: @"WKWebViewJavascriptBridge"];
一樣,iOS中的長期回調等其它一些非核心內容也暫時隱藏了
按照上述的實現,能夠構建出一個完整的JSBridge交互流程,H5和Native的交互已經通了
接下來就是設計API真正給外界調用
準確的來講,API的設計已經脫離了JSBridge交互內容,屬於混合框架框架應用層次,所以後續會有單獨的章節介紹quick hybrid
中的API
API如何實現?能夠參考上文中Android的繼承IBridgeImpl
法以及iOS的繼承RegisterBaseClass
而後重寫registerHandlers
至於該規劃些什麼API,這與實際的需求有關,不過通常狀況下,像ui.alert
等等通常都是必須的
更多詳情請待後續章節
最後再來一張圖鞏固下把
至此,整個JSBridge交互就已經完成了
其實在總結文章時,考慮過不少種形式,發現,
若是是全文字描述,十分枯燥,很難堅持讀下來,
若是是各類原理都用繪圖+描述,發現會化簡爲繁,硬生生把難度提升了幾個level,
因此最終採用的是僞代碼(半僞半真)展現形式(剔除一些無效信息,提取關鍵,並且還不和最終的代碼衝突)
雖說,這整套流程都沒有特別難的地方,涉及的知識點都不是特別深。可是卻包含了前端,Android,iOS三個領域。
所以若是要將整套工做作的比較好的化最好仍是有分工的好,比較一我的的精力有限,真正專精多個領域的人仍是比較少的,
並且後續各個優化的內容也很多(API,優化,等等...)
github
上這個框架的實現