JSBridge 设计和实现

2 minute read

Hybrid 中的桥梁

Hybrid 应用混合使用了原生和 Web 技术,原生应用使用移动平台的 WebView 控件,加载 Web 资源,并通过向其开放一些接口(如加速器、摄像头等),增强了 Web 应用的能力。如此 Hybrid 应用既有原生应用的能力,又有了 Web 的热更新、端代码统一、开发迭代快的优势。Hybrid 中实现不同层沟通交互的桥梁即是 JSBridge,它连通了原生层语言( Java、Swift等)与网页层 JavaScript 语言,使得彼此可以相互调用。

JSBridge 的实现技术

JS 调用原生层

Android 中 JS 与 Native 的通信,主要有两种:

  • 注入对象 通过注入对象到 window 对象上,连接原生应用层与 JS 层,实现相互交互, Android 使用 JavaScriptInterface addJavaScriptInterface(Object object, String name) 方法负责把 object 对象暴露成 JavaScript 中的 name 对象

  • 拦截 scheme 覆盖 WebChromeClient 中 onJsAlerton、JsConfirm、onJsPrompt 方法来对 JS 的 alert、confirm、prompt 进行捕获,或者覆盖 WebviewClient 中的 shouldOverrideUrlLoading 方法来拦截url 跳转

WebviewClient 和 WebChromeClient

两者通过 Webview 的 setWebViewClient 方法作为参数传入,拦截 Webview 的相关行为,当 Webview 发生相应的行为后,即可在 WebviewClient 和 WebChromeClient 对象中对应的回调方法中获得,覆盖两者中的方法,即可监听特定事件做相应的操作。

  • WebviewClient 影响网页 view 的行为,如页面开始加载和结束的onPageStarted、onPageFinished,url 跳转的 shouldOverrideUrlLoading。
  • WebChromeClient 浏览器的行为,如 onJsAlert、onJsConfirm、onJsPrompt和信息打印onConsoleMessage

iOS UIWebView 有个特性:在 UIWebView 内发起的所有网络请求,都可以通过 delegate 函数在 Native 层得到通知。这样,我们可以在 webview 中捕获 url scheme 的触发(原理是利 shouldStartLoadWithRequest)

WKWebView 通过 WKUserContentController 的 addScriptMessageHandler 方法注入一个JS对象名,当 JS 调用 window.webkit.messageHandlers.senderModel.postMessage({body: ‘sender message’}) 时可在didReceiveScriptMessage 方法中收到相应的 message 对象

原生层调用 JS

  • Android 通过 WebView 的 loadUrl 或 evaluateJavascript方法

  • iOS 通过UIWebView 的 stringByEvaluatingJavaScriptFromString 或 WKWebview 的 evaluateJavaScript 方法

JSBridge 的设计

异步通信

通过 JSBridge,JS 可使用原生层的资源、网络访问能力、接口处理能力等,这些大部分情况下都是异步的(尽管部分情况 JSBridge 原生层与 JS 的通信技术可同步取得结果),同时也为做到 Android 与 iOS 两端统一,接口最小化,双方的所有的通信都设计为异步的。

双方对等

JSBridge的通信是全双工通信,原生层与JS层都即可作为请求调用方,也可作为响应回复方。JS 可调用原生层的接口函数,同时原生层也可调用 JS 的接口函数,只要一方注册过处理函数,对方调用时即可找到对应的处理函数。

处理函数注册与调用

被调用方通过 registerHandler 将接口开发出来,注册 handler 后调用名与对应的处理函数,形成映射。

var messageHandlers = {};
function registerHandler(handlerName, handler) {
  messageHandlers[handlerName] = handle
}

调用 callHandler 采用传入函数名、参数、回调函数的方式异步使用。当响应方数据返回时,为能够正确的将响应数据传给对应的回调函数,需为回调函数生成对应的请求 id,放入映射表内。这样响应方将响应数据与请求 id 回传后,通过请求 id,可以将响应数据正确的传递给回调函数。

var uniqueId = 1;
var responseCallbacks = {};
function callHandler(handlerName, data, responseCallback) {
  if (arguments.length == 2 && typeof data == 'function') {
    responseCallback = data;
    data = null;
  }

  var message = {handlerName: handlerName, data: data};

  // 如果有回调,生成回调 id
  if (responseCallback) {
    var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
    responseCallbacks[callbackId] = responseCallback;
    message.callbackId = callbackId;
  }

  // _doSend 与原生层约定实现,schema 拦截或对象注入等
  _doSend(message);
}

通信链路的实现

  • 传输数据 schema 拦截可实现两端统一的代码,对象注入需根据 navigator.userAgent 做平台区分
    function _doSend(message) {
    var messageString = JSON.stringify(message);
    iframe.src = 'JSBridge://send' + encodeURIComponent(messageString);
    }
    
  • 接收数据 _handleMessageFromNative 接收原生层传来的数据,主要任务为:
    1. 接收原生层的请求响应数据;
    2. 响应原生层的函数调用请求

也就是说此处,此通道需要做请求数据与响应数据的区分,如果是原生层的响应数据,将响应数据根据响应 id 分发给映射表中的回调函数;如果是请求数据,则根据请求函数名,调用对应注册的handler 做处理。为保证线程安全,JS 层的实现将其用setTimeout 包裹,变为一个 macro task。

function _dispatchMessageFromNative(messageJSON) {
  setTimeout(function() {
    var message = JSON.parse(messageJSON);
    var responseCallback;

    // 与原生层约定,请求时传入的 id 值作为响应 id 返回,表示此数据为响应数据
    if (message.responseId) {
      responseCallback = responseCallbacks[message.responseId];
      if (!responseCallback) {
          return;
      }
      responseCallback(message.responseData);
      delete responseCallbacks[message.responseId];
    } else {
      // 原生层的请求数据
      if (message.callbackId) {
        var callbackResponseId = message.callbackId;
        responseCallback = function(responseData) {
          // 将请求 id 键名变为响应 id 键名,id 值不变,表示一次交互完成响应
          _doSend({
            responseId: callbackResponseId,
            responseData: responseData
          });
        };
      }

      var handler = _messageHandler; // 默认 handler
      if (message.handlerName) {
        handler = messageHandlers[message.handlerName];
      }
      //查找指定handler
      try {
        handler(message.data, responseCallback);
      } catch (exception) {
        if (typeof console != 'undefined') {
          console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
        }
      }
    }
  });
}

查询与订阅

JS 需要获取原生层的信息时,大部分通过发起一次查询请求的方式获取,但有些情形 JS 需要对某些维护在原生层的信息进行订阅,如长连接的推送消息、事件监听等。此时通过查询的方式不再适用,需建立对原生层信息的订阅关系,在 registerHandler 与 callHandler 的基础上封装一步实现,即 JS 先 registerHandler 一个订阅信息处理函数,开发给原生层,并通过 callHandler 告知原生层,这样当原生层的信息更新后,直接调用该订阅处理函数,完成信息的推送。

function subscribe(onMessageName, handler) {
  if(typeof onMessageName !== 'string' && typeof handler !== 'function') {
    throw new Error('invalid subscribe parameters');
  }

  JSBridge.registerHandler(onMessageName, handler);
  JSBridge.callHandler('subscribe', {onMessageName: onMessageName}, function(data) {
    if(data.status !== 'OK') {
      throw new Error(data.error);
    }
  });
  
  return function() {
    JSBridge.callHandler('unSubscribe', {onMessageName: onMessageName});
    delete messageHandlers[onMessageName];
  }
}

end


三方类库

Android https://github.com/lzyzsd/JsBridge

iOS https://github.com/marcuswestin/WebViewJavascriptBridge