在移动端盛行的年代,技术选型上基本都是混合开发(Hybrid),混合开发是一种开发模式,指使用多种开发模型开发App,通常会涉及到两大类技术:原生Native、Web H5 。
在Hybrid模式下,H5会经常需要使用Native的功能,比如:打开二维码扫描、调用原生页面、获取用户信息等,同时Native也需要向Web端发送推送、更新状态等,而JavaScript是运行在单独的JSContext中(Webview容器、JSCore等),与原生有运行环境的隔离,所以需要有一种机制实现Native端和Web端的双向通信,这就是JSBridge:以JavaScript引擎或Webview容器作为媒介,通过协定协议进行通信,实现Native和Web端双向通信的一种机制。
通过JSBridge,Web端可以调用Native端的Java接口,同样Native端也可以通过JSBridge调用Web端的JavaScript接口,实现彼此的双向调用。

首先,了解下webView,webView是移动端提供的运行JavaScript的环境,是系统渲染Web网页的一个控件,可与页面JavaScript交互,实现混合开发,其中Android和iOS又有些不同:
Android的webView采用的是低版本和高版本使用了不同的webkit内核,4.4后直接使用了Chrome。
iOS中的UIWebView算是自IOS2就有,但性能较差,特性支持较差,WKWebView是iOS8之后的升级版,性能更强特性支持也较好。
WebView控件除了能加载指定的url外,还可以对URL请求、JavaScript的对话框、进度加载、页面交互进行强大的处理,之后会提到拦截请求、执行JS脚本都依赖于此。
Web端和Native可以类似比于Client/Server模式,Web端调用原生接口时,就如同Client向Server端发送一个请求类似,JSB在此充当类似于HTTP协议的角色,实现JSBridge主要是两点:
首先来说Native端调用Web端,这个比较简单,JavaScript作为解释性语言,最大的一个特性就是可以随时随地的通过解释器执行一段JS代码,所以可以将拼接的JavaScript代码字符串,传入JS解析器执行就可以,JS解析器在这里就是webView。
Android 4.4之前只能用loadUrl来实现,并且无法执行回调:
|
1 2 |
String jsCode = String.format("window.showWebDialog('%s')", text); webView.loadUrl("javascript: " + jsCode); |
Android 4.4之后提供了evaluateJavascript来执行JS代码,并且可以获取返回值执行回调:
|
1 2 3 4 5 6 7 |
String jsCode = String.format("window.showWebDialog('%s')", text); webView.evaluateJavascript(jsCode, new ValueCallback<String>() { @Override public void onReceiveValue(String value) { } }); |
iOS的UIWebView使用stringByEvaluatingJavaScriptFromString :
|
1 2 |
NSString *jsStr = @"执行的JS代码"; [webView stringByEvaluatingJavaScriptFromString:jsStr]; |
iOS的WKWebView使用evaluateJavaScript :
|
1 2 3 |
[webView evaluateJavaScript:@"执行的JS代码" completionHandler:^(id _Nullable response, NSError * _Nullable error) { }]; |
Web调用Native端主要有两种方式
URL Schema是类URL的一种请求格式,格式如下:
|
1 |
<protocol>://<host>/<path>?<query>#fragment |
我们可以自定义JSBridge通信的URL Schema ,比如: jsbridge://showToast?text=hello
Native加载WebView之后,Web发送的所有请求都会经过WebView组件,所以Native可以重写WebView里的方法,从来拦截Web发起的请求,对请求的格式进行判断:

a 标签 ;location.href ;iframe.src ;ajax请求 。这些方法,a标签需要用户操作,location.href 可能会引起页面的跳转丢失调用,发送ajax请求Android没有相应的拦截方法,所以使用iframe.src 是经常会使用的方案:
这种方式从早期就存在,兼容性很好,但是由于是基于URL的方式,长度受到限制而且不太直观,数据格式有限制,而且建立请求有时间耗时。
这个方法会通过webView提供的接口,App将Native的相关接口注入到JS的Context(window)的对象中,一般来说这个对象内的方法名与Native相关方法名是相同的,Web端就可以直接在全局window下使用这个暴露的全局JS对象,进而调用原生端的方法。
这个过程会更加简单直观,不过有兼容性问题,大多数情况下都会使用这种方式:
Android(4.2+)提供了addJavascriptInterface注入:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 注入全局JS对象 webView.addJavascriptInterface(new NativeBridge(this), "NativeBridge"); class NativeBridge { private Context ctx; NativeBridge(Context ctx) { this.ctx = ctx; } // 增加JS调用接口 @JavascriptInterface public void showNativeDialog(String text) { new AlertDialog.Builder(ctx).setMessage(text).create().show(); } } |
在Web端直接调用这个方法即可:
|
1 |
window.NativeBridge.showNativeDialog('hello'); |
iOS的UIWebView提供了JavaSciptCore;
iOS的WKWebView提供了WKScriptMessageHandler ;
上面已经说到了Native、Web间双向通信的两种方法,但站在一端而言还是一个单向通信的过程,比如站在Web的角度:Web调用Native的方法,Native直接相关操作但无法将结果返回给Web,但实际使用中会经常需要将操作的结果返回,也就是JS回调。
所以在对端操作并返回结果,有输入有输出才是完整的调用,那如何实现呢?
其实,基于之前的单向通信就可以实现,在一端调用的时候,在参数中加一个callbackId标记对应的回调,对端接收到调用请求后,进行实际操作,如果带有callbackId,对端再进行一次调用,将结果、
callbackId回传回来,这端根据callbackId匹配相应的回调,将结果传入执行就可以了。

可以看到实际上还是通过两次单向通信实现的。
以Android,在Web端实现带有回调的JSB调用为例:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
// Web端代码: <body> <div> <button id="showBtn">获取Native输入,以Web弹窗展现</button> </div> </body> <script> let id = 1; // 根据id保存callback const callbackMap = {}; // 使用JSSDK封装调用与Native通信的事件,避免过多的污染全局环境 window.JSSDK = { // 获取Native端输入框value,带有回调 getNativeEditTextValue(callback) { const callbackId = id++; callbackMap[callbackId] = callback; // 调用JSB方法,并将callbackId传入 window.NativeBridge.getNativeEditTextValue(callbackId); }, // 接收Native端传来的callbackId receiveMessage(callbackId, value) { if (callbackMap[callbackId]) { // 根据ID匹配callback,并执行 callbackMap[callbackId](value); } } }; const showBtn = document.querySelector('#showBtn'); // 绑定按钮事件 showBtn.addEventListener('click', e => { // 通过JSSDK调用,将回调函数传入 window.JSSDK.getNativeEditTextValue(value => window.alert('Natvie输入值:' + value)); }); </script> // Android端代码 webView.addJavascriptInterface(new NativeBridge(this), "NativeBridge"); class NativeBridge { private Context ctx; NativeBridge(Context ctx) { this.ctx = ctx; } // 获取Native端输入值 @JavascriptInterface public void getNativeEditTextValue(int callbackId) { MainActivity mainActivity = (MainActivity)ctx; // 获取Native端输入框的value String value = mainActivity.editText.getText().toString(); // 需要注入在Web执行的JS代码 String jsCode = String.format("window.JSSDK.receiveMessage(%s, '%s')", callbackId, value); // 在UI线程中执行 mainActivity.runOnUiThread(new Runnable() { @Override public void run() { mainActivity.webView.evaluateJavascript(jsCode, null); } }); } } |
以上代码简单实现了一个demo,在Web端点击按钮,会获取Native端输入框的值,并将值以Web端弹框展现,这样就实现了Web->Native带有回调的JSB调用,同理Native->Web也是同样的逻辑,不同的只是callback保存在Native端罢了,在此就不详细论述了。
可以看到,实现一个完整的JSBridge还是挺麻烦的,还需要考虑低端机型的兼容问题、同步异步调用问题,好在已经有开源的JSBridge供我们直接使用了:
以DSBridge-Android为例:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
// Web端代码 <body> <div> <button id="showBtn">获取Native输入,以Web弹窗展现</button> </div> </body> // 引入SDK <script src="https://unpkg.com/dsbridge@3.1.3/dist/dsbridge.js"></script> <script> const showBtn = document.querySelector('#showBtn'); showBtn.addEventListener('click', e => { // 注意,这里代码不同:SDK在全局注册了dsBridge,通过call调用Native方法 dsBridge.call('getNativeEditTextValue', '', value => { window.alert('Native输入值' + value); }) }); </script> // Android代码 // 使用dwebView替换原生webView dwebView.addJavascriptObject(new JsApi(), null); class JSApi { private Context ctx; public JSApi (Context ctx) { this.ctx = ctx; } @JavascriptInterface public void getNativeEditTextValue(Object msg, CompletionHandler<String> handler) { String value = ((MainActivity)ctx).editText.getText().toString(); // 通过handler将value传给Web端,实现回调的JSB调用 handler.completed(value); } } |
可以看到,代码被精简了很多,其它更多使用直接看文档就可以。
至此,大家应该对JSBridge的原理、使用有了一个比较深入的认知,这里对文章做一个总结:
Hybrid开发是目前移动端开发的主流技术选项,其中,Native和Web端的双向通信就离不开JSBridge;
其中,Native调用Web端是直接在JS的Context直接执行JS代码,Web端调用Native端有两种方法,一种是基于URL Schema的拦截操作,另一种是向JS的Context(window)注入Api,其中注入Api是目前最好的选择。完整的调用是双向通信,需要一个回调函数,技术实现上就是使用了两次单向通信。
其次,相对于造轮子,更推荐使用目前已经开源的JSBridge:DSBridge、jsBridge。
记录一下,项目中的运用实例,方便以后参考:(PS:项目实例基于nuxt2+vue2为例)
1.桥接的方法,dsbrisge.js代码:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
/* * 桥接的方法 * dsBridge.call('约定的方法名称') // 调用原始的方法 * dsBridge.register('约定好的名称',function(){}) //注册方法给原始使用 * 时间2019/1/9 */ var bridge = { default: this, call: function (b, a, c) { var e = ""; "function" == typeof a && (c = a, a = {}); a = { data: void 0 === a ? null : a }; if ("function" == typeof c) { var g = "dscb" + window.dscb++; window[g] = c; a._dscbstub = g } a = JSON.stringify(a); if (window._dsbridge) e = _dsbridge.call(b, a); else if (window._dswk || -1 != navigator.userAgent.indexOf("_dsbridge")) e = prompt("_dsbridge=" + b, a); return JSON.parse(e || "{}").data }, register: function (b, a, c) { c = c ? window._dsaf : window._dsf; window._dsInit || (window._dsInit = !0, setTimeout(function () { bridge.call("_dsb.dsinit") }, 0)); "object" == typeof a ? c._obs[b] = a : c[b] = a }, registerAsyn: function (b, a) { this.register(b, a, !0) }, hasNativeMethod: function (b, a) { return this.call("_dsb.hasNativeMethod", { name: b, type: a || "all" }) }, disableJavascriptDialogBlock: function (b) { this.call("_dsb.disableJavascriptDialogBlock", { disable: !1 !== b }) } }; if (!window._dsf) { var b = { _dsf: { _obs: {} }, _dsaf: { _obs: {} }, dscb: 0, dsBridge: bridge, close: function () { bridge.call("_dsb.closePage") }, _handleMessageFromNative: function (a) { var e = JSON.parse(a.data), b = { id: a.callbackId, complete: !0 }, c = this._dsf[a.method], d = this._dsaf[a.method], h = function (a, c) { b.data = a.apply(c, e); bridge.call("_dsb.returnValue", b) }, k = function (a, c) { e.push(function (a, c) { b.data = a; b.complete = !1 !== c; bridge.call("_dsb.returnValue", b) }); a.apply(c, e) }; if (c) h(c, this._dsf); else if (d) k(d, this._dsaf); else if (c = a.method.split("."), !(2 > c.length)) { a = c.pop(); var c = c.join("."), d = this._dsf._obs, d = d[c] || {}, f = d[a]; f && "function" == typeof f ? h(f, d) : (d = this._dsaf._obs, d = d[c] || {}, (f = d[a]) && "function" == typeof f && k(f, d)) } } }, a; for (a in b) window[a] = b[a]; bridge.register("_hasJavascriptMethod", function (a, b) { b = a.split("."); if (2 > b.length) return !(!_dsf[b] && !_dsaf[b]); a = b.pop(); b = b.join("."); return (b = _dsf._obs[b] || _dsaf._obs[b]) && !!b[a] }) } export default dsBridge |

2.在plugins文件夹的mainUse.js文件中,全局注册引用:
|
1 2 |
import dsbridge from "../utils/dsbridge"; Vue.prototype.dsBridge = dsbridge; |

3.在页面中如何具体调用:
桥接的方法,有两种,①dsBridge.call(‘约定的方法名称’) //调用原始的方法;②dsBridge.register(‘约定好的名称’,function(){}) //注册方法给原始使用 。
|
1 |
this.dsBridge.call("orderPayAndBindSuccess", this.uid); // 支付成功通知app,第一个参数和app原生的交互协议名称;第二个参数JS回调callbackId |
参考博客:
深入浅出JSBridge:从原理到使用 https://zhuanlan.zhihu.com/p/438763800
dsbridge (android、ios、js三端互通工具) https://www.jianshu.com/p/356492d94089
from:https://blog.csdn.net/qq_26780317/article/details/126269925