原生App與javascript交互之JSBridge接口原理、設計與實現

CatalinaMGY 7年前發布 | 47K 次閱讀 移動開發 JavaScript JsBridge

前期調研

調研對象:

支付寶,微信,云之家

調研文檔:

Android中JS與Java的極簡交互庫 SimpleJavaJsBridge

設計需求

  1. 閱讀類型的業務功能頁面需要由前端H5實現,需要做到服務端可控;

  2. 頁面界面更改減少重新發布新版本的頻率;

  3. 功能頁面部分原型需求無法實現,需要原生功能支持;

  4. 對未來業務功能的拓展,方便迭代;

作用和意義

  1. 定制化JSBridge實際上是拓展NativeApp的hybrid程度, 參照微信和支付寶,可打造APP強力的生態圈;

  2. jsBridge在支付,錢包,媒體拓展,圖片處理,活動頁面,用戶地理位置網絡狀態都能得到原生強有力支持;

  3. 對于閱讀性頁面有更多拓展;

優秀的通信設計方案

  1. 前端和Native對對方的細節知道的越少越好,減少耦合度,暴露的接口盡量控制在5個以內;

  2. js與Native之間的通信,最好定義一套通信協議或者規則,減少js代碼為兼容不同系統而過多if;

  3. 主動發送消息給對方時,對方盡量對該消息進行反饋,即使無需求對某些功能做反饋,減少if判斷的兼容代碼;

實現方式(交互形式)

Native 調用 JS

使用前端暴露在window下的一個方法或者一個對象的方法;

_handlerFromApp(message)
JSBridge._handlerFromApp(message)

方法名: handlerFromApp
參數:

message: {
  cbId  : "cb_(:id)_(:timeStamp)",      //回調函數的id
  status: 0,                            //狀態數據 (0:失敗, 1:成功)
  msg   : "ok",                         //反饋的消息
  data  : {
    //...                               //一些處理后的數據
  } 
}

以下提供的部分參考方法

未對其進行真實測試,因為我使用的是iframe的方法,但原理幾乎相同

建議封裝后提供給Native開發工程師放入對應的APP包中,在webView讀取頁面的時候用對應的Native語言注入頁面,避免頁面在前端導入被抓取;

var doc = JSBridge || window;
var uniqueId = 1;
var invokeCBMap = {};
var listenCBMap = {};

// function _send(type, funcName, data, cb) { var id = 'cb' + (uniqueId++) + '_' + new Date().getTime(); data.cbId = _id; if (type == 'invoke') invokeCBMap[_id] = cb; else if (type == 'listen') listenCBMap[_id] = cb; doctype; } doc._handlerFromApp = function(msg) { var _id = msg.cbId, callback; if (_id) { callback = invokeCBMap[_id] || listenCBMap[_id]; if (callback) { delete msg.cbId; callback(msg.data); delete invokeCBMap[id]; } else { console.error('不存在該回調方法'); } } }</code></pre>

JS調用Native

以下只介紹前兩個方法,第三個和第二個比較類似

  • A. Native暴露一個含有通信方法的類給web調用

  • B. Native攔截iframe請求

  • C. Native攔截prompt彈出框

A 一個包含調用方法的類

iOS : 可使用javascriptCore

Android: 直接使用WebView的addJavascriptInterface方法

將一個js對象綁定到一個Native類,在類中實現相應的函數,當js需要調用Native的方法時,只需要直接在js中通過綁定的對象調用相應的函數

確定對象名稱: (:AppName)JSBridge

Native提供的對象含有的方法:

  • invoke(funcName, data)

  • listen(funcName, data)

invoke :用于web頁面調用Native私有方法的通用方法

參數 : funcName , data

funcName :對應為Native內部私有方法的方法名或映射

data :web傳遞給Native的必要數據

data 數據結構如下:

{
  cbId : "cb(:id)_(:timeStamp)",  //回調函數的id
  msg  : {}                        //提供給使用方法執行的一些參數
}
/* 
  //1.拿wx參考為例
  wx.previewImg({
    current: 'http://xxx_1.png',
    urls   : [
      'http: //xxx_0.png',
      'http: //xxx_1.png',
      'http: //xxx_2.png',
      'http: //xxx3.png',
    ]
  });
  //2.因為wx對jsbridge進行了一次封裝,jssdk, 而我們在未封裝時應該如下使用
  JSBridge.invoke('imagePreview', {
    cbId : "cb(:id)_(:timeStamp)",
    msg : {
      current: 'http://xxx_1.png',
      urls   : [
        'http: //xxx_0.png',
        'http: //xxx_1.png',
        'http: //xxx_2.png',
        'http: //xxx_3.png',
      ]
    }
  });/</code></pre> 
  

那么當調用之后,Native執行完成對應的私有方法后,執行一次我們提供的回調接口,以下是javascript的語法,請Native開發工程師對應修改

JSBridge.handlerFromApp({
  cbId  : "cb_(:id)_(:timeStamp)", //web傳給Native的cbId
  status: 1,                       //狀態數據 (0:失敗, 1:成功)
  msg   : "預覽成功", 
  data  : {} 
});

listen 是一個用于web頁面監聽Native方法實現的通用方法

使用環境: 不屬于web頁面上的操作。當用戶直接操作Native上的功能來影響或發送數據給web,或者操作的功能需要用到web頁面上的數據,我們需要告知Native我們希望能收到回調;

例子:

微信監聽分享操作

  1. 分享的內容是web上的內容(標題,描述,圖片);

  2. 獲取分享操作是否完成和分享操作的數據收集;

  3. 分享按鈕是原生APP提供;

數據結構和操作與 invoke 相似,對應Native開發哥們接收到listen操作后需要存儲一個映射,在被監聽的操作實現上判斷是不是需要執行web端提供的回調接口;

注意: 有關 java addJavascriptInterface 的使用有漏洞,詳情見參考第二條鏈接,未驗證,僅供讀者自行權衡;

B iframe的魔法

由于Native App可以監聽webview的請求,所以web端通過創建一個隱藏的iframe,請求商定后的統一協議來發送數據給Native App;

function createIframeCall(url) {
  setTimeout(function() {
    var iframe = document.createElement('iframe');
    iframe.style.width = '1px';
    iframe.style.height = '1px';
    iframe.style.display = 'none';
    iframe.src = url;
    document.body.appendChild(iframe);
    setTimeout(function() {
        document.body.removeChild(iframe);
    }, 100);
  }, 0);
}

url 格式:

(:scheme)://register_type?func=(:funcName)&cbId=(:cbId)&data={...}&verifyTimeStamp=(:new Date().getTime())

scheme :協議,可用appName,兩端商定,例如weixin,alipayjsbridge

register_type : 注冊形式,即 invoke 還是 listen

funcName : Native內的方法名或映射

cbId :見上文

data :詳細數據

verifyTimeStamp :驗證的時間參數,不必須

;(function() {
    if (window.ZaihuJSBridge) return;
    var CUSTOM_PROTOCOL_SCHEME = 'zaihu';
    var REGISTER_INVOKE = 'invoke';
    var REGISTER_LISTEN = 'listen';
    var uniqueId = 1;
    var invokeCbMap = {};
    var listenCbMap = {};
    function dataHandler(type, funcName, data, cb) {
      var register_type = '';
      switch (type) {
        case 'invoke': 
          register_type = REGISTER_INVOKE;break;
        case 'listen': 
          register_type = REGISTERLISTEN;break;
        default: break;
      }
      var cbId = '';
      if (cb) {
        cbId = 'cb' + (uniqueId++) + '_' + new Date().getTime();
        invokeCBMap[cbId] = cb;
      }
      var dataStr = '';
      if (data) dataStr = encodeURIComponent(JSON.stringify(data));
      var paramStr = CUSTOM_PROTOCOL_SCHEME + '://' + register_type + '?func=' + funcName + (cbId ? ('&cbId=' + cbId): '') + (data ? ('&data=' + dataStr): '');
      createIframeCall(paramStr);
        }
    function _invoke(nativeFuncName, data, cb) {
      dataHandler('invoke', nativeFuncName, data, cb);
    }

function _listen(h5FuncName, data, cb) {
  dataHandler('listen', h5FuncName, data, cb);
}
function _handlerFromZaihu(msg) {
  var data = JSON.parse(msg);
  var cbId = data.cbId;
  var cb = invokeCBMap[cbId] || listenCBMap[cbId];
  if (cb) {
    delete data.cbId && cb(data) && delete invokeCBMap[cbId];
  }
}
  var app;

  app = {
    version: '0.1',
    invoke: _invoke,
    on: _listen,
    log: _log,
    author: '伊吾魚O(∩_V)O',
    // private
    _handlerFromApp: _handlerFromApp
  };
  window.JSBridge = app;

})()</code></pre>

協作

  • 需要Native開發兄弟在webview開啟時候為頁面注入jsbridge.js代碼并執行(防止被前端瀏覽器直接查看源代碼了解app的代碼邏輯)

  • 獲取參數執行對應的功能后,執行回調

頁面前期準備

1.app打開webview

2.loadUrl(頁面url)

3.監聽webview開始,并執行一段js代碼將包內的jsbridge.js文件引入頁面中;

功能業務邏輯

  1. web頁面調用請求接口

    jsbridge.invoke(funcName, data);(A方法:Native提供,B&C方法: 前端實現);

  2. 接口調用原生功能

  3. 原生功能完成后執行回調

比較

A:android曝 安全漏洞 ,但相對來說實現簡單,調用方式容易,且傳遞參數,無需前端搭建jsbridge,只需要封裝易用的sdk,App不需要讀取本地靜態js文件;

B: iframe規定協議,規范統一,需要前端實現jsbridge和封裝sdk, iframe通過url的方式,數據統一為字符串格式,數據量受限制,兩端要轉義字符;

C: prompt在一些安卓設備受系統劫持,監聽prompt兼容性需要測試,也是字符串形式,數據量不受限,需要轉義字符;

還有很多參考頁面未注明,以及文中有問題的地方歡迎提出。

 

來自:https://segmentfault.com/a/1190000008012111

 

 本文由用戶 CatalinaMGY 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!