概述

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

何为JSBridge?

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

用伪代码描述如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 前端
前端调用(方法名,参数,回调) {
监听列表[回调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-8 UIWebView
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-8 UIWebView.stringByEvaluatingJavaScriptFromString 无法执行回调
Android 4.4+ WebView.evaluateJavascript 可以拿到 JS 执行完毕的返回值
Android 4.4- WebView.loadUrl 无法执行回调

iOS向Web发送消息的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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发送消息的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)
// ^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
}
}

前端接收消息的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 前端注册 客户端发送消息事件的处理逻辑
* @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);

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

1
2
3
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 特点
Android addJavascriptInterface 4.2 版本以下有安全风险
iOS 8+ WKScriptMessageHandler
iOS 7+ JavaSciptCore

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 监听客户端发送的回调事件,根据回调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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 前端调用客户端注入的对象,调用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消息的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)
}
}
1
2
3
func injectJSBridge(_ scriptMessageHandler: WKScriptMessageHandler, methodName: String) {
webView.configuration.userContentController.add(WeakScriptMessageDelegate(scriptMessageHandler), name: methodName)
}
1
2
3
4
5
6
7
8
class LeonWebCallProcessor: NSObject {
weak var webView: LeonWebView?
init(webView: LeonWebView?) {
super.init()
webView?.injectJSBridge(self, methodName: 'leonJSBridge')
// ^^^^^^^^^^^^
}
}

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

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

1
2
3
4
5
6
window.webkit.messageHandlers.leonJSBridge.postMessage({
^^^^^^^^^^^^
method: name,
params,
id,
});

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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端。

1
2
3
4
5
6
7
8
struct _LeonJSExexuter {

enum Function: String {
/// 客户端回调前端
case callbackDispatchEvent = "(function() { var event = new CustomEvent('leonJSBridgeCallback', {'detail': %@}); document.dispatchEvent(event)}());"
^^^^^^^^^^^^^^^^^^^^^
}
}
1
2
3
4
5
6
7
 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消息的实现

1
2
3
4
webView?.addJavascriptInterface(
LeonJSBridge(notificationDelegate, this@webview),
"leonJSBridge"
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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)
^^^^^^^^^^^^^^^^^^^^^
}
}
1
2
3
4
5
6
7
8
9
10
11
12
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的方式:

1
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事件时,可以知道要使用哪个回调来处理数据

参考