<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          JS Bridge 通信原理

          共 9938字,需瀏覽 20分鐘

           ·

          2021-01-18 22:02


          前言

          上一篇介紹了移動端開發(fā)的相關(guān)技術(shù),這一篇主要是從 Hybrid 開發(fā)的 JS Bridge 通信講起。

          顧名思義,JS Bridge 的意思就是橋,也就是連接 JS 和 Native 的橋梁,它也是 Hybrid App 里面的核心。一般分為 JS 調(diào)用 Native 和 Native 主動調(diào)用 JS 兩種形式。

          URL Scheme

          URL Scheme 是一種特殊的 URL,一般用于在 Web 端喚醒 App,甚至跳轉(zhuǎn)到 App 的某個頁面,比如在某個手機網(wǎng)站上付款的時候,可以直接拉起支付寶支付頁面。

          這里有個小例子,你可以在瀏覽器里面直接輸入 weixin://,系統(tǒng)就會提示你是否要打開微信。輸入 mqq:// 就會幫你喚起手機 QQ。

          這里有個常用 App URL Scheme 匯總:URL Schemes 收集整理

          在手機里面打開這個頁面后點擊這里,就會提示你是否要打開微信。

          我們常說的 Deeplink 一般也是基于 URL Scheme 來實現(xiàn)的。一個 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 這兩個常見的協(xié)議,還可以自定義協(xié)議。借用維基百科的一張圖:

          通常情況下,App 安裝后會在手機系統(tǒng)上注冊一個 Scheme,比如 weixin:// 這種,所以我們在手機瀏覽器里面訪問這個 scheme 地址,系統(tǒng)就會喚起我們的 App。

          一般在 Android 里面需要到 AndroidManifest.xml 文件中去注冊 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 里面注冊,有一些已經(jīng)是系統(tǒng)使用的不應該使用,比如 Maps、YouTube、Music。

          具體可以參考蘋果開發(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 特性,性能更加強大。

          但也有一些缺點,比如不支持緩存,需要自己注入 Cookie,發(fā)送 POST 請求的時候帶不了參數(shù),攔截 POST 請求的時候無法解析參數(shù)等等。

          「題外話(小小吐槽一下)」

          我們這邊有很多場景是需要客戶端和銀行交互的,為了從銀行 H5 回調(diào)到我們的客戶端,常常是會讓銀行跳到我們的 H5 中間頁,客戶端去攔截并解析銀行帶來的參數(shù)。

          以前有些頁面是 Python 服務端渲染做的,支持 POST 請求打開頁面。后來 iOS 客戶端升級 ?WKWebView,導致無法獲取銀行 POST 帶過來的參數(shù)。

          為了配合客戶端發(fā)版上線,曾經(jīng)連夜修改了我們 App 里面所有相關(guān)的綁卡和支付 H5 頁面,還好沒有搞出重大線上事故。

          JS 調(diào)用 Native 通信大致有三種方法:

          1. 攔截 Scheme
          2. 彈窗攔截
          3. 注入 JS 上下文

          這三種方式總體上各有利弊,下面會一一介紹。

          攔截 Scheme

          仔細思考一下,如果是 JS 和 Java 之間傳遞數(shù)據(jù),我們該怎么做呢?

          對于前端開發(fā)來說,調(diào) Ajax 請求接口是最常見的需求了。不管對方是 Java 還是 Python,我們都可以通過 http/https 接口來獲取數(shù)據(jù)。實際上這個流程和 JSONP 更加類似。

          已知客戶端是可以攔截請求的,那么可不可以在這個上面做文章呢?

          如果我們請求一個不存在的地址,上面帶了一些參數(shù),通過參數(shù)告訴客戶端我們需要調(diào)用的功能呢?

          比如我要調(diào)用掃碼功能:

          axios.get('http://xxxx?func=scan&callback_id=yyyy')

          客戶端可以攔截這個請求,去解析參數(shù)上面的 func 來判斷當前需要調(diào)起哪個功能。客戶端調(diào)起掃碼功能之后,會獲取 WebView 上面的 callbacks 對象,根據(jù) callback_id 回調(diào)它。

          所以基于上面的例子,我們可以把域名和路徑當做通信標識,參數(shù)里面的 func 當做指令,callback_id 當做回調(diào)函數(shù),其他參數(shù)當做數(shù)據(jù)傳遞。對于不滿足條件的 http 請求不應該攔截。

          當然了,現(xiàn)在主流的方式是前面我們看到的自定義 Scheme 協(xié)議,以這個為通信標識,域名和路徑當做指令。

          這種方式的好處就是 iOS6 以前只支持這種方式,兼容性比較好。

          JS 端

          我們有很多種方法可以發(fā)起請求,目前使用最廣泛的是 iframe 跳轉(zhuǎn):

          1. 使用 a 標簽跳轉(zhuǎn)
          "taobao://">點擊我打開淘寶
          1. 重定向
          location.href?=?"taobao://"
          1. iframe 跳轉(zhuǎn)
          const?iframe?=?document.createElement("iframe");
          iframe.src?=?"taobao://"
          iframe.style.display?=?"none"
          document.body.appendChild(iframe)

          Android 端

          在 Android 側(cè)可以用 shouldOverrideUrlLoading 來攔截 url 請求。

          @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ù)的形式,主要存在幾個問題。

          1. 連續(xù)續(xù)調(diào)用 location.href 會出現(xiàn)消息丟失,因為 WebView 限制了連續(xù)跳轉(zhuǎn),會過濾掉后續(xù)的請求。
          2. URL 會有長度限制,一旦過長就會出現(xiàn)信息丟失 因此,類似 WebViewJavaScriptBridge 這類庫,就結(jié)合了注入 API 的形式一起使用,這也是我們這邊目前使用的方式,后面會介紹一下。

          彈窗攔截

          Android 實現(xiàn)

          這種方式是利用彈窗會觸發(fā) WebView 相應事件來攔截的。

          一般是在 setWebChromeClient 里面的 onJsAlert、onJsConfirm、onJsPrompt 方法攔截并解析他們傳來的消息。

          //?攔截?Prompt
          @Override
          public?boolean?onJsPrompt(WebView?view,?String?url,?String?message,?String?defaultValue,?JsPromptResult?result)?{
          ???????if?(xxx)?{
          ????????//?解析?message?的值,調(diào)用對應方法
          ???????}
          ???????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 實現(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);
          ????}
          ?}

          這種方式的缺點就是在 iOS 上面 UIWebView 不支持,雖然 WKWebView 支持,但它又有更好的 scriptMessageHandler,比較尷尬。

          注入上下文

          前面我們有講過在 iOS 中內(nèi)置了 JavaScriptCore 這個框架,可以實現(xiàn)執(zhí)行 JS 以及注入 Native 對象等功能。

          這種方式不依賴攔截,主要是通過 WebView 向 JS 的上下文注入對象和方法,可以讓 JS 直接調(diào)用原生。

          PS:iOS 中的 Block 是 OC 對于閉包的實現(xiàn),它本質(zhì)上是個對象,定義 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)用是同步的,可以立馬拿到返回值。

          我們也不再需要像攔截方式一樣,每次傳值都要把對象做 JSON.stringify,可以直接傳 JSON 過去,也支持直接傳一個函數(shù)過去。

          iOS WKWebView

          WKWebView 里面通過 addScriptMessageHandler 來注入對象到 JS 上下文,可以在 WebView 銷毀的時候調(diào)用 removeScriptMessageHandler 來銷毀這個對象。

          前端調(diào)用注入的原生方法之后,可以通過 didReceiveScriptMessage 來接收前端傳過來的參數(shù)。

          WKWebView?*wkWebView?=?[[WKWebView?alloc]?init];
          WKWebViewConfiguration?*configuration?=?wkWebView.configuration;
          WKUserContentController?*userCC?=?configuration.userContentController;

          //?注入對象
          [userCC?addScriptMessageHandler:self?name:@"nativeObj"];
          //?清除對象
          [userCC?removeScriptMessageHandler:self?name:@"nativeObj"];

          //?客戶端處理前端調(diào)用
          -?(void)userContentController:(WKUserContentController?*)userContentController?didReceiveScriptMessage:(WKScriptMessage?*)message
          {
          ????//?獲取前端傳來的參數(shù)
          ????NSDictionary?*msgBody?=?message.body;
          ????//?如果是?nativeObj?就進行相應處理
          ????if?(![message.name?isEqualToString:@"nativeObj"])?{
          ????????//?
          ????????return;
          ????}
          }

          使用 addScriptMessageHandler 注入的對象實際上只有一個 postMessage 方法,無法調(diào)用更多自定義方法。前端的調(diào)用方式如下:

          window.webkit.messageHandlers.nativeObj.postMessage(data);

          需要注意的是,這種方式要求 iOS8 及以上,而且返回不是同步的。和 UIWebView 一樣的是,也支持直接傳 JSON 對象,不需要 stringify。

          Android addJavascriptInterface

          安卓4.2之前注入 JS 一般是使用 addJavascriptInterface ,和前面的 addScriptMessageHandler 有一些類似,但又沒有它的限制。

          public?void?addJavascriptInterface()?{
          ????????mWebView.addJavascriptInterface(new?DatePickerJSBridge(),?"DatePickerBridge");
          ????}
          private?class?PickerJSBridge?{
          ????public?void?_pick(...)?{
          ????}
          }

          在 JS 里面調(diào)用:

          window.DatePickerBridge._pick(...)

          但這種方案有一定風險,可以參考這篇文章:WebView中接口隱患與手機掛馬利用

          在 Android4.2 之后提供了 @JavascriptInterface 注解,暴露給 JS 的方法必須要帶上這個。

          所以前面的 _pick 方法需要帶上這個注解。

          private?class?PickerJSBridge?{
          ????@JavascriptInterface
          ????public?void?_pick(...)?{
          ????}
          }

          Native 調(diào)用 JS

          Native 調(diào)用 JS 一般就是直接 JS 代碼字符串,有些類似我們調(diào)用 JS 中的 eval 去執(zhí)行一串代碼。一般有 loadUrlevaluateJavascript 等幾種方法,這里逐一介紹。

          但是不管哪種方式,客戶端都只能拿到掛載到 window 對象上面的屬性和方法。

          Android

          在 Android 里面需要區(qū)分版本,在安卓4.4之前的版本支持 loadUrl,使用方式類似我們在 a 標簽的 href 里面寫 JS 腳本一樣,都是javascript:xxx 的形式。

          這種方式無法直接獲取返回值。

          webView.loadUrl("javascript:foo()")

          在安卓4.4以上的版本一般使用 evaluateJavascript 這個 API 來調(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 來調(diào)用 JS 代碼。這種方式是同步的,會阻塞線程。

          results?=?[self.webView?stringByEvaluatingJavaScriptFromString:"foo()"];

          WKWebView

          WKWebView 可以使用 evaluateJavaScript 方法來調(diào)用 JS 代碼。

          [self.webView?evaluateJavaScript:@"document.body.offsetHeight;"?completionHandler:^(id?_Nullable?response,?NSError?*?_Nullable?error)?{
          ????//?獲取返回值?response
          ????}];

          JS Bridge 設計

          前面講完了 JS 和 Native 互調(diào)的所有方法,這里來介紹一下我們這邊 JS Bridge 的設計吧。

          我們這邊的 JS Bridge 通信是基于 WebViewJavascriptBridge 這個庫來實現(xiàn)的。

          主要是結(jié)合 Scheme 協(xié)議+上下文注入來做??紤]到 Android 和 iOS 不一樣的通信方式,這里進行了封裝,保證提供給外部的 API 一致。

          具體功能的調(diào)用我們封裝成了 npm 包,下面的是幾個基礎 API:

          1. callHandler(name, params, callback):這個是調(diào)用 Native 功能的方法,傳模塊名、參數(shù)、回調(diào)函數(shù)給 Native。
          2. hasHandler(name):這個是檢查客戶端是否支持某個功能的調(diào)用。
          3. registerHandler(name):這個是提前注冊一個函數(shù),等待 Native 回調(diào),比如 pageDidBack 這種場景。

          那么這幾個 API 又是如何實現(xiàn)的呢?這里 Android 和 iOS 封裝不一致,應當分開來說。

          Android Bridge

          前面我們有說過安卓可以通過 @JavascriptInterface 注解來將對象和方法暴露給 JS。

          所以這里的幾個方法都是通過注解暴露給 JS 來調(diào)用的,在 JS 層面做了一些兼容處理。

          hasHandler

          首先最簡單的是這個 hasHandler,就是在客戶端里面維護一張表(其實我們是寫死的),里面有支持的 Bridge 模塊信息,只需要用 switch...case 判斷一下就行了。

          @JavascriptInterface
          public?boolean?hasHandler(String?cmd)?{
          ????????switch?(cmd)?{
          ????????????case?xxx:
          ????????????case?yyy:
          ????????????case?zzz:
          ????????????????return?true;
          ????????}
          ????????return?false;
          ????}

          callHandler

          然后我們來看 callHandler 這個方法,它是提供 JS 調(diào)用 Native 功能的方法。在調(diào)用這個方法之前,我們一般需要先判斷一下 Native 是否支持這個功能。

          function?callHandler(name,?params,?callback)?{
          ????if?(!window.WebViewJavascriptBridge.hasHandler(name))?{
          ????}
          }

          如果 Native 沒有支持這個 Bridge,我們就需要對回調(diào)進行兼容性處理。這個兼容性處理包括兩個方面,一個是功能方面,一個是 callback 的默認回參。

          比如我們調(diào)用 Native 的彈窗功能,如果客戶端沒支持這個 Bridge,或者我們是在瀏覽器里面打開的這個頁面,此時應該降級到使用 Web 的 alert 彈窗。

          對于 callback,我們可以默認給傳個 0,表示當前不支持這個功能。

          假設這個 alert 的 bridge 接收兩個參數(shù),分別是 titlecontent,那么此時就應該使用瀏覽器自帶的 alert 展示出來。

          function?fallback(params,?callback)?{
          ????let?content?=?`${params.title}\n{params.content}`
          ????window.alert(content);
          ????callback?&&?callback(0)
          }

          這個 fallback 函數(shù)我們希望能夠更加通用,每個調(diào)用方法都應該有自己的 fallback 函數(shù),所以前面的 callHandler 應該設計成這樣:

          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);
          ????????}
          ????}
          }

          我們可以基于這個函數(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 上隨便找的一張圖(侵刪):

          那么客戶端又如何實現(xiàn)回調(diào) callback 函數(shù)的呢?前面說過,客戶端想調(diào)用 JS 方法,只能調(diào)用掛載到 window 對象上面的。

          因此,這里使用了一種很巧妙的方法,實際上 callback 函數(shù)依然是 JS 執(zhí)行的。

          在調(diào)用 Native 之前,我們可以先將 callback 函數(shù)和一個 uniqueId 映射起來,然后存在 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)
          }

          在客戶端這里,當 send 方法接收到參數(shù)之后,會執(zhí)行相應功能,然后使用 webView.loadUrl 主動調(diào)用前端的一個接收函數(shù)。

          @JavascriptInterface
          public?void?send(final?String?cmd,?String?data,?final?String?callbackId)?{
          ????//?獲取數(shù)據(jù),根據(jù)?cmd?來調(diào)用對應功能
          ????//?調(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 需要事前定義好這個 onReceive 方法,它接收一個 callbackId 和一個 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

          注冊的流程比較簡單,也是我們把 callback 函數(shù)事先存到一個 messageHandler 對象里面,不過這次的 key 不再是一個隨機的 id,而是 name。

          function?registerHandler(handlerName,?callback)?{
          ????if?(!messageHandlers[handlerName])?{
          ??????messageHandlers[handlerName]?=?[handler];
          ????}?else?{
          ??????//?支持注冊多個?handler
          ??????messageHandlers[handlerName].push(handler);
          ????}
          }
          //?檢查是否有這個注冊可以直接檢查?messageHandlers?里面是否有
          function?hasRegisteredHandler(handlerName)?{
          ????let?has?=?false;
          ????try?{
          ??????has?=?!!messageHandlers[handlerName];
          ????}?catch?(exception)?{}
          ??????return?has;
          ????}

          這里不像 callHandler 需要主動調(diào)用 window.bridge.send 去通知客戶端,只需要等客戶端到了相應的時機來調(diào)用 window.bridge.onReceive 就行了。

          所以這里還需要改造一下 onReceive 方法。由于不再會有 callbackId 了,所以客戶端可以傳個空值,然后將 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)(
          ????????//?可能注冊了多個
          ????????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,我們再來講講 iOS,原本 iOS 可以和 Android 設計一致,可是由于種種原因?qū)е掠胁簧俨町悺?/p>

          iOS 和 Android 中最顯著的差異就在于這個 window.bridge.send 方法的實現(xiàn),Android 里面是直接調(diào)用 Native 的方法,iOS 中是通過 URL Scheme 的形式調(diào)用。

          協(xié)議依然是 WebViewJavaScriptBridge 里面的協(xié)議,URL Scheme 本身不會傳遞數(shù)據(jù),只是告訴 Native 有新的調(diào)用。

          然后 Native 會去調(diào)用 JS 的方法,獲取隊列里面所有需要執(zhí)行的方法。

          所以我們需要事先創(chuàng)建好一個 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)用的時候只需要復用這個 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 之后,它怎么拿到我們的 handlerNamedata 呢?我們可以實現(xiàn)一個 fetchQueue 的方法。

          ??function?_fetchQueue()?{
          ????const?messageQueueString?=?JSON.stringify(sendMessageQueue);
          ????sendMessageQueue?=?[];
          ????return?messageQueueString;
          ??}

          然后將其掛載到 window.WebViewJavascriptBridge 對象上面。

          ??window.WebViewJavascriptBridge?=?{
          ????_fetchQueue:?_fetchQueue
          ??};

          這樣 iOS 就可以使用 evaluateJavaScript 輕松拿到這個 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í)行對應操作
          }

          那么 iOS 又是如何回調(diào) JS 的 callback 函數(shù)呢?這個其實和 Android 的 onReceive 是同樣的原理。

          這里可以實現(xiàn)一個 _handleMessageFromObjC 方法,同樣掛載到 window.WebViewJavascriptBridge 對象上面,等待 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 原理是一模一樣的,都是提前注冊一個事件,等待 iOS 調(diào)用,具體就不多講了,這里直接放代碼:

          //?注冊
          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 交互的大致原理,忽略了不少細節(jié),比如初始化 WebViewJavascriptBridge 對象等等,感興趣的也可以參考一下這個庫:JsBridge

          瀏覽 70
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美成人在线三级片 | 影音先锋亚洲无码AV | 性色网 | 三级国产三级在线 | 色色大香蕉|