JSBridge 实现原理

大桔子
本位主要总结下 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<String>() {
    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 <JavaScriptCore/JavaScriptCore.h>
    // 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方法呢

  1. Android4.2 一下,addJavaScriptInterface方式有安全漏洞
  2. ios7以下,js无法调用native
  3. 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通过重写webviewClientonJsPrompt 来获取url,然后解析
    • ios: 在 UIWebView WKWebview 内发起的所有网络请求,都可以通过 delegate函数在native层得到通知,通过 shouldStartLoadWithRequest捕获webview中触发的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},由于是异步不支持批量调用

参考资料

  1. https://www.cnblogs.com/dailc...
  2. https://zhuanlan.zhihu.com/p/...JSBridgeDemo
  3. https://github.com/chemdemo/c...
  4. 安卓与js交互
阅读 2.8k

大桔子
在2017年成为一名合格的前端攻城狮狮狮

下一步该干什么呢...

520 声望
39 粉丝
0 条评论
你知道吗?

下一步该干什么呢...

520 声望
39 粉丝
宣传栏