资讯专栏INFORMATION COLUMN

原生App与javascript交互之JSBridge接口原理、设计与实现

Lyux / 1262人阅读

摘要:相关参考中与之间相互调用的实现实现了与相同的机制的对象注入漏洞解决方案存在的意义

前期调研

调研对象:
支付宝,微信,云之家

调研文档:
Android中JS与Java的极简交互库 SimpleJavaJsBridge

设计需求

阅读类型的业务功能页面需要由前端H5实现,需要做到服务端可控;

页面界面更改减少重新发布新版本的频率;

功能页面部分原型需求无法实现,需要原生功能支持;

对未来业务功能的拓展,方便迭代;

作用和意义

定制化JSBridge实际上是拓展NativeApp的hybrid程度, 参照微信和支付宝,可打造APP强力的生态圈;

jsBridge在支付,钱包,媒体拓展,图片处理,活动页面,用户地理位置网络状态都能得到原生强有力支持;

对于阅读性页面有更多拓展;

优秀的通信设计方案

前端和Native对对方的细节知道的越少越好,减少耦合度,暴露的接口尽量控制在5个以内;

js与Native之间的通信,最好定义一套通信协议或者规则,减少js代码为兼容不同系统而过多if;

主动发送消息给对方时,对方尽量对该消息进行反馈,即使无需求对某些功能做反馈,减少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;
  doc[type](funcName, data);
}
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("不存在该回调方法");
    }
  }
}
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私有方法的通用方法
参数: funcNamedata
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: //xxx_3.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",
      ]
    }
  });
*/

那么当调用之后,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我们希望能收到回调;
例子:
微信监听分享操作

分享的内容是web上的内容(标题,描述,图片);

获取分享操作是否完成和分享操作的数据收集;

分享按钮是原生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 = REGISTER_LISTEN;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;
})()
协作

需要Native开发兄弟在webview开启时候为页面注入jsbridge.js代码并执行(防止被前端浏览器直接查看源代码了解app的代码逻辑)

获取参数执行对应的功能后,执行回调

页面前期准备

1.app打开webview
2.loadUrl(页面url)
3.监听webview开始,并执行一段js代码将包内的jsbridge.js文件引入页面中;

功能业务逻辑

web页面调用请求接口
jsbridge.invoke(funcName, data);(A方法:Native提供,B&C方法: 前端实现);

接口调用原生功能

原生功能完成后执行回调

比较

A:android曝安全漏洞,但相对来说实现简单,调用方式容易,且传递参数,无需前端搭建jsbridge,只需要封装易用的sdk,App不需要读取本地静态js文件;

B: iframe规定协议,规范统一,需要前端实现jsbridge和封装sdk, iframe通过url的方式,数据统一为字符串格式,数据量受限制,两端要转义字符;

C: prompt在一些安卓设备受系统劫持,监听prompt兼容性需要测试,也是字符串形式,数据量不受限,需要转义字符;

还有很多参考页面未注明,以及文中有问题的地方欢迎提出。

相关参考
iOS中Objective-C与JavaScript之间相互调用的实现(实现了与Android相同的机制)
Android WebView的Js对象注入漏洞解决方案(JSBridge存在的意义)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/86758.html

相关文章

  • 跨平台技术演进

    摘要:接下来,我将从原理优缺点等方面为大家分享跨平台技术演进。小程序年是微信小程序飞速发展的一年,年,各大厂商快速跟进,已经有了很大的影响力。下面,我们以微信小程序为例,分析小程序的技术架构。 前言 大家好,我是simbawu ,@BooheeFE Team Leader,关于这篇文章,有问题欢迎来这里讨论。 随着移动互联网的普及和快速发展,手机成了互联网行业最大的流量分发入口。以及随着5G...

    魏宪会 评论0 收藏0
  • 跨平台技术演进

    摘要:接下来,我将从原理优缺点等方面为大家分享跨平台技术演进。小程序年是微信小程序飞速发展的一年,年,各大厂商快速跟进,已经有了很大的影响力。下面,我们以微信小程序为例,分析小程序的技术架构。 前言 大家好,我是simbawu ,@BooheeFE Team Leader,关于这篇文章,有问题欢迎来这里讨论。 随着移动互联网的普及和快速发展,手机成了互联网行业最大的流量分发入口。以及随着5G...

    MasonEast 评论0 收藏0

发表评论

0条评论

Lyux

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<