面试官问 js bridge 通信原理?不懂
JS Bridge 原理?有没有安全漏洞?

js跟native

双向通信交互

基本原理
WebViewJavaScriptBridge的基本原理简单来说就是,建立一个桥梁,然后注册自己,他人调用。

把 OC 的方法注册到桥梁中,让 JS 去调用
把 JS 的方法注册在桥梁中,让 OC 去调用

准备工作

  • JS初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function 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
    4
    WebViewJavascriptBridge.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
2
3
4
5
6
mWebView.evaluateJavascript("javascript: 方法名('参数,需要转为字符串')", new ValueCallback() {
@Override
public void onReceiveValue(String value) {
//这里的value即为对应JS方法的返回值
}
});

特点:

  • 不适合传输大量数据(大量数据建议用接口方式获取)
  • mWebView.loadUrl(“javascript: 方法名(‘参数,需要转为字符串’)”);函数需在UI线程运行,因为mWebView为UI控件

Js调用Native端方法

要想js能够Native,需要对WebView设置以下属性。

1
2
3
4
5
WebSettings webSettings = mWebView.getSettings();  
// Android容器允许JS脚本
webSettings.setJavaScriptEnabled(true);
// Android容器设置侨连对象
mWebView.addJavascriptInterface(getJSBridge(), "JSBridge");

这里我们看到了getJSBridge(),Native中通过addJavascriptInterface添加暴露出来的JS桥对象,然后再该对象内部声明对应的API方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private Object getJSBridge(){  
Object insertObj = new Object(){
@JavascriptInterface
public String H5foo(){
return "H5foo";
}

@JavascriptInterface
public String H5foo2(final String param){
return "H5foo2:" + param;
}
};
return insertObj;
}

那Html端怎么调用Native端方法呢

1
2
3
4
var JSBridge = window.JSBridge
JSBridge.call('H5foo', {}, (value: string) => {
// 原生回传给h5端信息
})

IOS端 跟 JS交互

Native端调JS

Native调用js的方法比较简单,Native通过stringByEvaluatingJavaScriptFromString调用H5端绑定在window上的函数。不过应注意Oc和Swift的写法。

1
2
3
4
//Swift
webview.stringByEvaluatingJavaScriptFromString("H5方法名(参数)")
//OC
[webView stringByEvaluatingJavaScriptFromString:@"H5方法名(参数);"];

优缺点

  • Native调用JS方法时,能拿到JS方法的返回值
  • 不适合传输大量数据(大量数据建议用接口方式获取)

JS调用Native端

Native中通过引入官方提供的JavaScriptCore库(iOS7以上),然后可以将api绑定到JSContext上(然后Html中JS默认通过* window.top.*可调用)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 引入官方的库文件
#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[@"H5foo"] = ^() {
//获取参数
NSArray *args = [JSContext currentArguments];
NSString *title = [NSString stringWithFormat:@"%@",[args objectAtIndex:0]];
//返回一个值 'foo:'+title
return [NSString stringWithFormat:@"H5foo:%@", title];
};
}

H5端 中JS调用Native方法

1
window.top.H5foo('test')
  • 说明
  • JS能调用到已经暴露的api,并且能得到相应返回值
  • iOS原生本身是无法被JS调用的,但是通过引入官方提供的第三方”JavaScriptCore”,即可开放api给JS调用

原生和h5 的另一种通讯方式:最广为流行的方法 JSBridge-桥协议

js跟native

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
    24
    webView.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;
    }
    @JavascriptInterface
    public void openNewPage(String msg) { // 打开新页面,接受前端传来的参数
    }

    @JavascriptInterface // 存在兼容性问题
    public void quit() { // 退出app
    }
    }
    @Override
    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
    9
    window.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
2
3
4
5
function callback_1(data) { 
console.log(data);
delete window.callback_1
};
window.callback_1 = callback_1;

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
2
3
4
const bridge = (window as any).getJsBridge()
bridge.call('原生上定义', (params: 'H5传给原生的参数'), function(res) {
// 拿到原生给的信息
})

以上通信都是单向通信交互

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
2
3
4
5
6
7
8
9
10
function jso(params) {
return Object.keys(params).reduce(
(acc, curKey, arr) => acc + `${ curKey }=${ params[curKey] }&`, ''
);
}
jso({
name: 'cpp',
age: 23
})
// name=cpp&age=23&

参考链接