前言
本文介紹quick hybrid
框架的核心JSBridge
的實現
由於在最新版本中,已經沒有考慮iOS7
等低版本,因此在選用方案時沒有采用url scheme
方式,而是直接基於WKWebView
實現
互動原理
具體H5和Native的互動原理可以參考前文的H5和Native互動原理
互動原理圖如下:
預計的最終效果
如果一步一步來分析,最後再看效果,可能會很枯燥,甚至還有點化簡為繁的樣子。(感覺直接看程式碼應該是最簡單的,奈何每次寫成文章時都得加一大堆的描述)
因此,先來看看最終完成後應該是什麼樣的。
// 呼叫ui中alert的示例
callHandler({
// 模組名,本文中的API劃分了模組
module: 'ui',
// 方法名
name: 'alert',
// 需要傳遞給native的請求引數
data: {
message: 'hello',
},
callback: function(res) {
/**
* 呼叫後的回撥,接收原生傳遞的回撥資料
* alert如果成功,可以點選後再回撥
{
// 1成功/0失敗
code: 1,
message: '描述',
// 資料
data: {},
}
*/
}
});
複製程式碼
架構
從頭開始實現一個JSBridge,很容易兩眼一抹黑,無從下手。
因此我們需要先從大方向上把功能互動確定好,然後再開始構建細節,編碼實現
功能分析與確認
根據核心架構,規劃需要實現的功能:
-
H5橋接物件的設計(JSBridge)
-
短期回撥池,需自動回收
-
長期回撥池,可多次使用
-
呼叫Native方法的通道,橋接物件上原生註冊的接收方法
-
接收Native呼叫的通道,橋接物件上H5註冊的接收方法
-
H5可以註冊主動給原生呼叫的方法
-
-
原生橋接物件的設計
-
長期方法池,每一個長期呼叫都會儲存在回撥池中,可以多次使用
-
短期立即執行,每一個短期呼叫都是立即執行
-
呼叫H5方法的通道,橋接物件上H5註冊的接收方法
-
接收H5呼叫的通道,橋接物件上原生註冊的接收方法,底層自動解析,然後執行對應API
-
回撥物件,底層基於呼叫H5的通道,每次執行完畢後都通過回撥物件回撥給H5
-
主動呼叫H5,不同於回撥物件只能被動響應,這個可以主動呼叫H5中註冊的方法
-
-
API的設計
-
H5中的API,供前端呼叫,底層通過呼叫Native方法的通道,然後將預處理後的引數傳送給原生
-
Native中的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(...);
}
複製程式碼
JSBridge物件的實現
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中橋接物件的實現
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核心實現,其他的如長期回撥,短期回撥,細節實現等優化不是核心邏輯,就列舉,詳情可以參考最後的原始碼
iOS中橋接物件的實現
這裡仍然是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中的長期回撥等其它一些非核心內容也暫時隱藏了
API的設計
按照上述的實現,可以構建出一個完整的JSBridge互動流程,H5和Native的互動已經通了
接下來就是設計API真正給外界呼叫
準確的來說,API的設計已經脫離了JSBridge互動內容,屬於混合框架框架應用層次,因此後續會有單獨的章節介紹quick hybrid
中的API
API如何實現?可以參考上文中Android的繼承IBridgeImpl
法以及iOS的繼承RegisterBaseClass
然後重寫registerHandlers
至於該規劃些什麼API,這與實際的需求有關,不過一般情況下,像ui.alert
等等一般都是必須的
更多詳情請待後續章節
結束語
最後再來一張圖鞏固下把
至此,整個JSBridge互動就已經完成了
其實在總結文章時,考慮過很多種形式,發現, 如果是全文字描述,十分枯燥,很難堅持讀下來, 如果是各種原理都用繪圖+描述,發現會化簡為繁,硬生生把難度提高了幾個level, 所以最終採用的是虛擬碼(半偽半真)展示形式(剔除一些無效資訊,提取關鍵,而且還不和最終的程式碼衝突)
雖然說,這整套流程都沒有特別難的地方,涉及的知識點都不是特別深。但是卻包含了前端,Android,iOS三個領域。 因此如果要將整套工作做的比較好的化最好還是有分工的好,比較一個人的精力有限,真正專精多個領域的人還是比較少的, 而且後續各個優化的內容也不少(API,優化,等等...)
返回根目錄
原始碼
github
上這個框架的實現