JSBridge
开发维护成本 和 更新成本 较低的 Web 技术成为混合开发中几乎不二的选择,而作为 Web 技术逻辑核心的 JavaScript 也理所应当肩负起与其他技术『桥接』的职责,并且作为移动不可缺少的一部分,任何一个移动操作系统中都包含可运行 JavaScript 的容器,例如 WebView 和 JSCore。所以,运行 JavaScript 不用像运行其他语言时,要额外添加运行环境。因此,基于上面种种原因,JSBridge 应运而生。
移动端混合开发中的 JSBridge,主要被应用在两种形式的技术方案上:
-
基于 Web 的 Hybrid 解决方案:例如微信浏览器、各公司的 Hybrid 方案
-
非基于 Web UI 但业务逻辑基于 JavaScript 的解决方案:例如 React-Native
【注】:微信小程序基于 Web UI,但是为了追求运行效率,对 UI 展现逻辑和业务逻辑的 JavaScript 进行了隔离。因此小程序的技术方案介于上面描述的两种方式之间。
API注入
android
native调js
webview执行js脚本,类似eval(),传入js字符串并执行;
mWebView.evaluateJavascript("javascript: 方法名('参数,需要转为字符串')", new ValueCallback() {
@Override
public void onReceiveValue(String value) {
//这里的value即为对应JS方法的返回值
}
});
js调native
允许JS脚本,然后webview挂载一个桥接对象到JS Context(window)下
//Js调用Native需要对WebView设置@JavascriptInterface注解。要想js能够调Native,需要对WebView设置以下属性。
WebSettings webSettings = mWebView.getSettings();
//Android容器允许JS脚本
webSettings.setJavaScriptEnabled(true);
//Android容器设置侨连对象(我的理解相当于在window下挂个命名空间,名字随便起,不对之处请大佬指出)
mWebView.addJavascriptInterface(getJSBridge(), "JSBridge");
//添加命名空间的方法
private Object getJSBridge(){
Object insertObj = new Object(){
@JavascriptInterface
public String foo(){
return "foo";
}
@JavascriptInterface
public String foo2(final String param){
return "foo2:" + param;
}
};
return insertObj;
}
//js中使用
//调用方法一
window.JSBridge.foo(); //返回:'foo'
//调用方法二
window.JSBridge.foo2('test');//返回:'foo2:test'
ios
native调js
与安卓类似,webview执行js脚本,类似eval();
//Swift
webview.stringByEvaluatingJavaScriptFromString("方法名(参数)")
//OC
[webView stringByEvaluatingJavaScriptFromString:@"方法名(参数);"];
js调native
设置JS接口,注册桥接对象的api方法
//在ios中引入官方的库文件
#import <JavaScriptCore/JavaScriptCore.h>
//Native注册api函数(OC)
-(void)webViewDidFinishLoad:(UIWebView *)webView{
[self hideProgress];
[self setJSInterface];
}
-(void)setJSInterface{
JSContext *context =[_wv valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
// 注册名为foo的api方法
context[@"foo"] = ^() {
//获取参数
NSArray *args = [JSContext currentArguments];
NSString *title = [NSString stringWithFormat:@"%@",[args objectAtIndex:0]];
//做一些自己的逻辑
//返回一个值 'foo:'+title
return [NSString stringWithFormat:@"foo:%@", title];
};
}
//在js中直接调用
window.top.foo('test');
总结
JavaScript调用Native,注入 API 方式的主要原理是,通过 WebView 提供的接口,向 JavaScript 的 Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到 JavaScript 调用 Native 的目的。
Native 调用 JavaScript,其实就是执行拼接 JavaScript 字符串,从外部调用 JavaScript 中的方法,因此 JavaScript 的方法必须在全局的 window 上。(闭包里的方法,JavaScript 自己都调用不了,更不用想让 Native 去调用了)
通信原理是 JSBridge 实现的核心,实现方式可以各种各样,但是万变不离其宗。这里,推荐的实现方式如下:
- JavaScript 调用 Native 推荐使用 注入 API 的方式(iOS6 忽略,Android 4.2以下使用 WebViewClient 的 onJsPrompt 方式)。
- Native 调用 JavaScript 则直接执行拼接好的 JavaScript 代码即可。
URL SCHEME
单纯的js调用Native还有一种拦截URL SCHEME方案
url scheme是一种类似于url的链接,是为了方便app直接互相调用设计的。具体来讲如果是系统的url scheme,则打开系统应用,否则找看是否有app注册这种scheme,打开对应app,主要区别是 protocol 和 host 一般是自定义的
在UIWebView的delegate函数中,我们只要发现是对应的url scheme开头的地址,就不进行内容的加载,转而执行相应的调用逻辑。
发起这样一个网络请求有两种方式:1. 通过iframe的src方式;(2. 通过localtion.href;)
缺陷:
location.href
1)就是如果我们连续多次修改window.location.href的值,在Native层只能接收到最后一次请求,前面的请求都会被忽略掉。
iframe的src:
1)发送 URL SCHEME 会有 url 长度的隐患。
2)创建请求,需要一定的耗时,比注入 API 的方式调用同样的功能,耗时会较长。
【注】:有些方案为了规避 url 长度隐患的缺陷,在 iOS 上采用了使用 Ajax 发送同域请求的方式,并将参数放到 head 或 body 里。这样,虽然规避了 url 长度的隐患,但是 WKWebView 并不支持这样的方式。
【注2】:为什么选择 iframe.src 不选择 locaiton.href ?因为如果通过 location.href 连续调用 Native,很容易丢失一些调用。
使用iframe方式,以唤起Native APP的分享组件为例,简单的封闭如下:
var url = 'jsbridge://doAction?title=分享标题&desc=分享描述&link=http%3A%2F%2Fwww.baidu.com';
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() {
iframe.remove();
}, 100);
其他平台
对于其他方式,诸如 React Native、微信小程序 的通信方式都与上描述的近似,并根据实际情况进行优化。
以 React Native 的 iOS 端举例:JavaScript 运行在 JSCore 中,实际上可以与上面的方式一样,利用注入 API 来实现 JavaScript 调用 Native 功能。不过 React Native 并没有设计成 JavaScript 直接调用 Object-C,而是 为了与 Native 开发里事件响应机制一致,设计成 需要在 Object-C 去调 JavaScript 时才通过返回值触发调用。原理基本一样,只是实现方式不同。
JSBridge 接口实现
window.JSBridge = {
// 调用 Native
invoke: function(bridgeName, data) {
// 判断环境,获取不同的 nativeBridge
nativeBridge.postMessage({
bridgeName: bridgeName,
data: data || {}
});
},
receiveMessage: function(msg) {
var bridgeName = msg.bridgeName,
data = msg.data || {};
// 具体逻辑
}
};
消息都是单向的,那么调用 Native 功能时 Callback 怎么实现的?
对于 JSBridge 的 Callback ,其实就是 RPC 框架的回调机制。当然也可以用更简单的 JSONP 机制解释:
当发送 JSONP 请求时,url 参数里会有 callback 参数,其值是 当前页面唯一 的,而同时以此参数值为 key 将回调函数存到 window 上,随后,服务器返回 script 中,也会以此参数值作为句柄,调用相应的回调函数。
(function () {
var id = 0,
callbacks = {},
registerFuncs = {};
window.JSBridge = {
// 调用 Native
invoke: function(bridgeName, callback, data) {
// 判断环境,获取不同的 nativeBridge
var thisId = id ++; // 获取唯一 id
callbacks[thisId] = callback; // 存储 Callback
nativeBridge.postMessage({
bridgeName: bridgeName,
data: data || {},
callbackId: thisId // 传到 Native 端
});
},
receiveMessage: function(msg) {
var bridgeName = msg.bridgeName,
data = msg.data || {},
callbackId = msg.callbackId, // Native 将 callbackId 原封不动传回
responstId = msg.responstId;
// 具体逻辑
// bridgeName 和 callbackId 不会同时存在
if (callbackId) {
if (callbacks[callbackId]) { // 找到相应句柄
callbacks[callbackId](msg.data); // 执行调用
}
} else if (bridgeName) {
if (registerFuncs[bridgeName]) { // 通过 bridgeName 找到句柄
var ret = {},
flag = false;
registerFuncs[bridgeName].forEach(function(callback) => {
callback(data, function(r) {
flag = true;
ret = Object.assign(ret, r);
});
});
if (flag) {
nativeBridge.postMessage({ // 回调 Native
responstId: responstId,
ret: ret
});
}
}
}
},
register: function(bridgeName, callback) {
if (!registerFuncs[bridgeName]) {
registerFuncs[bridgeName] = [];
}
registerFuncs[bridgeName].push(callback); // 存储回调
}
};
})();