Hybrid App從概念到實戰

辛月發表於2019-07-20

最近一直在準備找工作,看了很多公司的招聘介紹,有相當一部分直接寫:熟悉 Hybrid App 開發加分!正好,我司開發的就有這種 Hybrid App——使用WebViewJavascriptBridge通訊,前端封裝一些常用方法呼叫。

現在的 app 開發,已經不在是以前一樣所以頁面都是有原生開發,基於應用的更新上線繁瑣,由於 H5 的易更新,易維護性, 現在很多應用都採用同 H5 頁面混合開發模式,例如:淘寶、QQ、京東等等。下面就來看看 Native 和 H5 如何實現通訊:

JSBridge是個啥

直接來重點,記住:JSBridge 是一個很簡單的東西,更多的是一種形式、一種思想,為了解決 H5 和 Native 的雙向通訊

就像我們剛接觸 ajax 時,也很懵逼。其實,他們倆個差不多,ajax 是瀏覽器和伺服器通訊的規範(暫且叫規範,像CMD規範一樣,SeaJS是它的一種實現方式), JSBridge 是 H5 和 Native 通訊的規範。axiosajax 通訊的一種實現方式,WebViewJavascriptBridge(下文要說)是JSBridge的一種實現方式。明白了這些,下面就很好理解了。

H5 和 Native 的雙向通訊通用方法

H5通訊方式和相容性如下表所示。指的是藉助 Native 的 webview 載入H5頁面,H5 和 Native 之間通過注入API、URL攔截、全域性呼叫等形式,實現訊息通訊。站在大廠的角度考慮,在實戰的時候,會選擇更相容的方式。

H5呼叫Native方法

平臺 方法 備註
Android shouldOverrideUrlLoading scheme攔截方法
Android addJavascriptInterface API
Android onJsAlert()、onJsConfirm()、onJsPrompt()
IOS 攔截URL
IOS JavaScriptCore API方法,IOS7+ 支援
IOS window.webkit.messageHandlers APi方法,IOS8+支援

1.注入 API 方式的主要原理:通過 WebView 提供的介面,向 JavaScript 的 Context(window)中注入物件或者方法,讓 JavaScript 呼叫時,直接執行相應的 Native 程式碼邏輯,達到 JavaScript 呼叫 Native 的目的。

說白了就是,Native 往 window 物件掛物件或方法,讓 H5 可以調 Native 的方法。具體掛的物件或方法是 Native 定義的,比如人家掛了個getName(arg),H5 呼叫就是window.getName(arg),當然呼叫時可以向 Native 傳資料。

2.攔截 url scheme原理:先解釋一下 url scheme: url scheme 是一種類似於 url 的連結,是為了方便app直接互相呼叫設計的,形式和普通的 url 近似,主要區別是 protocol 和 host 一般是自定義的,例如: httpsss://bridge_loaded/url?url=http://ymfe.tech,protocol 是 httpsss,host 則是 bridge_loaded。

攔截 url scheme 的主要流程是:Web 端通過某種方式(例如 iframe.src)傳送 url scheme 請求,之後 Native 攔截到請求並根據 url scheme(包括所帶的引數)進行相關操作。

Native呼叫H5方法

平臺 方法 備註
Android loadurl()
Android evaluateJavascript() Android 4.4 +
IOS(UIwebview) stringByEvaluatingJavaScriptFromString
IOS(UIwebview) JavaScriptCore IOS7+ 支援
IOS(Wkwebview) evaluateJavaScript:javaScriptString IOS8+支援

相比於 JavaScript 呼叫 Native, Native 呼叫 JavaScript 較為簡單,畢竟不管是 iOS 的 UIWebView 還是 WKWebView,還是 Android 的 WebView 元件,都以子元件的形式存在於 View/Activity 中,直接呼叫相應的 API 即可。

Native 呼叫 JavaScript,其實就是執行拼接 JavaScript 字串,從外部呼叫 JavaScript 中的方法,因此 JavaScript 的方法必須在全域性的 window 上。(閉包裡的方法,JavaScript 自己都呼叫不了,更不用想讓 Native 去呼叫了)

通訊原理總結

通訊原理是 JSBridge 實現的核心,實現方式可以各種各樣,但是萬變不離其宗。這裡,推薦的實現方式如下:

  • JavaScript 呼叫 Native 推薦使用 注入 API 的方式(iOS6 忽略,Android 4.2以下使用 WebViewClient 的 onJsPrompt 方式)。
  • Native 呼叫 JavaScript 則直接執行拼接好的 JavaScript 程式碼即可。

說實話,作為一個前端開發,剛開始看了上面這些方法啥的,我是一臉懵*。畢竟是巢狀在人家 Native 裡面,規則都是他們實現的,我們H5只能遵循這個規則去玩。但一定要理清他們約定的這套規則是如何通訊的,才能保證我們的愉快的交流。

通訊的原理大概介紹完了,下面介紹實戰中如何使用,幫我們更好的理解概念。補充一句,通訊的實現方式有很多種,下面只是我司的實現方式:(沒有 Native 程式碼,純web前端角度)

H5 和 Native 通訊實戰

因為很多地方需要用同樣的方法,比如:關閉H5頁面並吐司、上傳圖片、預覽圖片、右上角的“增加”(╋)按鈕、...所以,我們把和原生通訊的方法寫在一個js 檔案裡,直接 export 匯出,方便程式碼的複用。

// mob.js
// 判斷是什麼平臺(裝置)
var browser = {
  versions:function(){
    var u = navigator.userAgent, app = navigator.appVersion;
    return {
        trident: u.indexOf('Trident') > -1, // IE
        presto: u.indexOf('Presto') > -1, // opera
        webKit: u.indexOf('AppleWebKit') > -1, // webkit
        gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') == -1, // firefox
        mobile: !!u.match(/AppleWebKit.*Mobile.*/), // mobile
        ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/), // iOS
        android: u.indexOf('Android') > -1 || u.indexOf('Linux') > -1, // android or uc
        iPhone: u.indexOf('iPhone') > -1 , // iPhone QQHD
        iPad: u.indexOf('iPad') > -1, // iPad
        webApp: u.indexOf('Safari') == -1,
        teacherApp: u.indexOf('XRJ-Admin') > -1 // 教師端 原生在userAgent放不同的字串代表不同app
        guardianApp: u.indexOf('XRJ-Edu') > -1 // 家長端
    };
  }(),
  language: (navigator.browserLanguage || navigator.language).toLowerCase()
}

// 判斷是否是移動裝置開啟。browser程式碼在下面
if(browser.versions.mobile) {
        var ua = navigator.userAgent.toLowerCase();//獲取判斷用的物件
        if (ua.match(/MicroMessenger/i) == "micromessenger") {
                //在微信中開啟
        }
        if (ua.match(/WeiBo/i) == "weibo") {
                //在新浪微部落格戶端開啟
        }
        if (ua.match(/QQ/i) == "qq") {
                //在QQ空間開啟
        }
        if (browser.versions.ios) {
                //是否在IOS瀏覽器開啟
        }
        if(browser.versions.android){
              //是否在安卓瀏覽器開啟
        }
} else {
   //是PC瀏覽器開啟
}

/**
* 我司通訊實現方案:
* iOS端注入 WebViewJavascriptBridge 物件或者攔截 url scheme,下面的setupWebViewJavascriptBridge是固定寫法
* 這是我司iOS的做法,也是iOS的通用做法。
*
* Android當然也可以這麼做,如果他們這樣實現,後面H5封裝的函式只用寫一套就可以適配2端了。
* 我司Android不是這麼做的,是通過注入物件或方法實現的,所以下面要針對2端寫不同程式碼。
*/
function setupWebViewJavascriptBridge(callback) {
    if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
    if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
    window.WVJBCallbacks = [callback];
    var WVJBIframe = document.createElement('iframe');
    WVJBIframe.style.display = 'none';
    WVJBIframe.src = 'https://__bridge_loaded__';
    document.documentElement.appendChild(WVJBIframe);
    setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}

// 關閉頁面並吐絲
// 呼叫:pageClose(1, '操作成功)
function pageClose(code, msg = ''){
    setupWebViewJavascriptBridge(function(bridge) {
        bridge.callHandler('iOS_RESPONSE_CALL_BACK', {"code":code,"msg":msg}, function responseCallback(responseData) {
      // 關閉頁面完畢的回撥函式,類似 ajax() 的 success: function(result) {}
      // responseData:回撥函式返回的資料,類似 success 裡面的 result
    });
    });
    if(browser.versions.android){
        if(code == 1){
            todo.closeWindow(0);
        }else if(code == 401){
            todo.refreshWebView;
            return false;
        };
        if(msg) todo.showToast(msg);
    }
}

// 圖片上傳
// 呼叫:load()
function load(){
    setupWebViewJavascriptBridge(function(bridge) {
      bridge.callHandler('iOS_UPLOAD_PHOTO', [], function responseCallback(responseData) {
                  // 上傳完畢回撥函式
          imgload(responseData,true);  // H5 頁面中定義的全域性方法 window.imgload = function(data,flag) {}
      })
    });
    if(browser.versions.android){
        picture.showPictureDialog(); // webView.loadUrl("javascript:imgload");
    }
}

// 圖片瀏覽 - data資料格式和原生商量好
// 呼叫:var imgData = {"position":position,"list":[imgsrc0,imgsrc1,imgsrc2]}; imgSee(imgData);
function imgSee(data){
    setupWebViewJavascriptBridge(function(bridge) {
      bridge.callHandler('iOS_PHOTO_BROWSER',data); // 呼叫 iOS 的 'iOS_PHOTO_BROWSER' 方法,同時傳資料data
    });
    if(browser.versions.android){
        data = JSON.stringify(data);
        todo.startGallery(data); // 呼叫 Android 的 startGallery 方法,同時傳資料data
    }
}

// 單個頭部選單,右上角的“新增”按鈕。因為是原生元件,我們操作不到,所以需要初始化頁面時往這個按鈕上繫結js方法,以便我們之後操作。
// 呼叫: topMenu('╋',0) || topMenu('新增',0)
function topMenu(title, index){
    setupWebViewJavascriptBridge(function(bridge) {
      // js註冊方法 'JS_MENU_ACTION' 給 iOS 呼叫 - 方法名 H5 決定
        bridge.registerHandler('JS_MENU_ACTION', function(data, responseCallback) {
              topMenuHandle(data,true); // H5 頁面中定義的全域性方法 window.topMenuHandle = function(data,flag) {}
              responseCallback(data); // 做完後,告訴 iOS 一聲
        })

            // 調 iOS 的 'iOS_MENU_JSON' 並傳參
        bridge.callHandler('iOS_MENU_JSON', [{"action":index,"title":title}]);  // - 方法名 iOS 決定
    });
    if(browser.versions.android){
    // topMenuHandle是往“新增”按鈕繫結的js方法,
    // index是觸發這個函式時回傳給js的資料,用來判定點選個哪個按鈕,title是這個按鈕的名字
    // H5 把一切安排的明明白白的
        var jsonStr = '[{"action":"javascript:topMenuHandle('+index+')","title":'+title+'}]'
        menu.inflateMenu(jsonStr);  // jsonStr必須是字串
    }
}

// 多個頭部選單
// 呼叫:var titleData = [{'title':"新增行為",'src':base.config.imgHost+"/cs/img/icon_bullet_behavior_add.png"},{'title':"稽核行為",'src':base.config.imgHost+"/cs/img/icon_bullet_behavior_review.png"}]
// topMenus(titleData)
function topMenus(data){
    var dataList = [];
    $.each(data,function(index,item){
        dataList.push('{"title":"'+item.title+'","src":"'+item.src+'"'+(browser.versions.android?',"action":"javascript:topMenusHandle('+index+')"':'')+'}');
    })
    setupWebViewJavascriptBridge(function(bridge) {
        bridge.registerHandler('JS_MORE_MENU_ACTION', function(responseData) {
            topMenusHandle(responseData,true);
        })
        bridge.callHandler('iOS_MORE_MENU', dataList)
    });
    if(browser.versions.android){
        menu.inflateCustomMenu('['+dataList+']');
    }
}

// 匯出
export { 
  browser,
  load,
  imgSee,
  topMenu,
  topMenus,
  pageClose,
}

總結

Hybrid是一種連線 H5 跟 Native 的思路,即可以快速迭代H5功能,又可以有NA的體驗,是混合開發的典型開發模式。實踐過程中需要根據業務形態模型來定製程式碼實現,注入時機也不是一成不變的可以根據業務形態來選擇。

參考連結:
JSBridge實戰
移動混合開發中的 JSBridge
WebViewJavascriptBridge詳細使用
Android混合開發之WebViewJavascriptBridge實現JS與java安全互動

相關文章