前端H5与客户端Native交互原理 - JSBridge双向通信机制的实现

·  阅读 3207

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第7天,点击查看活动详情

概述

原生WebView内嵌H5,实现业务复杂交互,是业界在混合应用实践中,总结出的一套成熟的,可供快速业务迭代的技术方案。
在实现这种类型的混合应用时,最重要的事,就是解决H5与Native之间的双向通信。
本文聚焦双向通信的实现方案——JSBridge,讲述整套通信机制是如何运行的。

何为JSBridge?

JSBridge是横跨原生运行环境和JavaScript运行环境的一道桥梁。这个桥梁,是双端进行通信的媒介。

用伪代码描述如下:

// 前端
前端调用(方法名,参数,回调) {
    监听列表[回调id] = 回调; // 存储回调
    window[客户端注入的方法名].发送信息(方法名, JSON.stringify(参数), id)
}
  
document监听(监听回调, (回调id, 数据) => {
  回调方法 = 监听列表[回调id];
  回调方法(数据);
})
  
// 客户端
JS全局上下文对象 = 获取JS全局上下文对象window;

JS全局上下文对象["客户端注入的方法名"] = 监听信息(msg) {
    返回数据 = 根据信息,调用原生方法,处理相关逻辑,生成数据;
    
    向web端发送消息({
      监听回调,
      回调id, 
      返回数据
    );
};
复制代码

伪代码展示注入方式下,原生运行环境和JavaScript运行环境通过window为媒介,进行互相调用。
这种用JS实现互相调用的Bridge,就叫JSBridge

承载JSBridge和H5的容器

在原生客户端开发中,有一个控件:WebView
它为JS运行提供了一个沙箱环境,并提供渲染引擎用于页面渲染。
同时,客户端依赖WebView提供的各种接口,实现对页面请求的拦截和控制。

以下是客户端不同版本的WebView内核:

平台与版本WebView内核
iOS8+WKWebView
iOS 2-8UIWebView
Android 4.4+Chrome
Android 4.4-WebKit

Native向Web发送消息

Native向WebView发送消息的原理:在WebView中动态执行一段JS脚本
通常情况下,都是调用挂载在全局上下文(window)下的方法。

以下是Android与iOS执行JS的方法:

平台与版本API特点
iOS8+WKWebView.evaluateJavaScript可以拿到 JS 执行完毕的返回值
iOS 2-8UIWebView.stringByEvaluatingJavaScriptFromString无法执行回调
Android 4.4+WebView.evaluateJavascript可以拿到 JS 执行完毕的返回值
Android 4.4-WebView.loadUrl无法执行回调

iOS向Web发送消息的实现

struct _LeonJSExexuter {
    
  enum Function: String {
    
    /// 客户端主动调用前端
    case triggerDispatchEvent = "(function() { var event = new CustomEvent('leonJSBridgeListener', {'detail': %@}); document.dispatchEvent(event)}());"
    //                                                                      ^^^^^^^^^^^^^^^^^^^^
  }
}

private func callJSListerer(_ methodName: String, _ params: [String: Any]?) {

  let dict: [String : Any] = ["name": methodName, "__params": params ?? [:]]
  
  let js = String(format: _LeonJSExexuter.Function.triggerDispatchEvent.rawValue, dict.toJsonString() ?? "{}")
  
  webView?.evaluateJavaScript(js, completionHandler: nil)
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^
}
复制代码

Android向Web发送消息的实现

fun callJSMethod(methodName: String, param: String?, callback: ValueCallback<String>?) {
    handler?.post {
        webView?.evaluateJavascript(
    //  ^^^^^^^^^^^^^^^^^^^^^^^^^^^
            "(function() { var event = new CustomEvent('$methodName', {'detail': ($param)}); 

            
            document.dispatchEvent(event)}());",
            callback
        )
    }
}

class LeonProcessor(private val webView: WebView) {

  private fun leonJSBridgeListener(params: String) {
      webView.callJSMethod("leonJSBridgeListener", params, null)
  // ^^^^^^^^^^^^^^^^^^^^^  ^^^^^^^^^^^^^^^^^^^^
  }
}
复制代码

前端接收消息的实现

/**
 * 前端注册 客户端发送消息事件的处理逻辑
 * @param name 消息名
 * @param 处理消息的回调
 * **/
public on(name: TNativeEvent, callback: TCallback) {
  let namedListeners = this.registerHandlers[name];
  if (!namedListeners) {
    namedListeners = [];
    this.registerHandlers[name] = namedListeners;
  }
  namedListeners.push(callback);
  return function () {
    delete namedListeners[namedListeners.indexOf(callback)];
  };
}

// 监听客户端调用前端时,发送的customEvent
document.addEventListener('leonJSBridgeListener', (e: any) => {
  //                       ^^^^^^^^^^^^^^^^^^^^
    const { name, __params } = e.detail;
    if (
      name !== undefined &&
      typeof name === 'string' &&
      this.registerHandlers[name] &&
      typeof this.registerHandlers[name] === 'object'
    ) {
      const namedListeners = this.registerHandlers[name];
      if (namedListeners instanceof Array) {
        const ret = __params;
        namedListeners.forEach(handler => {
          if (handler && typeof handler === 'function') {
            handler(ret);
          }
        });
      }
    }
  }, false);
复制代码

前端监听客户端回调的使用方式

JSBridge.on('on客户端call', (data) => {
  console.log('回调数据', data);
})
复制代码

小结

从实现代码可以看出:

  • iOS使用WKWebView.evaluateJavaScript,在webview容器中执行js
  • Android使用WebView.evaluateJavascript, 在webview容器中执行js
  • iOS和Android,都使用CustomEvent的方式,向window dispathEvent,事件名统一为leonJSBridgeListener
  • 前端通过监听document上的事件leonJSBridgeListener,接收到客户端传递过来的消息体,并进行处理

Web向Native发送消息

Web向Native发送消息,实现上的本质:JS的执行,可以被Native感知到的

业界的实现方案有两种:

  • 拦截式

    通过设置特殊的scheme头的链接,让客户端在拦截URL的时候,可以判断是否需要特殊处理。
    格式一般为:<scheme>://<path>
    例如:
    微信支持通过URL Scheme打开小程序:location.href = 'weixin://dl/business/?t= *TICKET*'
    特定的scheme头为:weixin

  • 注入式

    通过WebView提供的接口,向全局上下文对象(window)注入对象或方法handler。
    当该handler被JS执行时,Native端可以感知到。
    Native端就可以执行对应逻辑,从而达到Web调用Native的效果。

下面,主要讲解目前业界成熟的方案:注入式的实现

Native注入API

平台API特点
AndroidaddJavascriptInterface4.2 版本以下有安全风险
iOS 8+WKScriptMessageHandler
iOS 7+JavaSciptCore

前端向客户端发送消息的实现

// 监听客户端发送的回调事件,根据回调id(__callback_id),执行对应的方法
document.addEventListener('leonJSBridgeCallback', (e: any) => {
                           ^^^^^^^^^^^^^^^^^^^^^
  const { __callback_id, __params } = e.detail;
          ^^^^^^^^^^^^^^
  if (
    __callback_id !== undefined &&
    __callback_id !== '' &&
    this.callbacks[__callback_id] &&
    typeof this.callbacks[__callback_id] === 'function'
  ) {
    const ret = this.callbacks[__callback_id](__params);
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    delete this.callbacks[__callback_id];
    return ret;
  }
}, false);
复制代码
// 前端调用客户端注入的对象,调用postMessage方法发送信息
public call(name: TNativeMethod, params: unknown, callback?: TCallback) {
  const bridgeName = 'leonJSBridge';
  const id = (this.callbackID++).toString();
  this.callbacks[id] = callback;

  if (isAndroid) {
    if (window[bridgeName]) {
      try {
        try {
          window[bridgeName].postMessage(name, JSON.stringify(params), id);
        } catch (err) {
          window[bridgeName].postMessage(name, params, id);
        }
      } catch (error) {}
    }
    return;
  }

  if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers[bridgeName]) {
    try {
      window.webkit.messageHandlers[bridgeName].postMessage({
        method: name,
        params,
        id,
      });
    } catch (error) {}
  }
}
复制代码

iOS接收Web消息的实现

import UIKit
import WebKit


class WeakScriptMessageDelegate: NSObject, WKScriptMessageHandler {
  weak var scriptDelegate: WKScriptMessageHandler?
  
  init(_ scriptDelegate: WKScriptMessageHandler) {
      self.scriptDelegate = scriptDelegate
      super.init()
  }
  
  func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
      scriptDelegate?.userContentController(userContentController, didReceive: message)
  }
}
复制代码
func injectJSBridge(_ scriptMessageHandler: WKScriptMessageHandler, methodName: String) {
        webView.configuration.userContentController.add(WeakScriptMessageDelegate(scriptMessageHandler), name: methodName)
}
复制代码
class LeonWebCallProcessor: NSObject {
    weak var webView: LeonWebView?
    init(webView: LeonWebView?) {
        super.init()
        webView?.injectJSBridge(self, methodName: 'leonJSBridge')
        //                                         ^^^^^^^^^^^^
    }   
}
复制代码

上述代码,iOS使用WKUserContentController,对WebView注入自定义对象leonJSBridge,监听JS端调用的消息。

然后,前端发送消息的方式如下:

window.webkit.messageHandlers.leonJSBridge.postMessage({
                              ^^^^^^^^^^^^
  method: name,
  params,
  id,
});
复制代码

iOS端接收到消息到,进入处理逻辑:

extension LeonWebCallProcessor: WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        let params = message.body as? [String: Any]
                     ^^^^^^^^^^^^
        if message.name == "leonJSBridge", let method = params?["method"] as? String {
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^               ^^^^^^^^^^^^^^^^^
            
            if method == "getUserInfo" {
                
                if let callbackId = params?["id"] as? String {
                    callJSCallBack(callbackId, userInfo)
                  // ^^^^^^^^^^^^^
                }
                
            }
        } 
    }
}
复制代码

iOS在message.body中,拿到前端发送的消息体,解析出方法名method后,就可以知道需要执行哪个方法了。 执行完成后,使用callJSCallBack,将处理结果返回给Web端。

struct _LeonJSExexuter {
    
    enum Function: String {
        /// 客户端回调前端
        case callbackDispatchEvent = "(function() { var event = new CustomEvent('leonJSBridgeCallback', {'detail': %@}); document.dispatchEvent(event)}());"
                                                                                 ^^^^^^^^^^^^^^^^^^^^^
    }
}
复制代码
 private func callJSCallBack(_ callbackId: String, _ params: String) {
    let dict: [String : Any] = ["__callback_id": callbackId, "__params": params.toDictionary() ?? [:]]
    let js = String(format: _LeonJSExexuter.Function.callbackDispatchEvent.rawValue, dict.toJsonString() ?? "{}")
                                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    
    webView?.evaluateJavaScript(js, completionHandler: nil)
}
复制代码

通过发送leonJSBridgeCallback事件,将透传前端传过来的callbackID和其他结果一起返回,
则前端可以知道要执行哪个回调方法(callbackID),并将数据结果附上。

Android接收Web消息的实现

webView?.addJavascriptInterface(
    LeonJSBridge(notificationDelegate, this@webview),
    "leonJSBridge"
)
复制代码
class LeonJSBridge(
  private val notificationDelegate: LeonNotificationProtocol?,
  private val webview: WebView
) {

  @JavascriptInterface
  fun postMessage(method: String?, params: String?, id: String?) {
      when (method) {
          "getUserInfo" -> {
              getUserInfo(id)
          }
      }
  }
  
  private fun getUserInfo(id: String?) {
      val userInfoJson = JSONObject()

      userInfoJson.put("__callback_id", id)
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

      val paramJson = JSONObject()
      paramJson.put("userId", userId)
      paramJson.put("nickName", nickName)

      userInfoJson.put("__params", paramJson)

      leonJSBridgeCallback(userInfoJson.toString())
      ^^^^^^^^^^^^^^^^^^^^
  }
  
  private fun leonJSBridgeCallback(params: String) {
      webview.callJSMethod("leonJSBridgeCallback", params, null)
                            ^^^^^^^^^^^^^^^^^^^^^
  }
}
复制代码
fun callJSMethod(methodName: String, param: String?, callback: ValueCallback<String>?) {
    handler?.post {
        webView?.evaluateJavascript(
    //  ^^^^^^^^^^^^^^^^^^^^^^^^^^^
            "(function() { var event = new CustomEvent('$methodName', {'detail': ($param)}); 

            
            document.dispatchEvent(event)}());",
            callback
        )
    }
}
复制代码

Android端使用addJavascriptInterface,对WebView注入自定义对象leonJSBridge,监听JS端调用的消息。
然后根据postMessagemethod,决定调用哪个方法执行处理逻辑。
最后,调用evaluateJavascript方法,返回CustomEvent给前端。

前端调用Android的方式:

window.leonJSBridge.postMessage('getUserInfo', JSON.stringify(params), id);
复制代码

小结

从实现代码可以看出:

  • callback回调,和Native向Web发送消息的实现一样
    • iOS使用WKWebView.evaluateJavaScript,在webview容器中执行js
    • Android使用WebView.evaluateJavascript, 在webview容器中执行js
    • iOS和Android,都使用CustomEvent的方式,向window dispathEvent,事件名统一为leonJSBridgeCallback
  • 前端通过监听document上的事件leonJSBridgeCallback,接收到客户端传递过来的消息体,并进行处理
  • 重要的一点:callbackID的透传,这样前端在监听leonJSBridgeCallback事件时,可以知道要使用哪个回调来处理数据

参考


坚持原创,输出有价值的文章!

同学,如果文章有帮助到你,请通过以下方式给笔者反馈:

最近笔者在整理第一本电子书书稿《前端面试手册》,有兴趣的同学可以点个✨star✨关注下

分类:
前端
收藏成功!
已添加到「」, 点击更改