天天看點

前端H5與用戶端Native互動原理 - JSBridge

作者:閃念基因

概述

在混合應用開發中,一種常見且成熟的技術方案是将原生應用與 WebView 結合,使得複雜的業務邏輯可以通過網頁技術實作。實作這種類型的混合應用時,就需要解決H5與Native之間的雙向通信。JSBridge 是一種在混合應用中實作 Web 和原生代碼之間通信的重要機制。

混合開發

混合開發(Hybrid)是一種開發模式,指使用多種開發模型開發App,通常會涉及到兩大類技術:原生 Native、Web H5。

  • 原生技術主要指iOS、Android,原生開發效率較低,開發完成需要重新打包整個App,釋出依賴使用者的更新,性能較高功能覆寫率更高
  • Web H5可以更好的實作釋出更新,跨平台也更加優秀,但性能較低,特性也受限

混合開發的意義就在于吸取兩者的優點,而且随着手機硬體的更新疊代、系統(Android 5.0+、ISO 9.0+)對于Web特性的較好支援,H5的劣勢被逐漸縮小。

JSBridge 的概念和作用

  • 通信橋梁:JSBridge 充當了 Web 應用和原生應用之間的通信橋梁。通過 JSBridge,我們可以在 web 和原生代碼之間進行雙向通信,使這兩者能夠互相調用和傳遞資料。
  • 原生功能調用:使用 JSBridge,我們可以在 JavaScript 中調用原生應用中的功能。我們可以通過 web 來觸發原生應用中的特定操作,如打開相機、發送通知、調用硬體裝置等。
  • 資料傳遞:JSBridge 使得 JavaScript 和原生代碼之間可以友善地傳遞資料。意味着我們可以在 web 和原生代碼之間傳遞複雜的資料結構,如對象、數組等,以滿足應用的功能需求。
  • 回調機制:JSBridge 支援回調機制,使得在原生代碼執行完某些操作後可以通知 JavaScript,并傳遞相應的結果。

為什麼在混合應用開發中 JSBridge 如此重要

  • 跨平台開發:JSBridge 允許我們在混合應用中使用一套代碼同時運作在不同的平台上。這意味着我們可以使用 Web 技術來開發應用的核心邏輯,并在需要時通過 JSBridge 調用原生功能,進而實作跨平台開發,提高開發效率。
  • 原生功能擴充:使用 JSBridge,我們可以充分利用原生平台提供的功能和能力,例如通路硬體裝置、調用系統 API 等。這使得我們可以為應用添加更多豐富的功能,提升使用者體驗。
  • 靈活性和擴充性:JSBridge 提供了一種靈活和可擴充的方式來實作 Web 和原生代碼之間的通信。開發人員可以根據應用的需求随時添加新的原生功能,并通過 JSBridge 在 JavaScript 中調用這些功能,進而實作應用的功能擴充和更新。

JSBridge 做了什麼

在Hybrid模式下,H5會需要使用Native的功能,比如打開二維碼掃描、調用原生頁面、擷取使用者資訊等,同時Native也需要向Web端發送推送、更新狀态等,而JavaScript是運作在單獨的 JS Context 中(Webview容器)與原生有運作環境的隔離,是以需要有一種機制實作Native端和Web端的 雙向通信 ,這就是JSBridge:以JavaScript引擎或Webview容器作為媒介,通過協定協定進行通信,實作Native端和Web端雙向通信的一種機制。

通過JSBridge,Web端可以調用Native端的Java接口,同樣Native端也可以通過JSBridge調用Web端的JavaScript接口,實作彼此的雙向調用。

前端H5與用戶端Native互動原理 - JSBridge

JSBridge 實作原理

把 Web 端和 Native 端的通信比作 Client/Server 模式。JSBridge 充當了類似于 HTTP 協定的角色,實作了 Web 端和 Native 端之間的通信。

将 Native 端原生接口封裝成 JavaScript 接口:在 Native 端将需要被調用的原生功能封裝成 JavaScript 接口,讓 JavaScript 代碼可以調用。JavaScript 接口會被注冊到全局對象中,以供 JavaScript 代碼調用。

将 Web 端 JavaScript 接口封裝成原生接口:這一步是在 Web 端将需要被調用的 JavaScript 功能封裝成原生接口。這些原生接口會通過 WebView 的某些機制暴露給原生代碼,以供原生代碼調用。

Native -> Web

Native端調用Web端,JavaScript作為解釋性語言,最大的一個特性就是可以随時随地地通過解釋器執行一段JS代碼,是以可以将拼接的JavaScript代碼字元串,傳入JS解析器執行就可以,JS解析器在這裡就是webView。

1. Android

Android 提供了 evaluateJavascript 來執行JS代碼,并且可以擷取傳回值執行回調:

String jsCode = String.format("window.showWebDialog('%s')", text);
webView.evaluateJavascript(jsCode, new ValueCallback<String>() {
  @Override
  public void onReceiveValue(String value) {


  }
});           

2. IOS

IOS的 WKWebView 使用 evaluateJavaScript:

[webView evaluateJavaScript:@"執行的JS代碼" 
  completionHandler:^(id _Nullable response, NSError * _Nullable error) {
  // 
}];           

Web -> Native

Web調用Native端主要有兩種方式:

1. URL Schema

URL Schema是類URL的一種請求格式,格式如下:

<protocol>://<host>/<path>?<qeury>#fragment
  
// 我們可以自定義JSBridge通信的URL Schema,比如:
hellobike://showToast?text=hello           

Native加載WebView之後,Web發送的所有請求都會經過WebView元件,是以Native可以重寫WebView裡的方法,從來攔截Web發起的請求,我們對請求的格式進行判斷:

  • 符合我們自定義的URL Schema,對URL進行解析,拿到相關操作、操作,進而調用原生Native的方法
  • 不符合我們自定義的URL Schema,我們直接轉發,請求真正的服務

例如:

get existOrderRedirect() {
    let url: string;
    if (this.env.isHelloBikeApp) {
      url = 'hellobike://hellobike.com/xxxxx_xxx?from_type=xxxx&selected_tab=xxxxx';
    } else if (this.env.isSFCApp) {
      url = 'hellohitch://hellohitch.com/xxx/xxxx?bottomTab=xxxx';
    }
    return url;
  }           

這種方式從早期就存在,相容性很好,但是由于是基于URL的方式,長度受到限制而且不太直覺,資料格式有限制,而且建立請求有時間耗時。

2. 在Webview中注入JS API

通過webView提供的接口,App将Native的相關接口注入到JS的Context(window)的對象中

Web端就可以直接在全局 window 下使用這個暴露的全局JS對象,進而調用原生端的方法。

Android注入方法:

  • 4.2 前,Android 注入 JavaScript 對象的接口是 addJavascriptInterface 但是這個接口有漏洞
  • 4.2 之後,Android引入新的接口 @JavascriptInterface 以解決安全問題,是以 Android 注入對對象的方式是有相容性問題的。

IOS注入方法:

  • iOS的UIWebView:JavaSciptCore 支援 iOS 7.0 及以上系統
  • iOS的WKWebView:WKScriptMessageHandler 支援 iOS 8.0 及以上系統

例如:

  • 注入全局對象
// 注入全局JS對象
webView.addJavascriptInterface(new NativeBridge(this), "NativeBridge");


class NativeBridge {
    private Context ctx;
    NativeBridge(Context ctx) {
        this.ctx = ctx;
    }


    // 綁定方法
    @JavascriptInterface
    public void showNativeDialog(String text) {
        new AlertDialog.Builder(ctx).setMessage(text).create().show();
    }
}

           
  • Web調用方法:
// 調用nativeBridge的方法
window.NativeBridge.showNativeDialog('hello');           

H5具體實作

将功能抽象為一個 AppBridge 類,封裝兩個方法,處理互動和回調。

具體步驟:

  • 首先需要定義一個 JavaScript 類或者對象來封裝 JSBridge 方法。
  • 在 JavaScript 類或對象的構造函數中,初始化橋接回調的方法。這個方法負責接收來自原生應用的回調資料,并根據回調資料中的資訊執行相應的操作。
  • 調用原生方法:定義一個方法,用于在 JavaScript 中調用原生方法。這個方法需要接收原生類的映射、要調用的原生方法名以及傳遞給原生方法的參數,并将這些資訊傳遞給原生應用。
  • 處理原生回調:在初始化橋接回調的方法中,需要定義處理原生回調的邏輯。當收到原生應用的回調資料時,根據回調資料中的資訊執行相應的操作,比如調用 JavaScript 中注冊的回調函數,并傳遞執行結果或錯誤資訊等。

具體實作代碼:

  • 調用原生方法:
// 定義一個名為 callNative 的方法,用于在 JavaScript 中調用原生方法
callNative<P, R>(classMap: string, method: string, params: P): Promise<R> {
    return new Promise<R>((resolve, reject) => {
        // 生成一個唯一的回調 ID
        const id = v4();
        // 将目前的回調函數儲存到 __callbacks 對象中,以 callbackId 作為鍵
        this.__callbacks[id] = { resolve, reject, method: `${classMap} - ${method}` };
        // 構造通信資料,包括原生類映射、要調用的方法、參數和 callbackId 
        const data = {
            classMap,
            method,
            params: params === null ? '' : JSON.stringify(params),
            callbackId: id,
        };
        const dataStr = JSON.stringify(data);
        // 根據目前環境判斷是 iOS 還是 Android,并調用相應平台的原生方法
        if (this.env.isIOS && isFunction(window?.webkit?.messageHandlers?.callNative?.postMessage)) {
            // 如果是 iOS 平台,則調用 iOS 的原生方法
            window.webkit.messageHandlers.callNative.postMessage(dataStr);
        } else if (this.env.isAndroid && isFunction(window?.AppFunctions?.callNative)) {
            // 如果是 Android 平台,則調用 Android 的原生方法
            window.AppFunctions.callNative(dataStr);
        }
    });
}           
  • 回調處理:
// 初始化橋接回調函數,該參數在 constructor 中調用
private initBridgeCallback() {
    // 儲存舊的回調函數到 oldCallback 變量中
    const oldCallback = window.callBack;
    // 重新定義 window.callBack 方法,用于處理原生應用的回調資料
    window.callBack = (data) => {
        // 如果存在舊的回調函數,則調用舊的回調函數
        if (isFunction(oldCallback)) {
            oldCallback(data);
        }
        // 擷取原生應用的回調資訊,包括資料和回調 ID
        console.info('native callback', data, data.callbackId);
        // 從回調資料中擷取回調 ID
        const { callbackId } = data;
        // 根據回調 ID 查找對應的回調函數
        const callback = this.__callbacks[callbackId];
        // 如果找到了對應的回調函數
        if (callback) {
            // 如果回調資料中的 code 為 0,則表示執行成功,調用 resolve 方法處理成功的結果
            if (data.code === 0) {
                callback.resolve(data.data);
            } else {
                // 否則,表示執行失敗,構造一個錯誤對象并調用 reject 方法處理錯誤資訊
                const error = new Error(data.msg) as Error & {response:unknown};
                error.response = data;
                callback.reject(error);
            }
            // 删除已經處理過的回調函數
            delete this.__callbacks[callbackId];
        }
    };
}           
  • 使用:
// 調用原生方法的封裝函數
callNative<P, R>(classMap: string, method: string, params: P) {
    // 從容器中解析出 AppBridge 執行個體
    const bridge = container.resolve<AppBridge>(AppBridge);
    // 使用 bind 方法将 AppBridge 執行個體中的 callNative 方法綁定到 bridge 對象上,并儲存到 func 變量中
    const func = bridge.callNative.bind(bridge);
    // 調用 func 方法,并傳入 classMap、method 和 params 參數,實作調用原生方法的功能
    return func<P, R>(classMap, method, params);
}




// 打開 webview
// 調用 callNative 方法,傳入參數 url,classMap 為 'xxxxx/hitch',method 為 'openWebview'
openWebView(url: string): Promise<void> {
    return this.callNative<{url:string}, void>('xxxxx/hitch', 'openWebview', { url });
}




// 擷取駕駛證 OCR 資訊
getDriverLicenseOcrInfo(
    params: HBNative.getDriverLicenseOcrInfo.Params,
): Promise<HBNative.getDriverLicenseOcrInfo.Result> {
    // 調用 callNative 方法,傳入參數 params,classMap 為 'xxxxx/hitch',method 為 'getOcrInfo'
    // 傳回一個 Promise 對象,該 Promise 對象用于處理異步結果
    return this.callNative<
        HBNative.getDriverLicenseOcrInfo.Params,
        HBNative.getDriverLicenseOcrInfo.Result>(
            'xxxxx/hitch', 'getOcrInfo', params,
        );
}           

作者:佟健

來源-微信公衆号:哈啰技術

出處:https://mp.weixin.qq.com/s/h6vlkf5rgyI9GpEpPxO9cg