面试官问 js bridge 通信原理?不懂
JS Bridge 原理?有没有安全漏洞?
双向通信交互
基本原理
WebViewJavaScriptBridge的基本原理简单来说就是,建立一个桥梁,然后注册自己,他人调用。
把 OC 的方法注册到桥梁中,让 JS 去调用
把 JS 的方法注册在桥梁中,让 OC 去调用
准备工作
JS初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback]; // 创建一个 WVJBCallbacks 全局属性数组,并将 callback 插入到数组中。
var WVJBIframe = document.createElement('iframe'); // 创建一个 iframe 元素
WVJBIframe.style.display = 'none'; // 不显示
WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__'; // 设置 iframe 的 src 属性
document.documentElement.appendChild(WVJBIframe); // 把 iframe 添加到当前文导航上。
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
// 这里主要是注册 OC 将要调用的 JS 方法。下面具体的交互操作会提到
setupWebViewJavascriptBridge(function(bridge){
});原生调用H5
原生调用H5有2个步骤,首先是js要注入一个方法 testA 到桥梁中,其次是原生调用桥梁中的方法 testA
js要注入一个方法 testA 到桥梁中
1
2
3
4
5
6
7
8
9
10// 往桥梁中注入js方法a
setupWebViewJavascriptBridge(function(bridge){
// 声明 OC 需要调用的 JS 方法。
bridge.registerHanlder('testA',function(data,responseCallback){
// data 是 OC 传递过来的数据.
// responseCallback 是调用完毕之后传递给 OC 的数据
alert("JS 被 OC 调用了.");
responseCallback({jsdata: "js 的数据",from : "JS"});
})
});原生调用桥梁中的方法 testA
1
2
3[_jsBridge callHandler:@"testA" data:@"传递给 JS 的参数" responseCallback:^(id responseData) {
NSLog(@"JS 的返回值: %@", responseData);
}];
H5调用原生
H5调用原生同样是两个步骤。注册自己,他人调用
- 往桥梁中注入Oc的testB方法
1
2
3
4
5[_jsBridge registerHandler:@"testB" handler:^(id data, WVJBResponseCallback responseCallback) {
// data 是 JS 传递过来的数据.
// responseCallback 是调用完毕之后传递给 js 的数据
responseCallback(@"传给js的值");
}]; - JS调用桥梁中的testB方法
1
2
3
4WebViewJavascriptBridge.callHandler('testB',{data : "传给 OC 的入参"},function(dataFromOC){
alert("JS 调用了 OC 的方法");
alert('调用结束后OC返回给JS的数据:', dataFromOC);
});调用方式
- ObjectC调js三种方式
1
2
3
4
5
6
7
8// 单纯的调用 JSFunction,不往 JS 传递参数,也不需要 JSFunction 的返回值。
[_jsBridge callHandler:@"a"];
// 调用 JSFunction,并向 JS 传递参数,但不需要 JSFunciton 的返回值。
[_jsBridge callHandler:@"a" data:@"传给js的入参"];
// 调用 JSFunction ,并向 JS 传递参数,也需要 JSFunction 的返回值。
[_jsBridge callHandler:@"a" data:@"传递给 JS 的参数" responseCallback:^(id responseData) {
NSLog(@"JS 的返回值: %@",responseData);
}]; - Js调用原生的三种方式
1
2
3
4
5
6
7
8
9
10
11// JS 单纯的调用 OC 的 block
WebViewJavascriptBridge.callHandler('b');
// JS 调用 OC 的 block,并传递 JS 参数
WebViewJavascriptBridge.callHandler('b',"JS 参数");
// JS 调用 OC 的 block,传递 JS 参数,并接受 OC 的返回值。
WebViewJavascriptBridge.callHandler('b',{data : "这是 JS 传递到 OC 的数据"},function(dataFromOC){
alert("JS 调用了 OC 的方法!");
document.getElementById("returnValue").value = dataFromOC;
});
Android端 跟 JS交互
Native端调用JS
native调用js比较简单,只要遵循:”javascript: 方法名(‘参数,需要转为字符串’)”的规则即可。
使用以下方式:
1 | mWebView.evaluateJavascript("javascript: 方法名('参数,需要转为字符串')", new ValueCallback() { |
特点:
- 不适合传输大量数据(大量数据建议用接口方式获取)
- mWebView.loadUrl(“javascript: 方法名(‘参数,需要转为字符串’)”);函数需在UI线程运行,因为mWebView为UI控件
Js调用Native端方法
要想js能够Native,需要对WebView设置以下属性。
1 | WebSettings webSettings = mWebView.getSettings(); |
这里我们看到了getJSBridge(),Native中通过addJavascriptInterface添加暴露出来的JS桥对象,然后再该对象内部声明对应的API方法。
1 | private Object getJSBridge(){ |
那Html端怎么调用Native端方法呢
1 | var JSBridge = window.JSBridge |
IOS端 跟 JS交互
Native端调JS
Native调用js的方法比较简单,Native通过stringByEvaluatingJavaScriptFromString调用H5端绑定在window上的函数。不过应注意Oc和Swift的写法。
1 | //Swift |
优缺点
- Native调用JS方法时,能拿到JS方法的返回值
- 不适合传输大量数据(大量数据建议用接口方式获取)
JS调用Native端
Native中通过引入官方提供的JavaScriptCore库(iOS7以上),然后可以将api绑定到JSContext上(然后Html中JS默认通过* window.top.*可调用)。
1 | // 引入官方的库文件 |
H5端 中JS调用Native方法
1 | window.top.H5foo('test') |
- 说明
- JS能调用到已经暴露的api,并且能得到相应返回值
- iOS原生本身是无法被JS调用的,但是通过引入官方提供的第三方”JavaScriptCore”,即可开放api给JS调用
原生和h5 的另一种通讯方式:最广为流行的方法 JSBridge-桥协议
JSBridge 是广为流行的Hybrid 开发中JS和Native一种通信方式,简单的说,JSBridge就是定义Native和JS的通信,Native只通过一个固定的桥对象调用JS,JS也只通过固定的桥对象调用native,
基本原理是:
h5 –> 通过某种方式触发一个url –> native捕获到url,进行分析 –>原生做处理 –> native 调用h5的JSBridge对象传递回调
上面我们看到native已经和js实现通信,为什么还要通过url scheme 的这种jsBridge方法呢
- Android4.2 一下,addJavaScriptInterface方式有安全漏洞
- ios7以下,js无法调用native
- url scheme交互方式是一套现有的成熟方案,可以兼容各种版本
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…),分析当前触发了哪种方法,然后根据定义来实现
js调native三种方式
在H5中JavaScript调用Native的方式主要有一下几种
- 注入API,注入Native对象或方法到JavaScript的window对象中(可以类比于RPC调用)。
- 拦截URL Scheme,客户端拦截WebView的请求并做相应的操作(可以类比于JSONP)
- JSBridge
注入API
通过WebView提供的接口,向JavaScript的window中注入对象或方法(Android使用addJavascriptInterface()方法),让JavaScript调用时相当于执行相应的Native端的逻辑,达到JavaScript调用Native的效果。
安卓核心代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24webView.addJavascriptInterface(new InjectNativeObject(this), "NativeBridge");
// 主要代码
public class MainActivity extends AppCompatActivity {
private WebView webView;
public class InjectNativeObject { // 注入到JavaScript的对象
private Context context;
public InjectNativeObject(Context context) {
this.context = context;
}
public void openNewPage(String msg) { // 打开新页面,接受前端传来的参数
}
// 存在兼容性问题
public void quit() { // 退出app
}
}
protected void onCreate(Bundle savedInstanceState) {
// JS注入
webView.addJavascriptInterface(new InjectNativeObject(this), "NativeBridge");
webView.loadUrl(String.format("http://%s:3000/login_webview", host)); // 加载Webview
}
}H5端
1
2
3
4
5
6
7
8
9window.NativeBridge = window.NativeBridge || {}; // 注入的对象
// 登录按钮点击,调用注入的openNewPage方法,并传入相应的值
loginButton.addEventListener("click", function (e) {
window.NativeBridge.openNewPage(accountInput.value + passwordInput.value);
}, false);
// 退出按钮点击,调用quit方法
quitButton.addEventListener("click", function (e) {
window.NativeBridge.quit();
}, false)
拦截URL Scheme
H5端通过iframe.src或localtion.href发送Url Schema请求,之后Native(Android端通过shouldOverrideUrlLoading()方法)拦截到请求的Url Scheme(包括参数等)进行相应的操作。
通俗点讲就是,H5发一个普通的http请求可能是: daydream.com/?a=1&b=1, 而与客户端约定的JSBridge Url Schema可能是: Daydream://jsBridgeTest/?data={a:1,b:2},客户端可以通过schema来区分是JSBridge调用还是普通的https请求从而做不同的处理。
其实现过程原理类似于JSONP
首先在H5中注入一个全局callback方法,放在window对象中
1 | function callback_1(data) { |
Native通过shouldOverrideUrlLoading(),拦截到WebView的请求,并通过与前端约定好的Url Schema判断是否是JSBridge调用。
Native解析出前端带上的callback,并使用下面方式调用callback
1 | webView.loadUrl(String.format("javascript:callback_1(%s)", isChecked)); // 可以带上相应的参数 |
缺陷:使用URL Schema有一定的长度问题,url过长可能会导致丢失; 一次JSBridge调用耗时可能比较长,创建请求需要一定的时间。
第三方框架使用getJsBridge
1 | const bridge = (window as any).getJsBridge() |
以上通信都是单向通信交互
native调js
Native 调用 JS 一般就是直接 JS 代码字符串,有些类似我们调用 JS 中的 eval 去执行一串代码比如eval("alert('cpp')")
。一般有 loadUrl/evaluateJavascript 等几种方法,这里逐一介绍。
但是不管哪种方式,客户端都只能拿到挂载到 window 对象上面的属性和方法。
其他相关问题
明明不是同一个语言,为什么 js 和 native 可以通信?
这就好像问,为什么JS能调用C++实现的原生方法。关键词,宿主环境。JS在Webview的宿主环境,而Webview在Android的宿主环境。所以通过Webview这个中间方就能通信。
怎么判断 webview 是否加载完成?
怎么实现 h5 页面秒开
webview预加载
捕获url参数
1 | function jso(params) { |