顾名思义,JS Bridge 的意思就是桥,这是一个连接 JS 和 Native 的桥接,也是 Hybrid App 里面的核心。一般分为 JS 调用 Native 和 Native 主动调用 JS 两种形式。
URL Scheme介绍
URL Scheme 是一种特殊的 URL,一般用于在 Web 端唤醒 App,甚至跳转到 App 的某个页面,比如在某个手机网站上付款的时候,可以直接拉起支付宝支付页面。
在浏览器里面通过修改 window.location.href
为 weixin://
,系统就会提示你是否要打开微信。设置为 bixin://
就会帮你唤起手机的比心。
一个 URI 的组成结构如下:
通常情况下,App 安装后会在手机系统上注册一个 Scheme,比如 bixin://
这种,所以我们在手机浏览器里面访问这个 Scheme 地址,系统就会唤起我们的 App。
JS 调用 Native
JS 调用 Native 通信大致有三种方法:
- 拦截 Scheme
- 弹窗拦截
- 注入 JS 上下文
拦截 Scheme
就像JS 和 Java 之间是通过 http/https
接口来获取数据一样,JS也可以这样和客服端进行通信。
客户端是可以拦截页面发起的请求的,如果我们请求一个不存在的地址,上面带了一些参数,通过参数告诉客户端我们需要调用的功能。
比如我要调用扫码功能:
js
axios.get('http://bixin?func=scan&callbackId=yyyy')
客户端可以拦截这个请求,去解析参数上面的 func
来判断当前需要调起哪个功能。客户端调起扫码功能之后,会获取 WebView
上面的 callbacks
对象,根据 callbackId
回调它。
基于上面的例子,我们可以把域名和路径当做通信标识,参数里面的 func
当做指令,callbackid
当做回调函数,其他参数当做数据传递。对于不满足条件的 http 请求不应该拦截。
JS侧
JS还有很多种方法可以发起请求:
- 使用 a 标签跳转
js
<a href="bixin://">点击我打开比心</a>
- 重定向
js
location.href = "bixin://"
- iframe 跳转
js
const iframe = document.createElement("iframe");
iframe.src = "bixin://"
iframe.style.display = "none"
document.body.appendChild(iframe)
目前使用最广泛的是 iframe 跳转
以Android举例
在 Android 侧可以用 shouldOverrideUrlLoading
来拦截 url
请求。
java
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith("bixin")) {
// 拿到调用路径后解析调用的指令和参数,根据这些去调用 Native 方法
return true;
}
}
拦截 URL Scheme 解析参数的形式,会存在以下几个问题。
- 连续续调用
location.href
会出现消息丢失,因为WebView
限制了连续跳转,会过滤掉后续的请求。 - URL 会有长度限制,一旦过长就会出现信息丢失
弹窗拦截
通过在JS中调用以下方法,在Android和IOS中可进行对应的拦截
以Android举例
这种方式是利用弹窗会触发 WebView
相应事件来拦截的。一般是在 setWebChromeClient
里面的 onJsAlert
、onJsConfirm
、onJsPrompt
方法拦截并解析他们传来的消息。
java
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
// js执行的是prompt('bridge://yupaopao.com?message=' + encodeMessageJson, "")
Uri uri = Uri.parse(message);
// 如果url的协议 = 预先约定的 bridge 协议
if ( uri.getScheme().equals("bridge")) {
// 然后再解析uri的其他参数,例如action来决定执行具体的方法
return true;
}
return super.onJsPrompt(view, url, message, defaultValue, result);
}
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return super.onJsAlert(view, url, message, result);
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return super.onJsConfirm(view, url, message, result);
}
注入上下文
这种方式不依赖拦截,主要是通过 WebView
向 JS 的上下文注入对象和方法,可以让 JS 直接调用原生。
以Android举例
安卓4.2之前注入 JS 一般是使用 addJavascriptInterface
。
java
public void addJavascriptInterface() {
webView.addJavascriptInterface(new PickerJSBridge(), "PickerBridge");
}
private class PickerJSBridge {
public void _pick(...) {
}
}
在 JS 里面调用:
js
window.PickerBridge._pick(...)
在 Android4.2 之后提供了 @JavascriptInterface
注解,暴露给 JS 的方法必须要带上这个。 所以前面的 _pick 方法需要带上这个注解。
java
private class PickerJSBridge {
@JavascriptInterface
public void _pick(...) {
}
}
Native 调用 JS
Native 调用 JS 一般就是直接 JS 代码字符串,有些类似我们调用 JS 中的 eval
去执行一串代码。一般有 loadUrl
、evaluateJavascript
等几种方法,这里逐一介绍。 但是不管哪种方式,客户端都只能拿到挂载到 window
对象上面的属性和方法。
以Android举例
在 Android 里面需要区分版本,在安卓4.4之前的版本支持 loadUrl,使用方式类似我们在 a
标签的 href
里面写 JS 脚本一样,都是 javascript:xxx
的形式。这种方式无法直接获取返回值。
java
webView.loadUrl("javascript:foo()")
在安卓4.4以上的版本一般使用 evaluateJavascript
这个 API 来调用。这里需要判断一下版本。
java
if (Build.VERSION.SDK_INT > 19) //see what wrapper we have
{
webView.evaluateJavascript("javascript:foo()", null);
} else {
webView.loadUrl("javascript:foo()");
}
JS Bridge 设计
我们bixin的 Bridge 通信主要是根据prompt
弹窗的拦截的来实现的,下面的是几个基础 API:
- call(action, params, callback, needShell):这个是调用 Native 功能的方法,传模块名、参数、回调函数给 Native,needShell是返回的结果中不会带 code/result/msg 的壳。
- callbackToWeb:主要是供Native执行完call的操作之后回调使用
- bindEvent(type, callback):
willAppear
、willDisappear等事件触发的events
。 - unbindEvent(type, callback):解绑
willAppear
、willDisappear
等事件触发的events
。 - onEvent(type, args):执行
willAppear
、willDisappear
等事件的events
。
call方法的实现
call
这个方法,它是提供 JS 调用 Native 功能的方法,MAP
中是预设好的方法,responseCallbacks
方法中存的是回调的方法
js
var MAP = {
closeWebview: "page_close",
triggerWebviewError: "page_triggerError",
closePullEffect: "ui_disableBounces",
setNavbarType: "ui_setNavbarType",
setWebviewDisplayType: "ui_setDisplayType",
enablePullToRefresh: "ui_enablePullToRefresh",
closeLoading: "ui_refreshComplete",
setShareOptions: "share_setOptions",
openSharePicker: "share_openSharePicker",
getDeviceInfo: "device_getDeviceInfo",
disableSwipeBack: "ui_disableSwipeBack",
dispatchEvent: "app_dispatchEvent",
saveImage: "image_saveToAlbum",
openAudioMemos: "audio_simpleRecord",
openAudioMemosInHeadless: "audio_recordService",
checkAudioMode: "audio_checkMode",
getAuthInfo: "audio_checkRecordGranted",
issueAuthPicker: "audio_acquireRecordAuth",
isSpecifiedAppInstalled: "app_isAppInstalled",
openSpecifiedApp: "app_openApp",
issueCollectRequest: "log_trackEvent"
};
var uniqueId = 1;
var responseCallbacks = {};
js
function generateCallbackId(message, callback, needShell) {
if (callback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
responseCallbacks[callbackId] = {
callback: callback,
needShell: needShell
};
message.callbackId = callbackId;
}
}
// 如果不带 needShell 参数,则返回的结果中不会带 code/result/msg 的壳
function call(action, params, callback, needShell) {
var message = {
action: action,
params: params,
};
if (MAP[action]) {
message.action = MAP[action];
}
generateCallbackId(message, callback, needShell);
var encodeMessageJson = encodeURIComponent(JSON.stringify(message));
prompt('bridge://yupaopao.com?message=' + encodeMessageJson, "");
}
callbackToWeb
callbackToWeb
是供native调用的,及native在执行完对应的 call
方法之后调用 callbackToWeb
进行执行回调
js
function callbackToWeb() {
var jsonData = '';
for (var i = 0; i < arguments.length; i++) {
jsonData += arguments[i];
}
jsonData = JSON.parse(jsonData);
if (jsonData.callbackId) {
var responseData = jsonData.responseData;
var savedData = responseCallbacks[jsonData.callbackId];
if (!savedData || !responseData) {
return;
}
var callback = savedData.callback;
var needShell = savedData.needShell;
if (!needShell && responseData.hasOwnProperty('code') && responseData.hasOwnProperty('result')) {
callback(responseData.result);
} else {
callback(responseData);
}
delete responseCallbacks[jsonData.callbackId];
}
}
bindEvent、unbindEvent、onEvent
event事件目前有 willAppear
、willDisappear
及页面出现、页面消失两个。event[type]
需要和客户端约定好,先使用 bindEvent
将具体的事件注册到 events
,等到Native触发了 onEvent
然后再执行具体 events[type]
。
js
var events = {};
function bindEvent(type, callback) {
if (callback) {
if (!events.hasOwnProperty(type)) events[type] = [];
events[type].push({ type: type, callback: callback });
}
}
function unbindEvent(type, callback) {
if (!events.hasOwnProperty(type)) return;
if (!callback) {
delete events[type];
} else {
events[type] = events[type].filter(function (it) {
return it.callback !== callback;
})
}
}
function onEvent (type, args) {
// 兼容 2.0 以前版本的 bridge
if (type === 'pullToRefresh' && window.onYppRefresh) {
window.onYppRefresh(args)
}
var targets = events[type] || [];
targets.forEach(function (it) {
it.callback && it.callback({ type: type, data: args });
})
},
最后就是将这些 _YPP_JS_BRIDGE_
挂载到 window
上
js
window._YPP_JS_BRIDGE_ = {
bindEvent: bindEvent,
unbindEvent: unbindEvent,
call: call,
/** native 调用此方法回调前端 */
callbackToWeb: callbackToWeb,
/** native 调用此方法来触发前端事件 */
onEvent: function (type, args) {
// 兼容 2.0 以前版本的 bridge
if (type === 'pullToRefresh' && window.onYppRefresh) {
window.onYppRefresh(args)
}
var targets = events[type] || [];
targets.forEach(function (it) {
it.callback && it.callback({ type: type, data: args });
})
},
};
call方法通信大致流程图如下:
event事件与call几乎一致,只不过少了js通过prompt和native进行通信的过程
文章参考:JS Bridge 通信原理与实践