调研对象:
支付宝,微信,云之家
调研文档:
Android中JS与Java的极简交互库 SimpleJavaJsBridge阅读类型的业务功能页面需要由前端H5实现,需要做到服务端可控;
页面界面更改减少重新发布新版本的频率;
功能页面部分原型需求无法实现,需要原生功能支持;
对未来业务功能的拓展,方便迭代;
定制化JSBridge实际上是拓展NativeApp的hybrid程度, 参照微信和支付宝,可打造APP强力的生态圈;
jsBridge在支付,钱包,媒体拓展,图片处理,活动页面,用户地理位置网络状态都能得到原生强有力支持;
对于阅读性页面有更多拓展;
前端和Native对对方的细节知道的越少越好,减少耦合度,暴露的接口尽量控制在5个以内;
js与Native之间的通信,最好定义一套通信协议或者规则,减少js代码为兼容不同系统而过多if;
主动发送消息给对方时,对方尽量对该消息进行反馈,即使无需求对某些功能做反馈,减少if判断的兼容代码;
使用前端暴露在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('不存在该回调方法'); } } }
以下只介绍前两个方法,第三个和第二个比较类似
A. Native暴露一个含有通信方法的类给web调用
B. Native拦截iframe请求
C. Native拦截prompt弹出框
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: //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
的使用有漏洞,详情见参考第二条链接,未验证,仅供读者自行权衡;
由于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兼容性需要测试,也是字符串形式,数据量不受限,需要转义字符;
还有很多参考页面未注明,以及文中有问题的地方欢迎提出。