JSBridge實戰

Keelvin發表於2018-11-01

前言

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(如下圖)。

JSBridge實戰

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) {
         
        // 場景一: 攔截請求、接收schema
        if (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];&nbsp;
&nbsp;
/*
webView協議中的方法
shouldStartLoadWithRequest //準備載入內容時呼叫的方法,通過返回值來進行是否載入的設定
webViewDidStartLoad //開始載入時呼叫的方法
webViewDidFinishLoad //結束載入時呼叫的方法
didFailLoadWithError //載入失敗時呼叫的方法
*/
&nbsp;
- (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互不關心對方的樣子,通過中間層暴露的方法進行功能呼叫即可。

JSBridge實戰

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在實際專案中的使用的坑

相關文章