持续创作,加速成长!这是我参与「掘金日新计划 · 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-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发送消息的实现
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 | 特点 |
---|---|---|
Android | addJavascriptInterface | 4.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端调用的消息。
然后根据postMessage
的method
,决定调用哪个方法执行处理逻辑。
最后,调用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
- iOS使用
- 前端通过监听
document
上的事件leonJSBridgeCallback
,接收到客户端传递过来的消息体,并进行处理 - 重要的一点:
callbackID
的透传,这样前端在监听leonJSBridgeCallback
事件时,可以知道要使用哪个回调来处理数据