概述
原生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
|
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)]; }; }
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
| 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
| 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端调用的消息。
然后根据postMessage
的method
,决定调用哪个方法执行处理逻辑。
最后,调用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
事件时,可以知道要使用哪个回调来处理数据
参考