JS Bridge 通信原理
前言
上一篇介紹了移動(dòng)端開(kāi)發(fā)的相關(guān)技術(shù),這一篇主要是從 Hybrid 開(kāi)發(fā)的 JS Bridge 通信講起。
顧名思義,JS Bridge 的意思就是橋,也就是連接 JS 和 Native 的橋梁,它也是 Hybrid App 里面的核心。一般分為 JS 調(diào)用 Native 和 Native 主動(dòng)調(diào)用 JS 兩種形式。
URL Scheme
URL Scheme 是一種特殊的 URL,一般用于在 Web 端喚醒 App,甚至跳轉(zhuǎn)到 App 的某個(gè)頁(yè)面,比如在某個(gè)手機(jī)網(wǎng)站上付款的時(shí)候,可以直接拉起支付寶支付頁(yè)面。
這里有個(gè)小例子,你可以在瀏覽器里面直接輸入 weixin://,系統(tǒng)就會(huì)提示你是否要打開(kāi)微信。輸入 mqq:// 就會(huì)幫你喚起手機(jī) QQ。

這里有個(gè)常用 App URL Scheme 匯總:URL Schemes 收集整理
在手機(jī)里面打開(kāi)這個(gè)頁(yè)面后點(diǎn)擊這里,就會(huì)提示你是否要打開(kāi)微信。
我們常說(shuō)的 Deeplink 一般也是基于 URL Scheme 來(lái)實(shí)現(xiàn)的。一個(gè) URI 的組成結(jié)構(gòu)如下:

URI?=?scheme:[//authority]path[?query][#fragment]
//?scheme?????=?http
//?authority??=?www.baidu.com
//?path???????=?/link
//?query??????=?url=xxxxx
authority?=?[userinfo@]host[:port]
除了 http/https 這兩個(gè)常見(jiàn)的協(xié)議,還可以自定義協(xié)議。借用維基百科的一張圖:

通常情況下,App 安裝后會(huì)在手機(jī)系統(tǒng)上注冊(cè)一個(gè) Scheme,比如 weixin:// 這種,所以我們?cè)谑謾C(jī)瀏覽器里面訪問(wèn)這個(gè) scheme 地址,系統(tǒng)就會(huì)喚起我們的 App。
一般在 Android 里面需要到 AndroidManifest.xml 文件中去注冊(cè) Scheme:
????android:name=".login.dispatch.DispatchActivity"
????android:launchMode="singleTask"
????android:theme="@style/AppDispatchTheme">
????
????????"android.intent.action.VIEW"?/>
????????"android.intent.category.DEFAULT"?/>
????????"android.intent.category.BROWSABLE"?/>
????????"taobao"?/>
????????"xxx"?/>
????????"/goods"?/>
????
在 iOS 中需要在 Xcode 里面注冊(cè),有一些已經(jīng)是系統(tǒng)使用的不應(yīng)該使用,比如 Maps、YouTube、Music。
具體可以參考蘋果開(kāi)發(fā)者官網(wǎng)文檔:Defining a Custom URL Scheme for Your App

JS 調(diào)用 Native
在 iOS 里面又需要區(qū)分 UIWebView 和 WKWebView 兩種 WebView:

WKWebView 是 iOS8 之后出現(xiàn)的,目的是取代笨重的 UIWebView,它占用內(nèi)存更少,大概是 UIWebView 的 1/3,支持更好的 HTML5 特性,性能更加強(qiáng)大。
但也有一些缺點(diǎn),比如不支持緩存,需要自己注入 Cookie,發(fā)送 POST 請(qǐng)求的時(shí)候帶不了參數(shù),攔截 POST 請(qǐng)求的時(shí)候無(wú)法解析參數(shù)等等。
「題外話(小小吐槽一下)」
我們這邊有很多場(chǎng)景是需要客戶端和銀行交互的,為了從銀行 H5 回調(diào)到我們的客戶端,常常是會(huì)讓銀行跳到我們的 H5 中間頁(yè),客戶端去攔截并解析銀行帶來(lái)的參數(shù)。
以前有些頁(yè)面是 Python 服務(wù)端渲染做的,支持 POST 請(qǐng)求打開(kāi)頁(yè)面。后來(lái) iOS 客戶端升級(jí) ?WKWebView,導(dǎo)致無(wú)法獲取銀行 POST 帶過(guò)來(lái)的參數(shù)。
為了配合客戶端發(fā)版上線,曾經(jīng)連夜修改了我們 App 里面所有相關(guān)的綁卡和支付 H5 頁(yè)面,還好沒(méi)有搞出重大線上事故。
JS 調(diào)用 Native 通信大致有三種方法:
攔截 Scheme 彈窗攔截 注入 JS 上下文
這三種方式總體上各有利弊,下面會(huì)一一介紹。
攔截 Scheme
仔細(xì)思考一下,如果是 JS 和 Java 之間傳遞數(shù)據(jù),我們?cè)撛趺醋瞿兀?/p>
對(duì)于前端開(kāi)發(fā)來(lái)說(shuō),調(diào) Ajax 請(qǐng)求接口是最常見(jiàn)的需求了。不管對(duì)方是 Java 還是 Python,我們都可以通過(guò) http/https 接口來(lái)獲取數(shù)據(jù)。實(shí)際上這個(gè)流程和 JSONP 更加類似。
已知客戶端是可以攔截請(qǐng)求的,那么可不可以在這個(gè)上面做文章呢?
如果我們請(qǐng)求一個(gè)不存在的地址,上面帶了一些參數(shù),通過(guò)參數(shù)告訴客戶端我們需要調(diào)用的功能呢?
比如我要調(diào)用掃碼功能:
axios.get('http://xxxx?func=scan&callback_id=yyyy')
客戶端可以攔截這個(gè)請(qǐng)求,去解析參數(shù)上面的 func 來(lái)判斷當(dāng)前需要調(diào)起哪個(gè)功能。客戶端調(diào)起掃碼功能之后,會(huì)獲取 WebView 上面的 callbacks 對(duì)象,根據(jù) callback_id 回調(diào)它。
所以基于上面的例子,我們可以把域名和路徑當(dāng)做通信標(biāo)識(shí),參數(shù)里面的 func 當(dāng)做指令,callback_id 當(dāng)做回調(diào)函數(shù),其他參數(shù)當(dāng)做數(shù)據(jù)傳遞。對(duì)于不滿足條件的 http 請(qǐng)求不應(yīng)該攔截。
當(dāng)然了,現(xiàn)在主流的方式是前面我們看到的自定義 Scheme 協(xié)議,以這個(gè)為通信標(biāo)識(shí),域名和路徑當(dāng)做指令。
這種方式的好處就是 iOS6 以前只支持這種方式,兼容性比較好。
JS 端
我們有很多種方法可以發(fā)起請(qǐng)求,目前使用最廣泛的是 iframe 跳轉(zhuǎn):
使用 a 標(biāo)簽跳轉(zhuǎn)
"taobao://">點(diǎn)擊我打開(kāi)淘寶
重定向
location.href?=?"taobao://"
iframe 跳轉(zhuǎn)
const?iframe?=?document.createElement("iframe");
iframe.src?=?"taobao://"
iframe.style.display?=?"none"
document.body.appendChild(iframe)
Android 端
在 Android 側(cè)可以用 shouldOverrideUrlLoading 來(lái)攔截 url 請(qǐng)求。
@Override
public?boolean?shouldOverrideUrlLoading(WebView?view,?String?url)?{
????if?(url.startsWith("taobao"))?{
????????//?拿到調(diào)用路徑后解析調(diào)用的指令和參數(shù),根據(jù)這些去調(diào)用?Native?方法
????????return?true;
????}
}
iOS 端
在 iOS 側(cè)需要區(qū)分 UIWebView 和 WKWebView 兩種方式。在 UIWebView 中:
-?(BOOL)shouldStartLoadWithRequest:(NSURLRequest?*)request
????????????????????navigationType:(BPWebViewNavigationType)navigationType
{
????if?(xxx)?{
????????//?拿到調(diào)用路徑后解析調(diào)用的指令和參數(shù),根據(jù)這些去調(diào)用?Native?方法
????????return?NO;
????}
????return?[super?shouldStartLoadWithRequest:request?navigationType:navigationType];
}
在 WKWebView 中:
-?(void)webView:(WKWebView?*)webView?decidePolicyForNavigationAction:(nonnull?WKNavigationAction?*)navigationAction?decisionHandler:(nonnull?void?(^)(WKNavigationActionPolicy))decisionHandler
{
????if(xxx)?{
????????//?拿到調(diào)用路徑后解析調(diào)用的指令和參數(shù),根據(jù)這些去調(diào)用?Native?方法
????????BLOCK_EXEC(decisionHandler,?WKNavigationActionPolicyCancel);
????}?else?{
????????BLOCK_EXEC(decisionHandler,?WKNavigationActionPolicyAllow);
????}
????[self.webView.URLLoader?webView:webView?decidedPolicy:policy?forNavigationAction:navigationAction];
}
目前不建議只使用攔截 URL Scheme 解析參數(shù)的形式,主要存在幾個(gè)問(wèn)題。
連續(xù)續(xù)調(diào)用 location.href會(huì)出現(xiàn)消息丟失,因?yàn)?WebView 限制了連續(xù)跳轉(zhuǎn),會(huì)過(guò)濾掉后續(xù)的請(qǐng)求。URL 會(huì)有長(zhǎng)度限制,一旦過(guò)長(zhǎng)就會(huì)出現(xiàn)信息丟失 因此,類似 WebViewJavaScriptBridge 這類庫(kù),就結(jié)合了注入 API 的形式一起使用,這也是我們這邊目前使用的方式,后面會(huì)介紹一下。
彈窗攔截
Android 實(shí)現(xiàn)
這種方式是利用彈窗會(huì)觸發(fā) WebView 相應(yīng)事件來(lái)攔截的。
一般是在 setWebChromeClient 里面的 onJsAlert、onJsConfirm、onJsPrompt 方法攔截并解析他們傳來(lái)的消息。
//?攔截?Prompt
@Override
public?boolean?onJsPrompt(WebView?view,?String?url,?String?message,?String?defaultValue,?JsPromptResult?result)?{
???????if?(xxx)?{
????????//?解析?message?的值,調(diào)用對(duì)應(yīng)方法
???????}
???????return?super.onJsPrompt(view,?url,?message,?defaultValue,?result);
???}
//?攔截?Confirm
@Override
public?boolean?onJsConfirm(WebView?view,?String?url,?String?message,?JsResult?result)?{
???????return?super.onJsConfirm(view,?url,?message,?result);
???}
//?攔截?Alert
@Override
public?boolean?onJsAlert(WebView?view,?String?url,?String?message,?JsResult?result)?{
???????return?super.onJsAlert(view,?url,?message,?result);
???}
iOS 實(shí)現(xiàn)
我們以 WKWebView 為例:
+?(void)webViewRunJavaScriptTextInputPanelWithPrompt:(NSString?*)prompt
????defaultText:(NSString?*)defaultText
????completionHandler:(void?(^)(NSString?*?_Nullable))completionHandler
{
????/**?Triggered?by?JS:
????var?person?=?prompt("Please?enter?your?name",?"Harry?Potter");
????if?(person?==?null?||?person?==?"")?{
???????txt?=?"User?cancelled?the?prompt.";
????}?else?{
???????txt?=?"Hello?"?+?person?+?"!?How?are?you?today?";
????}
????*/
????if?(xxx)?{
????????BLOCK_EXEC(completionHandler,?text);
????}?else?{
????????BLOCK_EXEC(completionHandler,?nil);
????}
?}
這種方式的缺點(diǎn)就是在 iOS 上面 UIWebView 不支持,雖然 WKWebView 支持,但它又有更好的 scriptMessageHandler,比較尷尬。
注入上下文
前面我們有講過(guò)在 iOS 中內(nèi)置了 JavaScriptCore 這個(gè)框架,可以實(shí)現(xiàn)執(zhí)行 JS 以及注入 Native 對(duì)象等功能。
這種方式不依賴攔截,主要是通過(guò) WebView 向 JS 的上下文注入對(duì)象和方法,可以讓 JS 直接調(diào)用原生。
PS:iOS 中的 Block 是 OC 對(duì)于閉包的實(shí)現(xiàn),它本質(zhì)上是個(gè)對(duì)象,定義 JS 里面的函數(shù)。
iOS UIWebView
iOS 側(cè)代碼:
//?獲取?JS?上下文
JSContext?*context?=?[webview?valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//?注入?Block
context[@"callHandler"]?=?^(JSValue?*?data)?{
????//?處理調(diào)用方法和參數(shù)
????//?調(diào)用?Native?功能
????//?回調(diào)?JS?Callback
}
JS 代碼:
window.callHandler(JSON.stringify({
????type:?"scan",
????data:?"",
????callback:?function(data)?{
????}
}));
這種方式的牛逼之處在于,JS 調(diào)用是同步的,可以立馬拿到返回值。
我們也不再需要像攔截方式一樣,每次傳值都要把對(duì)象做 JSON.stringify,可以直接傳 JSON 過(guò)去,也支持直接傳一個(gè)函數(shù)過(guò)去。
iOS WKWebView
WKWebView 里面通過(guò) addScriptMessageHandler 來(lái)注入對(duì)象到 JS 上下文,可以在 WebView 銷毀的時(shí)候調(diào)用 removeScriptMessageHandler 來(lái)銷毀這個(gè)對(duì)象。
前端調(diào)用注入的原生方法之后,可以通過(guò) didReceiveScriptMessage 來(lái)接收前端傳過(guò)來(lái)的參數(shù)。
WKWebView?*wkWebView?=?[[WKWebView?alloc]?init];
WKWebViewConfiguration?*configuration?=?wkWebView.configuration;
WKUserContentController?*userCC?=?configuration.userContentController;
//?注入對(duì)象
[userCC?addScriptMessageHandler:self?name:@"nativeObj"];
//?清除對(duì)象
[userCC?removeScriptMessageHandler:self?name:@"nativeObj"];
//?客戶端處理前端調(diào)用
-?(void)userContentController:(WKUserContentController?*)userContentController?didReceiveScriptMessage:(WKScriptMessage?*)message
{
????//?獲取前端傳來(lái)的參數(shù)
????NSDictionary?*msgBody?=?message.body;
????//?如果是?nativeObj?就進(jìn)行相應(yīng)處理
????if?(![message.name?isEqualToString:@"nativeObj"])?{
????????//?
????????return;
????}
}
使用 addScriptMessageHandler 注入的對(duì)象實(shí)際上只有一個(gè) postMessage 方法,無(wú)法調(diào)用更多自定義方法。前端的調(diào)用方式如下:
window.webkit.messageHandlers.nativeObj.postMessage(data);
需要注意的是,這種方式要求 iOS8 及以上,而且返回不是同步的。和 UIWebView 一樣的是,也支持直接傳 JSON 對(duì)象,不需要 stringify。
Android addJavascriptInterface
安卓4.2之前注入 JS 一般是使用 addJavascriptInterface ,和前面的 addScriptMessageHandler 有一些類似,但又沒(méi)有它的限制。
public?void?addJavascriptInterface()?{
????????mWebView.addJavascriptInterface(new?DatePickerJSBridge(),?"DatePickerBridge");
????}
private?class?PickerJSBridge?{
????public?void?_pick(...)?{
????}
}
在 JS 里面調(diào)用:
window.DatePickerBridge._pick(...)
但這種方案有一定風(fēng)險(xiǎn),可以參考這篇文章:WebView中接口隱患與手機(jī)掛馬利用
在 Android4.2 之后提供了 @JavascriptInterface 注解,暴露給 JS 的方法必須要帶上這個(gè)。
所以前面的 _pick 方法需要帶上這個(gè)注解。
private?class?PickerJSBridge?{
????@JavascriptInterface
????public?void?_pick(...)?{
????}
}
Native 調(diào)用 JS
Native 調(diào)用 JS 一般就是直接 JS 代碼字符串,有些類似我們調(diào)用 JS 中的 eval 去執(zhí)行一串代碼。一般有 loadUrl、evaluateJavascript 等幾種方法,這里逐一介紹。
但是不管哪種方式,客戶端都只能拿到掛載到 window 對(duì)象上面的屬性和方法。
Android
在 Android 里面需要區(qū)分版本,在安卓4.4之前的版本支持 loadUrl,使用方式類似我們?cè)?a 標(biāo)簽的 href 里面寫 JS 腳本一樣,都是javascript:xxx 的形式。
這種方式無(wú)法直接獲取返回值。
webView.loadUrl("javascript:foo()")
在安卓4.4以上的版本一般使用 evaluateJavascript 這個(gè) API 來(lái)調(diào)用。這里需要判斷一下版本。
if?(Build.VERSION.SDK_INT?>?19)?//see?what?wrapper?we?have
{
????webView.evaluateJavascript("javascript:foo()",?null);
}?else?{
????webView.loadUrl("javascript:foo()");
}
UIWebView
在 iOS 的 UIWebView 里面使用 stringByEvaluatingJavaScriptFromString 來(lái)調(diào)用 JS 代碼。這種方式是同步的,會(huì)阻塞線程。
results?=?[self.webView?stringByEvaluatingJavaScriptFromString:"foo()"];
WKWebView
WKWebView 可以使用 evaluateJavaScript 方法來(lái)調(diào)用 JS 代碼。
[self.webView?evaluateJavaScript:@"document.body.offsetHeight;"?completionHandler:^(id?_Nullable?response,?NSError?*?_Nullable?error)?{
????//?獲取返回值?response
????}];
JS Bridge 設(shè)計(jì)
前面講完了 JS 和 Native 互調(diào)的所有方法,這里來(lái)介紹一下我們這邊 JS Bridge 的設(shè)計(jì)吧。
我們這邊的 JS Bridge 通信是基于 WebViewJavascriptBridge 這個(gè)庫(kù)來(lái)實(shí)現(xiàn)的。
主要是結(jié)合 Scheme 協(xié)議+上下文注入來(lái)做??紤]到 Android 和 iOS 不一樣的通信方式,這里進(jìn)行了封裝,保證提供給外部的 API 一致。
具體功能的調(diào)用我們封裝成了 npm 包,下面的是幾個(gè)基礎(chǔ) API:
callHandler(name, params, callback):這個(gè)是調(diào)用 Native 功能的方法,傳模塊名、參數(shù)、回調(diào)函數(shù)給 Native。 hasHandler(name):這個(gè)是檢查客戶端是否支持某個(gè)功能的調(diào)用。 registerHandler(name):這個(gè)是提前注冊(cè)一個(gè)函數(shù),等待 Native 回調(diào),比如 pageDidBack這種場(chǎng)景。
那么這幾個(gè) API 又是如何實(shí)現(xiàn)的呢?這里 Android 和 iOS 封裝不一致,應(yīng)當(dāng)分開(kāi)來(lái)說(shuō)。
Android Bridge
前面我們有說(shuō)過(guò)安卓可以通過(guò) @JavascriptInterface 注解來(lái)將對(duì)象和方法暴露給 JS。
所以這里的幾個(gè)方法都是通過(guò)注解暴露給 JS 來(lái)調(diào)用的,在 JS 層面做了一些兼容處理。
hasHandler
首先最簡(jiǎn)單的是這個(gè) hasHandler,就是在客戶端里面維護(hù)一張表(其實(shí)我們是寫死的),里面有支持的 Bridge 模塊信息,只需要用 switch...case 判斷一下就行了。
@JavascriptInterface
public?boolean?hasHandler(String?cmd)?{
????????switch?(cmd)?{
????????????case?xxx:
????????????case?yyy:
????????????case?zzz:
????????????????return?true;
????????}
????????return?false;
????}
callHandler
然后我們來(lái)看 callHandler 這個(gè)方法,它是提供 JS 調(diào)用 Native 功能的方法。在調(diào)用這個(gè)方法之前,我們一般需要先判斷一下 Native 是否支持這個(gè)功能。
function?callHandler(name,?params,?callback)?{
????if?(!window.WebViewJavascriptBridge.hasHandler(name))?{
????}
}
如果 Native 沒(méi)有支持這個(gè) Bridge,我們就需要對(duì)回調(diào)進(jìn)行兼容性處理。這個(gè)兼容性處理包括兩個(gè)方面,一個(gè)是功能方面,一個(gè)是 callback 的默認(rèn)回參。
比如我們調(diào)用 Native 的彈窗功能,如果客戶端沒(méi)支持這個(gè) Bridge,或者我們是在瀏覽器里面打開(kāi)的這個(gè)頁(yè)面,此時(shí)應(yīng)該降級(jí)到使用 Web 的 alert 彈窗。
對(duì)于 callback,我們可以默認(rèn)給傳個(gè) 0,表示當(dāng)前不支持這個(gè)功能。
假設(shè)這個(gè) alert 的 bridge 接收兩個(gè)參數(shù),分別是 title 和 content,那么此時(shí)就應(yīng)該使用瀏覽器自帶的 alert 展示出來(lái)。
function?fallback(params,?callback)?{
????let?content?=?`${params.title}\n{params.content}`
????window.alert(content);
????callback?&&?callback(0)
}
這個(gè) fallback 函數(shù)我們希望能夠更加通用,每個(gè)調(diào)用方法都應(yīng)該有自己的 fallback 函數(shù),所以前面的 callHandler 應(yīng)該設(shè)計(jì)成這樣:
function?callHandler(name,?params,?fallback)?{
????return?function(...rest,?callback)?{
????????const?paramsList?=?{};
????????for?(let?i?=?0;?i?????????????paramsList[params]?=?rest[i];
????????}
????????if?(!callback)?{
????????????callback?=?function(result)?{};
????????}
????????if?(fallback?&&?!window.WebViewJavascriptBridge.hasHandler(name)))?{
????????????fallback(paramsList,?callback);
????????}?else?{
????????????window.WebViewJavascriptBridge.callHandler(name,?params,?callback);
????????}
????}
}
我們可以基于這個(gè)函數(shù)封裝一些功能方法,比如前面的 alert:
function?fallback(params,?callback)?{
????let?content?=?`${params.title}\n{params.content}`
????window.alert(content);
????callback?&&?callback(0)
}
function?alert(
??title,
??content,
??cb:?any
)?{
??return?callHandler(
????'alert',
????['title',?'content'],
????fallback
??)(title,?content,?cb);
}
alert(`this?is?title`,?`hahaha`,?function()?{
????console.log('success')
})
具體效果類似下面這種,這是從 Google 上隨便找的一張圖(侵刪):

那么客戶端又如何實(shí)現(xiàn)回調(diào) callback 函數(shù)的呢?前面說(shuō)過(guò),客戶端想調(diào)用 JS 方法,只能調(diào)用掛載到 window 對(duì)象上面的。
因此,這里使用了一種很巧妙的方法,實(shí)際上 callback 函數(shù)依然是 JS 執(zhí)行的。
在調(diào)用 Native 之前,我們可以先將 callback 函數(shù)和一個(gè) uniqueId 映射起來(lái),然后存在 JS 本地。我們只需要將 callbackId 傳給 Native 就行了。
function?callHandler(name,?data,?callback)?{
????const?id?=?`cb_${uniqueId++}_${new?Date().getTime()}`;
????callbacks[id]?=?callback;
????window.bridge.send(name,?JSON.stringify(data),?callbackId)
}
在客戶端這里,當(dāng) send 方法接收到參數(shù)之后,會(huì)執(zhí)行相應(yīng)功能,然后使用 webView.loadUrl 主動(dòng)調(diào)用前端的一個(gè)接收函數(shù)。
@JavascriptInterface
public?void?send(final?String?cmd,?String?data,?final?String?callbackId)?{
????//?獲取數(shù)據(jù),根據(jù)?cmd?來(lái)調(diào)用對(duì)應(yīng)功能
????//?調(diào)用結(jié)束后,回調(diào)前端?callback
????String?js?=?String.format("javascript:?window.bridge.onReceive(\'%1$s\',?\'%2$s\');",?callbackId,?result.toDataString());
????webView.loadUrl(js);
}
所以 JS 需要事前定義好這個(gè) onReceive 方法,它接收一個(gè) callbackId 和一個(gè) result。
window.bridge.onReceive?=?function(callbackId,?result)?{
????let?params?=?{};
????try?{
????????params?=?JSON.parse(result)
????}?catch?(err)?{
????????//
????}
????if?(callbackId)?{
????????const?callback?=?callbacks[callbackId];
????????callback(params)
????????delete?callbacks[callbackId];
????}
}
大致流程如下:

registerHandler
注冊(cè)的流程比較簡(jiǎn)單,也是我們把 callback 函數(shù)事先存到一個(gè) messageHandler 對(duì)象里面,不過(guò)這次的 key 不再是一個(gè)隨機(jī)的 id,而是 name。
function?registerHandler(handlerName,?callback)?{
????if?(!messageHandlers[handlerName])?{
??????messageHandlers[handlerName]?=?[handler];
????}?else?{
??????//?支持注冊(cè)多個(gè)?handler
??????messageHandlers[handlerName].push(handler);
????}
}
//?檢查是否有這個(gè)注冊(cè)可以直接檢查?messageHandlers?里面是否有
function?hasRegisteredHandler(handlerName)?{
????let?has?=?false;
????try?{
??????has?=?!!messageHandlers[handlerName];
????}?catch?(exception)?{}
??????return?has;
????}
這里不像 callHandler 需要主動(dòng)調(diào)用 window.bridge.send 去通知客戶端,只需要等客戶端到了相應(yīng)的時(shí)機(jī)來(lái)調(diào)用 window.bridge.onReceive 就行了。
所以這里還需要改造一下 onReceive 方法。由于不再會(huì)有 callbackId 了,所以客戶端可以傳個(gè)空值,然后將 handlerName 放到 result 里面。
window.bridge.onReceive?=?function(callbackId,?result)?{
????let?params?=?{};
????try?{
????????params?=?JSON.parse(result)
????}?catch?(err)?{
????????//
????}
????if?(callbackId)?{
????????const?callback?=?callbacks[callbackId];
????????callback(params)
????????delete?callbacks[callbackId];
????}?else?if?(params.handlerName)(
????????//?可能注冊(cè)了多個(gè)
????????const?handlers?=??messageHandlers[params.handlerName];
????????for?(let?i?=?0;?i?????????????try?{
????????????????delete?params.handlerName;
????????????????handlers[i](params);
????????????}?catch?(exception)?{
????????????}
????????}
????)
}
這種情況下的流程如下,可以發(fā)現(xiàn)完全不需要 JS 調(diào)用 Native:

iOS Bridge
講完了 Android,我們?cè)賮?lái)講講 iOS,原本 iOS 可以和 Android 設(shè)計(jì)一致,可是由于種種原因?qū)е掠胁簧俨町悺?/p>
iOS 和 Android 中最顯著的差異就在于這個(gè) window.bridge.send 方法的實(shí)現(xiàn),Android 里面是直接調(diào)用 Native 的方法,iOS 中是通過(guò) URL Scheme 的形式調(diào)用。
協(xié)議依然是 WebViewJavaScriptBridge 里面的協(xié)議,URL Scheme 本身不會(huì)傳遞數(shù)據(jù),只是告訴 Native 有新的調(diào)用。
然后 Native 會(huì)去調(diào)用 JS 的方法,獲取隊(duì)列里面所有需要執(zhí)行的方法。
所以我們需要事先創(chuàng)建好一個(gè) iframe,插入到 DOM 里面,方便后續(xù)使用。
const?CUSTOM_PROTOCOL_SCHEME?=?'wvjbscheme';
const?QUEUE_HAS_MESSAGE?=?'__WVJB_QUEUE_MESSAGE__';
function?_createQueueReadyIframe(doc)?{
????messagingIframe?=?doc.createElement('iframe');
????messagingIframe.style.display?=?'none';
????messagingIframe.src?=?CUSTOM_PROTOCOL_SCHEME?+?'://'?+?QUEUE_HAS_MESSAGE;
????doc.documentElement.appendChild(messagingIframe);
??}
callHandler
每次調(diào)用的時(shí)候只需要復(fù)用這個(gè) iframe 就行了。這里是處理 callback 并通知 Native 的代碼:
function?callHandler(handlerName,?data,?responseCallback)?{
????_doSend({?handlerName:?handlerName,?data:?data?},?responseCallback);
??}
function?_doSend(
????message,
????callback
??)?{
????if?(responseCallback)?{
??????const?callbackId?=?`cb_${uniqueId++}_${new?Date().getTime()}`;
??????callbacks[callbackId]?=?callback;
??????message['callbackId']?=?callbackId;
????}
????sendMessageQueue.push(message);
????messagingIframe.src?=?CUSTOM_PROTOCOL_SCHEME?+?'://'?+?QUEUE_HAS_MESSAGE;
??}
通知 Native 之后,它怎么拿到我們的 handlerName 和 data 呢?我們可以實(shí)現(xiàn)一個(gè) fetchQueue 的方法。
??function?_fetchQueue()?{
????const?messageQueueString?=?JSON.stringify(sendMessageQueue);
????sendMessageQueue?=?[];
????return?messageQueueString;
??}
然后將其掛載到 window.WebViewJavascriptBridge 對(duì)象上面。
??window.WebViewJavascriptBridge?=?{
????_fetchQueue:?_fetchQueue
??};
這樣 iOS 就可以使用 evaluateJavaScript 輕松拿到這個(gè) messageQueue。
-?(void)flushMessageQueue
{
????[_webView?evaluateJavaScript:@"WebViewJavascriptBridge._fetchQueue();"?completionHandler:^(id?_Nullable?result,?NSError?*?_Nullable?error)?{
????????[self?_flushMessageQueue:result];
????}];
}
-?(void)_flushMessageQueue:(id)messageQueueObj
{
????//?解析?messageQueueString
????//?根據(jù)傳入的?handlerName?執(zhí)行對(duì)應(yīng)操作
}
那么 iOS 又是如何回調(diào) JS 的 callback 函數(shù)呢?這個(gè)其實(shí)和 Android 的 onReceive 是同樣的原理。
這里可以實(shí)現(xiàn)一個(gè) _handleMessageFromObjC 方法,同樣掛載到 window.WebViewJavascriptBridge 對(duì)象上面,等待 iOS 回調(diào)。
function?_dispatchMessageFromObjC(messageJSON)?{
????const?message?=?JSON.parse(messageJSON);
????if?(message.responseId)?{
????????var?responseCallback?=?callbacks[message.responseId];
????????if?(!responseCallback)?{
??????????return;
????????}
????????responseCallback(message.responseData);
????????delete?callbacks[message.responseId];
?????}
}
流程如下圖:

registerHandler
registerHandler 和 Android 原理是一模一樣的,都是提前注冊(cè)一個(gè)事件,等待 iOS 調(diào)用,具體就不多講了,這里直接放代碼:
//?注冊(cè)
function?registerHandler(handlerName,?handler)?{
????if?(typeof?messageHandlers[handlerName]?===?'undefined')?{
??????messageHandlers[handlerName]?=?[handler];
????}?else?{
??????messageHandlers[handlerName].push(handler);
????}
??}
//?回調(diào)
function?_dispatchMessageFromObjC(messageJSON)?{
????const?message?=?JSON.parse(messageJSON);
????if?(message.responseId)?{
????????var?responseCallback?=?callbacks[message.responseId];
????????if?(!responseCallback)?{
??????????return;
????????}
????????responseCallback(message.responseData);
????????delete?callbacks[message.responseId];
?????}?else?if?(message.handlerName){
????????handlers?=?messageHandlers[message.handlerName];
????????for?(let?i?=?0;?i???????????try?{
????????????handlers[i](message.data,?responseCallback);
??????????}?catch?(exception)?{
??????????}
????????}
?????}
}
總結(jié)
這些就是 Hybrid 里面 JS 和 Native 交互的大致原理,忽略了不少細(xì)節(jié),比如初始化 WebViewJavascriptBridge 對(duì)象等等,感興趣的也可以參考一下這個(gè)庫(kù):JsBridge
