本位主要总结下 JSBridge 前端实现原理,来自工作中的总结,安卓/ios代码仅为示意
JavaScript 与 Native之间的互相调用
JavaScript是运行在一个单独的 JS Context中(例如: webview的webkit引擎,JSCore)
1. 注入api的形式
- 安卓操作方法
// 安卓4.4版本之前,无法获取返回值
// mWebView = new WebView(this); // 即当前webview对象
mWebView.loadUrl("javascript: 方法名('参数,需要转为字符串')")
// 安卓4.4及以后
// webView.evaluateJavascript("javascript:if(window.callJS){window.callJS('" + str + "');}", new ValueCallback() {
mWebView.evaluateJavascript("javascript: 方法名,参数需要转换为字符串", new ValueCallback() {
@Override
public void onReceiveValue(String value) {
// 这里的value即为对应JS方法的返回值
}
})
// js 在全局window上声明一个函数供安卓调用
window.callAndroid = function() {
console.log('来自中h5的方法,供native调用')
return "来自h5的返回值"
}
// 总结:
1. 4.4 之前Native通过loadUrl来调用js方法,只能让某个js方法执行,但是无法获取该方法的返回值
2. 4.4 之后,通过evaluateJavaScript异步调用js方法,并且能在onReceive中拿到返回值
3. 不适合传输大量数据
4. mWebView.loadUrl("javascript: 方法名") 函数需在UI线程运行,因为mWebView为UI控件,会阻塞UI线程
// JS调用Native
// 安卓环境配置
WebSettings webSettings = mWebView.getSettings();
// Android容器允许js脚本,必须要
webSettings.setJavaScriptEnabled(true);
// Android 容器设置侨连对象
mWebView.addJavascriptInterface(getJSBridge(), "JSBridge");
// Android中JSBridge的业务代码
private Object getJSBridge() {
Object insterObj = new Object() {
@JavascriptInterface
public String foo() {
// 此处执行 foo bridge的业务代码
return "foo" // 返回值
}
@JavascriptInterface
public String foo2(final String param) {
// 此处执行 foo2 方法 bridge的业务代码
return "foo2" + param;
}
}
return inserObj;
}
// html 中 js调用原生的代码
// JSBridge 通过addJavascriptInterface已被注入到 window 对象上了
window.JSBridge.foo(); // 返回 'foo'
window.JSBridge.foo2(); // 返回 'foo2:test'
注意:在安卓4.2之前 addJavascriptInterface有风险,hacker可以通过反编译获取Native注册的Js对象,然后在页面通过反射Java的内置 静态类,获取一些敏感的信息和破坏
- ios 操作方法
// native 调用 js
// UIWebview
[webView stringByEvaluatingJavaScriptFromString:@"方法名(参数);"];
// WKWebview
[_customWebView evaluateJavaScript:[@"方法名(参数)"] completionHandler:nil];
--------------------
// js 调用 native
// 引用官方库文件 UIWebview(ios8 以前的版本,建议弃用)
#import
// webview 加载完毕后设置一些js接口
-(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.foo('test'); // 返回 'foo:test'
// 注意:ios7 以前 js无法调用native方法,ios7之后可以引入第三方提供的 JavaScriptCore 库
总结:
1. ios7 才出现这种方式,在这之前js无法直接调用Native,只能通过JSBridge方式调用
2. JS 能调用到已经暴露的api,并且能得到相应返回值
3. ios原生本身是无法被js调用的,但是通过引入官方提供的第三方“JavaScriptCore”,即可开发api给JS调用
// WKWebview ios8之后才出现,js调用native方法
// ios 代码配置 https://zhuanlan.zhihu.com/p/32899522
// js代码
window.webkit.messageHandlers.JSBridge.postMessage(msgObj);
ios开发自带两种webview控件
UIWebview(ios8 以前的版本,建议弃用)
版本较老
可使用JavaScriptCore来注入全局自定义对象
占用内存大,加载速度慢
WKWebview
版本较新
加载速度快,占用内存小
js使用全局对象window.webkit.messageHandlers.{NAME}.postMessage 来调用native的方法
原生和h5 的另一种通讯方式:最广为流行的方法 JSBridge-桥协议
JSBridge 是广为流行的Hybrid 开发中JS和Native一种通信方式,简单的说,JSBridge就是定义Native和JS的通信,Native只通过一个固定的桥对象调用JS,JS也只通过固定的桥对象调用native,基本原理是:
h5 --> 通过某种方式触发一个url --> native捕获到url,进行分析 -->原生做处理 --> native 调用h5的JSBridge对象传递回调
为什么要用JSBridge
上面我们看到native已经和js实现通信,为什么还要通过url scheme 的这种jsBridge方法呢
- Android4.2 一下,addJavaScriptInterface方式有安全漏洞
- ios7以下,js无法调用native
- url scheme交互方式是一套现有的成熟方案,可以兼容各种版本
- 注意:jsBridge是一种交互理念一种协议,而上述url scheme则是其中的一种实现方式,所以也就是说,就算后面实现变为了 addJavaScriptInterface、JavaScriptCore,也是一样和JSBridge交互
url scheme 介绍
- url scheme是一种类似于url的链接,是为了方便app直接互相调用设计的:具体为:可以用系统的 OpenURI 打开类似与url的链接(可拼入参数),然后系统会进行判断,如果是系统的 url scheme,则打开系统应用,否则找看是否有app注册中scheme,打开对应app,需要注意的是,这种scheme必须原生app注册后才会生效,如微信的scheme为
weixin://
- 本文JSBridge中的url scheme则是仿照上述的形式的一种
具体位置app不会注册对应的scheme,而是由前端页面通过某种方式触发scheme(如用
iframe.src
),然后native用某种方法捕获对应的url触发事件,然后拿到当前触发url
,根据定好的协议(scheme://method...
),分析当前触发了哪种方法,然后根据定义来实现
实现一个JSBridge
1. 设计出一个native与js交互的`全局桥对象`
2. js如何调用native
3. native如何得知api被调用
4. 分析 url 参数和回调的格式
5. native如何调用js
6. h5中api方法的注册以及格式
- 设计一个native与js交互的全局对象 ==> 规定js和native之间的通信必须通过一个h5全局对象JSBridge来实现
// 名称: JSBridge 挂在 window上的一个属性
var JSBridge = window.JSBridge || (window.JSBridge = {});
/**
该对象有如下方法:
registerHandler(String, Function) 注册本地 js 方法,注册后 native可通过 JSBridge调用,注册后会将方法注册到本地变量 messageHandles中
sendHandler(String, JSON, Function) h5 调用原生开放的api,调用后实际上还是本地通过 url scheme触发,调用时会将回调 id 存放到本地变量responseCallbacks 中
_handleMessageFromNative h5 调用native之后的回调通知
参数为 {reposeId: 回调id, responseData: 回调数据}
*/
var JSBridge = {
// 注册本地方法供原生调用
registerHandler: function(method, cb) {
// 会将cb 放入 messageHandlers里面,待原生调用
},
messageHandles: {}, // h5注册方法集合,供native通知后回调调用
// h5 主动调用native,需生成唯一的callbackId
sendHandler: function(mathod, data, succCb, errCb) {
// 内部通过iframe src url scheme 向native发送请求
// 并将对应的回调注册进 responseCallbacks
// native 处理结束后将结果信息通知到h5 通过 _handleMessageFromNative
// h5 拿到返回信息处理 responseCallbacks 里对应的回调
},
responseCallbacks: {}, // 回调集合
// native 通知 h5
_handleMessageFromNative: function(message) {
// 解析 message,然后根据通知类型执行 messageHandles 或 responseCallbacks里的回调
}
}
/**
注意:
1. native 调用_handleMessageFromNative通知h5,参数为 json 字符串
2. native 主动调用h5方法时 {methodName: api名, data, callbackId}
methodName: 开放api的名称
data: 原生处理后传递给 h5 参数
需要把回调函数的值 return 出去,供native拿到,
或者再发一个 bridge 回去,方法名是 methodNameSuccess,或者严禁掉,方法名为native生产的callbackId
*/
如:
bridge.register("hupu.ui.datatabupdate", (name) => {
if(name) {
// 再发一个bridge通知原生tab更新成功,,,method 可以为native生成的 callbackId
bridge.send('hupu.ui.datatabsuccess', {})
}
});
- js 如何调用native ==> 通过 sendHandler 方法调用原生
// sendHandler 执行步骤
1. 判断是否有回调函数,如果有,生成一个回调函数id,并将id,和对应的回调添加放入回调函数集合 responseCallbacks 中
2. 通过特定的参数转换方法,将传入的数据,方法名一起拼接成一个 url scheme,如下:
var param = {
method: 'methodName',
data: {xx: 'xx'},
success: 'successId',
error: 'errorId'
}
// 变成字符串并编码
var url = scheme://ecape(JSON.stringify(param))
3. 使用内部创建好的iframe来触发scheme(location.href = 可能会造成跳转问题)
...创建iframe
var iframe = document.createElment('iframe');
iframe.src = url;
document.head.appendChild(iframe);
setTimeout(() => document.head.removeChild('iframe'), 200)
-
native 如何得知 api 被调用
- 安卓捕获 url scheme:
shouldoverrideurlloading
捕获到url进行分析 - 安卓端也可通过h5的
window.prompt(url, '')
来触发scheme,然后native通过重写webviewClient
的onJsPrompt
来获取url,然后解析 - ios: 在
UIWebView WKWebview
内发起的所有网络请求,都可以通过 delegate函数在native层得到通知,通过shouldStartLoadWithRequest
捕获webview中触发的url scheme
- 安卓捕获 url scheme:
-
分析url参数和回调的格式
- native已经接收到了js调用的方法,接下来原生应该按照定义好的数据格式来解析数据了,从url中提取出 method、data、successId、errorId
- 根据方法名,再本地寻找对应的方法,接收对应的参数进行执行,执行完毕后,然后通知 h5并携带相应参数
-
native如何调用 js (参照上面的native执行js的方法)
- h5调用native后的被动通知
JSBridge._handleMessageFromNative(messageJSON)
,json格式:{responseId, reponseData}
- native 主动调用h5 注册的方法,
JSBridge._handleMessageFromNative(param)
,param 格式为{methodName, data}
,由于是异步不支持批量调用
- h5调用native后的被动通知