Android JSBridge
1. JSBridge 简介
1.1. 什么是JSBridge
随着HTML5的不断普及及优化,其在移动端开发的应用也越来越多,在开发中我们经常需要web和native进行交互,一般我们都是使用JSBridge来实现。通过JSBridge,native可以调用web的js代码,web也可以调用指定的终端代码。
1.2. JSBridge的实现方式
目前关于JSBridge的终端实现,方法很多。具体实现方法有:
ShouldOverrideUrlLoading
页面将伪协议以主资源路径的方式传入(例如新建一个iframe),然后终端通过WebViewClient的ShouldOverrideUrlLoading方法来拦截主资源请求并解析伪协议请求。
ShouldInterceptRequest
页面将伪协议以子资源路径的方式传入(例如新建一个script),然后终端通过WebViewClient的ShouldInterceptRequest方法来拦截子资源请求并解析伪协议请求。
Prompt 系列
页面通过调用window的prompt、alert、confirm等方法并将伪协议内容传入,然后终端通过WebChromeClient的onJsPrompt、onJsAlert、onJsConfirm方法来处理伪协议请求。
Console
页面通过调用console.log方法并将伪协议内容传入,然后终端通过WebChromeClient的onConsoleMessage方法来处理伪协议请求。
addjavascriptinterface
直接通过WebView的addjavascriptinterface方法,给webview添加JS的支持,这种方式由于存在致命的远程代码执行漏洞,目前基本被禁用了。
2. AAF的JSBridge
我们的JSBridge使用shouldOverrideUrlLoading的方式实现,然后为了降低开发和维护的成本,业务JSBridge来维护各自的业务逻辑。由平台负责维护底层JSBridge在H5和终端之间的交互。具体内容可以 点击链接 https://github.com/bihe0832/AndroidAppFactory/tree/master/LibWebview/src/main/java/com/bihe0832/android/lib/webview/jsbridge 查看对应源码
2.1. JSBridge协议
我们JSBridge的协议为:
jsb://方法名/系列号/回调方法?key=value&key2=value2……
调用时url参数值记得做url encode,系列号字段为必须,回调方法可以省略,省略时则不回调。具体事例如:
jsb://toast/1/ZixieJSBridge.callback?text=%E8%BF%99%E6%98%AF%E4%B8%80%E4%B8%AA%E6%B5%8B%E8%AF%95&duration=1
对于有callback方法的调用,客户端在方法执行完毕后会回调这个callback方法,并将返回结果以json的形式返回回来。 例如调用了获取指定应用信息:
call('getAppInfo', {packagename:"com.bihe0832.android.test"}, (res) => { console.log(res) })
返回的数据可能是下面这样的(注意是json):
{result: 0, data: "{"com.bihe0832.android.test":{"install":1,"verCode":17,"verName":"1.2.0"}}", method: "getAppInfo", seqid: 3}
2.2. 如何调试JSBridge接口
这里简单介绍一下自己测试的事例。
使用内置的webview打开网页
初始化Web端的JSBridge的调用
手机连接电脑,开启调试模式,然后在chrome浏览器里面通过inspect打开webview调试页面,如下图:
点击当前打开的webview页面地址下方的inspec超链接,打开详细调试页面
初始化web端的JSBridge实例
在新打卡页面的Console中输入下面的JS代码,初始化web端的JSBridge实例。如果是实际开发,可以在页面中引入js:
https://cdn.bihe0832.com/js/jsbridge.js
即可直接调用,具体可以参考页面:
https://cdn.bihe0832.com/tools/jsbridge/index.html
//注册全局回调必须 window.ZixieJSBridge = window.ZixieJSBridge || {}; var framesPool = []; function createNewFrame() { var frame = document.createElement("iframe"); framesPool.push(frame); frame.style.cssText = "position:absolute;left:0;top:0;width:0;height:0;visibility:hidden;"; frame.frameBorder = "0"; document.body.appendChild(frame); return frame; } var _callWithScheme = callWithScheme = function (url) { console.log("JsBridge.callWithScheme: ", url); // @debug var frame; for (var i = 0; frame = framesPool[i]; i++) { if (!frame._busy) { break; } } if (!frame || frame._busy) { frame = createNewFrame(); } frame._busy = true; frame.src = url; setTimeout(function () { frame._busy = false; }, 0); }; var allowBatchCall = true; var seq = 1; var map = {}; var pool = []; var timer = 0; function callSingle(name, args, callback, callbackChain) { var url = ["jsb:/", name, seq, "ZixieJSBridge.callback?"].join("/"), params = []; for (var key in args) { params.push(encodeURIComponent(key) + "=" + encodeURIComponent(args[key] + "")); } url += params.join("&"); map[seq++] = { callback: callback, callbackChain: callbackChain }; _callWithScheme(url); } function callBatch() { timer = 0; if (pool.length == 1) { var one = pool[0]; callSingle(one.name, one.args, one.callback, one.callbackChain); } else { var params = []; for (var i = 0, one; one = pool[i]; i++) { if (one.args) { for (var p in one.args) { if(one.args.hasOwnProperty(p)) { if (one.args[p]) { one.args[p] = encodeURIComponent(one.args[p]); } } } } params.push({ method: one.name, seqid: seq, args: one.args, callback: "ZixieJSBridge.callback" }); map[seq++] = { callback: one.callback }; } var url = ["jsb://callBatch", seq++, "ZixieJSBridge.callback?param="].join("/"); url += encodeURIComponent(JSON.stringify(params)); _callWithScheme(url); } pool = []; } var call = _call = function (name, args, callback) { console.log("JsBridge._call: ", name, args); // @debug args = args || {}; var callbackChain = [].slice.call(arguments, 3); if (allowBatchCall) { pool.push({ name: name, args: args, callback: callback, callbackChain: callbackChain }); !timer && (timer = setTimeout(callBatch, 0)); } else { callSingle(name, args, callback, callbackChain); } }; var callback = function (args) { console.log("ZixieJSBridge.callback: ", args); // @debug var one, res, callbackChain; if (map[args.seqid]) { one = map[args.seqid]; callbackChain = one.callbackChain; res = one.callback && one.callback(args); delete map[args.seqid]; } if (res && typeof res == "object") { call.apply(null, [res.name, res.args || {}].concat(callbackChain || [])); } }; ZixieJSBridge.callback = callback;
当在页面中完成Web端的JSBridge实例的实例化以后,就可以调用新增的接口,测试效果,例如:
call('getAppInfo', {packagename:"com.bihe0832.android.test"}, (res) => { console.log(res) })
详细返回截图如下:
至此就完成了一个终端JSBridge接口的调试。