javascript
JSBridge实战
前言
H5 VS Native 一直是前端技術界爭執不下的話題。react、vue等技術棧引領著純H5開發,rn、week則倡導原生體驗。但在項目實戰中,經常會選擇一個中立的方案:混合開發。大眾稱呼:Hybrid。
本人目前從事新聞類產品研發,對于大家來講,就是熟知的如今日頭條、百度新聞、網易新聞等。在產品設計初期,考慮到一些實現難易程度問題(如新聞詳情頁,圖文混排,NA實現起來不如H5這樣自如),一些部分選擇了Hybrid方式開發,本篇就把開發過程中的一些想法分享一下,以供大家參考。
JSBridge解決的問題
混合開發,最重要的問題是:H5和Native的雙向通信。 但現實中JS和NA的交互方法非常有限,下面會詳細說明。開發中如只是單純的方法調用,既無法確保調用成功率,也無法確保代碼足夠簡潔。于是就有了JSBridge。JSBridge,是一種JS實現的Bridge,是一種思路,可以有不同理解,不同的代碼實現。主旨思想是在H5和NA之間搭建一個橋梁(Bridge),給兩端留好更友好、更合理的接口。
H5和NA的雙向通信通用方法
H5通信方式和兼容性如下表所示。指的是借助Native的webview加載H5頁面,H5和NA之間通過API、URL攔截、全局調用等形式,實現消息通信。站在大廠的角度考慮,在實戰的時候,會選擇更兼容的方式。
H5調用NA方法梳理
| Android | shouldOverrideUrlLoading | scheme攔截方法 |
| Android | addJavascriptInterface | API |
| Android | onJsAlert()、onJsConfirm()、onJsPrompt() | |
| IOS | 攔截URL | |
| IOS(UIwebview) | JavaScriptCore | API方法,IOS7 支持 |
| IOS(WKwebview) | window.webkit.messageHandlers | APi方法,IOS8 支持 |
NA調用H5方法梳理
| Android | loadurl() | |
| Android | evaluateJavascript() | Android 4.4 |
| IOS(UIwebview) | stringByEvaluatingJavaScriptFromString | |
| IOS(UIwebview) | JavaScriptCore | IOS7.0 |
| IOS(Wkwebview) | evaluateJavaScript:javaScriptString | iOS8.0 |
通過上面兩端調用方法梳理表,不難分析出,URL攔截 & 執行JS是 安卓和IOS比較通用且兼容性較好的方案。我們混合開發的基礎正是基于這種方法來實現的。
常規混合開發思路
H5和NA通信方面,最簡單直接的思路是:NA攔截H5的URL獲取消息(一般是通過修改iframe的src來實現 ①),經過業務處理,NA執行JS(在H5側提前注冊好的全局方法③)回調通知H5(如下圖)。
H5代碼實現如下:
<html> ... <body><div class="content">XXXXX</div> </body><script>// ① 注冊全局函數,以便端調用window.setAllContent = function(){}// ② 通用方法函數var sendschema = function(action,param){let tempnode = document.createElement('iframe');tempnode.src = "bdnews://" action param;}// ③ H5邏輯開始 運行函數document.addEventListener("DOMContentLoaded",function(){sendschema('load_finish');},false); </script>... </html>Android原理大致如下:
webView.setWebViewClient(new WebViewClient() {public boolean shouldOverrideUrlLoading(WebView view, String url) {// 場景一: 攔截請求、接收schemaif (url.equals("load_url")) {// 處理邏輯dosomething// 回掉view.loadUrl("javascript:setAllContent(" json ");")}// 場景二:端自己調用H5,沒有請求發起clickbutton(){view.loadUrl("javascript:setAllContent(" json ");")}} });IOS大概邏輯如下:
// 初始化webview UIWebView * view = [[UIWebView alloc]initWithFrame:self.view.frame]; [view loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.xx.com"]]]; [self.view addSubview:view]; /* webView協議中的方法 shouldStartLoadWithRequest //準備加載內容時調用的方法,通過返回值來進行是否加載的設置 webViewDidStartLoad //開始加載時調用的方法 webViewDidFinishLoad //結束加載時調用的方法 didFailLoadWithError //加載失敗時調用的方法 */ - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {if ([urlString hasPrefix:@"scheme://hybrid?info="]) {if([name isEqualToString:@"load_finish"]){// [self.webView setContent];[self.webView stringByEvaluatingJavaScriptFromString:strFormat];}} }- clickbutton(){[self.webView setContent]; }但這樣開發存在一些痛點:
1)回調函數不明確。可以說目前沒有回調函數的機制,這導致一些依賴于回調函數的分析及判斷無法正常使用,如:功能調用方、調用是否成功、調用失敗異常處理等這些CASE;
2)對應關系不明確。有一些調用看起來像是回調,但沒有把他們放到一起,導致代碼散亂,難以維護。如上面demo:sendschema('load_finish') 和 setAllContent 本來含義是 告訴NA頁面準備好了,NA收到后,向頁面塞數據。本來緊密相關的一對功能,拆分開看不出有什么聯系;
3)全局函數冗雜。理想中如果調用和回調成對出現,DEMO中注冊及維護全局函數的工作就會減少很多。提升頁面可讀性和維護成本。如 load_finish 和 setAllContent,只保留 load_finish 即可;
4)端內代碼冗雜。端內注冊了與H5約定的調用方法,很顯然也需要維護一套代碼標識什么時候調用。
以上開發中遇到的問題,也許剛開始功能不多的時候還察覺不出問題,但是隨著功能增加,后期維護成本很大。
JSB方案設計
在H5和NA之間增加一個中間層,這層封裝了H5和NA通信的交互方式。H5和NA互不關心對方的樣子,通過中間層暴露的方法進行功能調用即可。
JSB交互模型
H5跟NA交互,從H5角度來看大致可分為兩大類:有去無回&有去有回、無去有回。
第一類交互模型
請求邏輯:有去無回、有去有回。這里有兩種實現方案(初步思路稿如下):
① 函數名關聯
let BDAPPnode = {callbacks: {},// 調用函數注冊invoke(action, params, successfnname, successfn) {this.callbacks[successfnname] = {success: successfn};sendschema(action, params);},// NA調用callbackSuccess(callbackname, params) {try {BDAPPnode.callbackFromNative(callbackname, params, true);} catch (e) {console.log('Error in error callback: ' callbackname ' = ' e);}},callbackFromNative(callbackname, params, isSuccess) {let callback = this.callbacks[callbackname];if (callback) {if (isSuccess) {callback.success && callback.success(params);}};} };② ID 關聯
let BDAPPnode = {callbackId: Math.floor(Math.random() * 2000000000),callbacks: {},invoke(action, params, onSuccess, onFail) {this.callbackId ;this.callbacks[self.callbackId] = {success: onSuccess,fail: onFail};sendschema(action, params, this.callbackId);},callbackSuccess(callbackId, params) {try {BDAPPnode.callbackFromNative(callbackId, params, true);} catch (e) {console.log('Error in error callback: ' callbackId ' = ' e);}},callbackError(callbackId, params) {try {BDAPPnode.callbackFromNative(callbackId, params, false);} catch (e) {console.log('Error in error callback: ' callbackId ' = ' e);}},callbackFromNative(callbackId, params, isSuccess) {let callback = this.callbacks[callbackId];if (callback) {if (isSuccess) {callback.success && callback.success(callbackId, params);} else {callback.fail && callback.fail(callbackId, params);}delete BDAPPnode.callbacks[callbackId];};} };在發出請求的時候,注冊回調方法。這么做有兩個目的:
-
無需提前注冊所有全局回掉函數,減少不必要的初始化,進而減少白屏時間;
-
不用額外起回掉函數的名稱,發起請求的時候傳入一個隨機ID,同時注冊此ID的回掉函數。NA通過統一封裝好的回掉函數調用,回調ID和參數,進而達到執行回調邏輯。
具體選用那個,還得根據具體情況具體分析看。
第二類交互模型看
請求邏輯:無去有回,沒有發出請求,NA主動調用。此類還需注冊全局變量,等待NA調用。跟非JSBridge的實現是一個道理
window.fn1 = () =>{// do fn1 }window.fn2 = () =>{// do fn2 }方案選擇
實戰過程中深刻體會到,混合開發可以分為兩大類:NA服務H5,H5服務NA。
前者H5為主,大多數交互是H5發起NA請求,等待NA回調,可稱之為:『一對一請求』,如:H5請求獲取地理位置,NA做完后返回N\S坐標;
后者主要是為了解決NA成本實現高的問題,多為NA主動調用H5提前注冊好的方法,可稱之為:『單獨請求』,確保功能順利實現。
在項目實戰過程中,經常會有這種情況:回調函數既是一對一請求,也是單獨調用,如:評論功能,可以頁面點擊彈出NA輸入框發送,也可以點擊底BAR上NA實現的按鈕彈框發送。對于頁面來講都需要更新。站在H5角度希望NA區分,H5頁面調用的評論成功和NA調用的評論成功進行區分,這樣就可以把模型一和模型二區分開獨立實現(同時也可以區分頁面刷新的來源)。但站在NA角度來講,不關心誰吊起的,只要評論成功,就應該去調用更新頁面的H5方法。不然NA需要從調用開始就攜帶參數,一路到底。跟端溝通后,雙方都妥協了一步,簡單功能的進行了來源區分模型一實現,較為復雜的模型二實現。
API封裝
API層處于JSBridge底層和業務,有些人也把它當做JSBridge的一部分,為了更好理解,我將它單獨抽離出來。此處主要封裝業務層調用,如下面代碼。
此處多說一句:平日開發要有封裝和抽離的思想,一方面減少重復代碼,一方面不斷抽離將代碼分層,沒一層可以做一些封裝和擴展,可以提高代碼復用性。
JSB注入時機
NA注入
我們肯定是期望JSB注入越早越好,這樣不論在前端頁面中任何位置都可以隨時調用,NA注入JS的方法和時機都比較局限。如下表:
| IOS[UI] | [self.webView stringByEvaluatingJavaScriptFromString:injectjs] | webViewDidFinishLoad(會有時機問題) |
| IOS[wk] | evaluateJavaScript:xxxx | didCreateJavaScriptContext |
| Android | webView.loadUrl("javascript:" injectjs);) | OnPageFinished |
網頁描述頁面狀態的值有以下方法,根據兼容性及實現完整性,一般用DOMContentLoaded,IE9以下用readystatechange來判斷頁面是否加載成功。
| DOMContentLoaded | doc | 頁面內容OK | IE9 |
| onload | win | 頁面所有只要加載完成 | |
| readystatechange | doc | 頁面加載狀態:uninitialized(為初始化):對象存在但尚未初始化。loading(正在加載):對象正在加載數據。loaded(加載完畢):對象加載數據完成。interactive(交互):可以操作對象了,但還沒有完全加載。complete(完成):對象已經加載完畢 | IE9&IE10有實現bug |
IOS的uiwebview提供了代理WebViewDidFinishLoad,WebViewDidFinishLoad 被調用時,readyState 可能處在 interactive 和 complete 兩種狀態,所以初始化頁面直接調用會有問題。對于這個問題從NA角度可以實現一個NSObject的擴展,并實現webView:didCreateJavaScriptContext:forFrame。從H5角度可以檢測頁面狀態,在complete之后再調用native。
IOS的didCreateJavaScriptContext和Android的OnPageFinished(the page has finished loading)均是在網頁onload之前完成,所以這兩個時機沒有調用順序的問題。
優點:
1)注冊早,即使在頁面初始化就調用端能力,也可以滿足
缺點:
由于我們選擇的是uiwebview如果按照上面的考慮,這樣做有幾點不足之處 1)監聽實現成本高 2)需要NA注入,NA對于JS不熟悉,JS往往也不清楚NA邏輯,后面維護成本不可控制。
如果時間不充裕的情況下,除了NA注入,還有別的辦法嘛?
JS注入
其實JS也可以在頁面一開始就注入。比如在head里直接應用抽離出來的Jsbridge代碼,本次8.0我們采用了這種降級方案,短時間內完成了架構搭建。
優點:
這樣減小了維護成本,功能完整,提高了調用成功的幾率。
缺點:
增加了頁面加載解析時間會影響白屏時間。
總結
Hybrid是一種連接H5跟NA的思路,即可以快速迭代H5功能,又可以有NA的體驗,是混合開發的典型開發模式。實踐過程中需要根據業務形態模型來定制代碼實現,注入時機也不是一成不變的可以根據業務形態來選擇。
參考文獻
移動混合開發中的 JSBridge
遠程過程調用
你要的WebView與 JS 交互方式 都在這里了
UIWebView與WKWebView、JavaScript與OC交互
iOS中UIWebView的使用詳解
UIWebView代碼注入時機與姿勢
Hybrid 開發
JavaScriptCore在實際項目中的使用的坑
總結
以上是生活随笔為你收集整理的JSBridge实战的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用HTML5 IndexDB存储图像和
- 下一篇: vue2.0中的:is和is的区别