jsbridge初探

深紅發表於2020-03-05

jsbridge是隨著Hybrid App的流行而產生的一種技術。那麼Hybrid App是啥?Hybrid App又稱混合App,即同時使用了前端web技術(js,css,html)和原生native技術(java,kotlin,swfit,object-c)進行開發的移動應用。

混合開發的優缺點

  • 優點:開發快,易更新,開發週期短,跨平臺
  • 缺點:效能問題,相容性問題

常見的混合開發框架

  • webview渲染:Cordova,uni-app
  • 原生渲染:React Native,Weex,Flutter
  • 混合渲染:小程式

jsbridge

現在很多App的頁面,不一定都是原生實現的,可能是通過webview直接載入一個線上的h5站點。比如開啟某粉紅App的會員購頁面,其實就是個移動端的網站。

jsbridge初探

這麼一說,好像和混合開發也沒啥聯絡。不過你仔細看下頁面的右上角,會發現有個分享按鈕:點選分享圖示,可以把當前頁面分享到第三方平臺,分享後,web頁面需要知道是否分享成功。

這裡就涉及了native端和web端的通訊:native分享的內容,需要web端的js進行設定(js -> native);native分享成功後,需要把訊息通知給js(natvie -> js)。為實現兩端的雙向通訊機制,就需要jsbridge技術了。

Native通知JS

因為h5網頁是通過原生端的webview載入的,所以原生端對當前網頁擁有很高的許可權:Native端可以直接在當前webview裡執行js程式碼。

// web端
function nativeCallback(data) {
  console.log('data', data);
}
複製程式碼

我們在js的執行環境裡定義了一個全域性方法nativeCallback,native端可以直接執行nativeCallback(123)方法,也就把資料傳給了js。

這種方案是不是有點熟悉,jsonp就是類似的原理:只不過呼叫全域性方法的時機,從伺服器端改成了native端。

JS通知Native

前端常見的協議有:

  1. http/https協議:https://www.baidu.com
  2. 本地file協議: file:///Users/deepred/myproject/index.html

其實我們也可以自定義協議:sslocal://openModal?text=hello,客戶端通過分析這段scheme就能知道web端要呼叫原生的哪些方法,同時資料也通過query引數進行了傳遞。

那web端如何傳送這段scheme給native端呢?

  • 攔截console alert prompt 全域性方法。
alert('sslocal://openModal?text=hello')
複製程式碼

native可以攔截webview中的這些方法,從而呼叫原生方法。

  • 攔截url請求
const ifr = document.createElement('iframe');
ifr.style.display = 'none';
ifr.src = 'sslocal://openModal?text=hello';
document.body.appendChild(ifr);
複製程式碼

web端載入了一個iframe,請求了sslocal://openModal?text=hello, native端通過攔截url請求,從而呼叫原生方法。

使用scheme字串來呼叫方法始終不夠直觀,其實我們還可以向webview裡注入一個js全域性物件,這個全域性物件擁有呼叫native的方法的能力。

  • API注入
// nativeApp是由native端注入的全域性變數
nativeApp.openModal('hello');
複製程式碼

雙向通訊

前面我們介紹的幾種方法,都只能單向通訊。如何進行雙向通訊呢?這時候就需要前端自己實現一個JS-SDK,維護js回撥函式的Map。

首先,我們假設客戶端會向webview中注入一個全域性物件BILIAPP

// BILIAPP是原生端注入的
const BILIAPP = {
  invoke(methodName, param, onSuccessKey, onFailKey) {}
}
複製程式碼

該物件有個invoke方法,接收4個引數:

  • 呼叫的原生方法名
  • 方法引數
  • 成功回撥函式id
  • 失敗回撥函式id

我們沒法直接傳函式給原生方法,所以這裡只能傳回撥函式的id,id對應的實際函式,由前端這邊維護。

sdk.js

let id = 1;

const uuid = () => {
  return `callback_${id++}`;
};

// BILISDK是web端注入的
const BILISDK = {

  // key是回撥函式的id
  // value是回撥函式的值
  callbacks: {

  },

  // 暴露給前端使用的方法,支援Promise
  invokeP(methodName, param) {
    return new Promise((resolve, reject) => {
      const successCb = (data) => {
        resolve(data);
      };
      const failureCb = (data) => {
        reject(data);
      };

      return BILISDK._invoke(methodName, param, successCb, failureCb);
    });
  },

  // 實際真正呼叫原生物件的方法
  _invoke(methodName, param, successCb, failureCb) {
    const onSuccessKey = uuid();
    const onFailKey = uuid();
    // 存入callbacks hash表中
    this.callbacks[onSuccessKey] = successCb;
    this.callbacks[onFailKey] = failureCb;
    // BILIAPP是否注入成功
    BILIAPP && BILIAPP.invoke && BILIAPP.invoke(methodName, JSON.stringify(param), onSuccessKey, onFailKey);
  },

  // 暴露給原生端使用的方法
  invokeFromNative(key, param) {
    if (typeof param === "string") {
      try {
        param = JSON.parse(param)
      } catch (ex) {

      }
    }
    const callback = this.callbacks[key];

    if (callback) {
      callback(param);
    }
  }

}

// 使用BILISDK呼叫原生方法
BILISDK.invokeP('getVersion').then((res) => {
  console.log('res', res);
})
複製程式碼

現在前端調原生方法,不要直接使用BILIAPP.invoke,而是通過BILISDK.invokeP間接呼叫。BILISDK.invokeP支援Promise化,同時維護了一個hash表

const BILISDK = {
  callbacks: {
    'callback_1': function() {},
    'callback_2': function() {},
  },
}
複製程式碼

BILISDK.invokeFromNative是暴露給Native端使用的。當原生方法呼叫完成後,根據成功還是失敗,Native端可以呼叫BILISDK.invokeFromNative(成功或者失敗的id),而這個id就是當初BILIAPP.invoke呼叫時傳進來的id。

通過上面的方法,我們就實現了js -> native -> js 的雙向通訊了。當然理論上,我們還需實現:native -> js -> native 的雙向通訊,但是原理是一樣的,這時客戶端就需要自己實現一個Native-SDK,維護Native端回撥函式的Map。

JS-SDK的接入

前面我們實現的sdk.js,如何引入web站點呢?

把sdk打包成umd規範的js靜態檔案,上傳到cdn或者釋出到npm

  • 在index.html裡面直接通過script標籤引入或者js直接import匯入即可。該方案,前端維護sdk。(維護成本高)
  • 客戶端在初始化一個WebView開啟頁面時,直接注入sdk。該方案,客戶端維護sdk。(優先推薦)

參考

  1. Hybrid App技術解析 -- 原理篇
  2. 小白必看,JSBridge 初探
  3. 2小時搞定移動端混合開發基礎入門