目前使用Flutter对APP的部分页面进行改写,在原生基础上展示Flutter页面。其中遇到了打开的Flutter页面(WebView)无法响应H5桥接的问题。
按照网上的方案,WebView和H5的桥接交互主要通过JavascriptChannel实现
WebView(
javascriptChannels: <JavascriptChannel>[
_alertJavascriptChannel(context),
].toSet(),
)
JavascriptChannel _alertJavascriptChannel(BuildContext context) {
return JavascriptChannel(
name: 'Toast',
onMessageReceived: (JavascriptMessage message) {
showToast(message.message);
});
}
但是在我们的项目中使用了WebViewJavascriptBridge进行桥接的封装,所以需要在Flutter端实现WebViewJavascriptBridge的处理逻辑。
iOS有现成的WebViewJavascriptBridge的处理方案,尝试直接搬过来看看能不能跑。
需要理解WebViewJavascriptBridge实现的大致流程,主要是几个时间节点需要对应上:
1.创建桥接对象WebViewJavascriptBridge。这个直接在页面init里面初始化就行了,问题不大
_bridge = WebViewJavascriptBridge();
2.给WebViewJavascriptBridge绑定到一个页面上
iOS是在页面初始化中绑定的,Flutter页面中在WebView的onWebViewCreated绑定是比较合理的
onWebViewCreated: (WebViewController webViewController) {
_webViewController = webViewController;
_bridge.webViewController = _webViewController;
_bridge.messageHandler = null;
registerWebViewJavascriptBridgeHandler();
},
3.WebViewJavascriptBridge注册桥接方法。同上
4.WebViewJavascriptBridge通知前端启动
iOS是在webView:didFinishNavigation:(用的WKNavigationDelegate)回调中执行这一步的,对应WebView的onPageFinished
onPageFinished: (String url) {
print('Page finished loading: $url');
///获取标题
_webViewController.evaluateJavascript("document.title")
.then((result){
print(result);
setState(() {
widget.title = result;
});
});
///WebViewJavascriptBridge桥接相关 通知前端启动
_bridge.webViewPageFinished();
},
5.WebViewJavascriptBridge监听页面发出的桥接请求
iOS是在webView:decidePolicyForNavigationAction:decisionHandler:中实现的,对应WebView的navigationDelegate
navigationDelegate: (NavigationRequest request) {
print('allowing navigation to $request');
String scheme = request.url.split("://")[0]??'';
String host = request.url.split("://")[1]??'';
if(scheme.compareTo('wvjbscheme') == 0) {
if(host.compareTo('__WVJB_QUEUE_MESSAGE__') == 0) {
_bridge._flushMessageQueue();
}
else {
print('WebViewJavascriptBridge: WARNING: Received unknown WebViewJavascriptBridge command'
+' wvjbscheme://'+host);
}
}
return NavigationDecision.navigate;
},
能一一对照的话这个流程就很好理解。
WebViewJavascriptBridge.dart
typedef WVJBResponseCallbackWithCode = void Function(int code,dynamic responseData);
typedef WVJBHandler = void Function(dynamic data,WVJBResponseCallbackWithCode responseCallback);
class WebViewJavascriptBridge {
WebViewController webViewController;
WVJBHandler messageHandler;//默认Handle
WebViewJavascriptBridge() {
_startupMessageQueue = [];
_responseCallbacks = {};
_messageHandlers = {};
_uniqueId = 0;
}
int _uniqueId;
List<Map> _startupMessageQueue;
Map<String,WVJBResponseCallbackWithCode> _responseCallbacks;
Map<String,WVJBHandler> _messageHandlers;
void send(var data,WVJBResponseCallbackWithCode responseCallback) {
_sendData(data, responseCallback, '');
}
void callHandler(String handlerName, var data, WVJBResponseCallbackWithCode responseCallback) {
_sendData(data,responseCallback,handlerName);
}
void registerHandler(String handlerName, WVJBHandler handler) {
_messageHandlers[handlerName] = handler;
}
void _sendData(var data, WVJBResponseCallbackWithCode responseCallback, String handlerName) {
Map message = {};
if(data != null) {
message['data'] = data;
}
if(responseCallback != null) {
String callbackId = "objc_cb_%ld"+(++_uniqueId).toString();
_responseCallbacks[callbackId] = responseCallback;
message['callbackId'] = callbackId;
}
if(handlerName != null) {
message['handlerName'] = handlerName;
}
_queueMessage(message, -1);
}
void _flushMessageQueue() {
webViewController.evaluateJavascript('WebViewJavascriptBridge._fetchQueue();').then((response) {
var messages = json.decode(response);
print(messages);
if(messages is List != true) {
String str = messages.runtimeType.toString();
print('WebViewJavascriptBridge: WARNING: Invalid $str received: $messages');
return;
}
for (var message in messages) {
if(message is Map != true) {
String str = message.runtimeType.toString();
print('WebViewJavascriptBridge: WARNING: Invalid $str received: $message');
continue;
}
print(message);
String responseId = message['responseId']??'';
if(responseId.length > 0) {
WVJBResponseCallbackWithCode responseCallback = _responseCallbacks[responseId];
responseCallback(-1, message['responseData']);
}
else {
WVJBResponseCallbackWithCode responseCallback;
String callbackId = message['callbackId']??'';
if(callbackId.length > 0) {
responseCallback = (code, responseData) {
if(responseData == null) {
///ensure no crash
}
Map msg = {'responseId':callbackId,'responseData':responseData};
_queueMessage(msg, code);
};
}
else {
responseCallback = (code, responseData) {
//Do nothiing
};
}
WVJBHandler handler;
if((message['handlerName']??'').length > 0) {
handler = _messageHandlers[message['handlerName']];
}
else {
handler = messageHandler;
}
if(handler != null) {
handler(message['data'], responseCallback);
}
else {
print('No handler for message from JS: $message');
}
}
}
});
}
void _queueMessage(Map message, int code) {
if(_startupMessageQueue != null) {
_startupMessageQueue.add(message);
}
else {
_dispatchMessage(message, code);
}
}
void _dispatchMessage(Map message, int code) {
String messageJSON = json.encode(message);
messageJSON = messageJSON.replaceAll('\\', '\\\\');
messageJSON = messageJSON.replaceAll('\"', '\\\"');
messageJSON = messageJSON.replaceAll('\'', '\\\'');
messageJSON = messageJSON.replaceAll('\n', '\\\n');
messageJSON = messageJSON.replaceAll('\r', '\\\r');
messageJSON = messageJSON.replaceAll('\f', '\\\f');
messageJSON = messageJSON.replaceAll('\u2028', '\\\u2028');
messageJSON = messageJSON.replaceAll('\u2029', '\\\u2029');
String javascriptCommand = "WebViewJavascriptBridge._handleMessageFromObjC($code,'$messageJSON');";
webViewController.evaluateJavascript(javascriptCommand).then((value) {
});
}
void webViewPageFinished(){
///WebViewJavascriptBridge桥接相关
webViewController.evaluateJavascript("typeof WebViewJavascriptBridge == 'object'").then((result){
if(result != null) {
if(int.parse(result) == 0 ) {
rootBundle.loadString('assets/WebViewJavascriptBridge.js.txt').then((data){
webViewController.evaluateJavascript(data.toString()).then((result) {
});
});
}
}
});
if(_startupMessageQueue != null){
for(Map queuedMessage in _startupMessageQueue) {
_dispatchMessage(queuedMessage, -1);
}
_startupMessageQueue = null;
}
}
}
WebViewJavascriptBridge.js.txt
;(function() {
if (window.WebViewJavascriptBridge) { return }
var messagingIframe
var sendMessageQueue = []
var receiveMessageQueue = []
var messageHandlers = {}
var CUSTOM_PROTOCOL_SCHEME = 'wvjbscheme'
var QUEUE_HAS_MESSAGE = '__WVJB_QUEUE_MESSAGE__'
var responseCallbacks = {}
var uniqueId = 1
function _createQueueReadyIframe(doc) {
messagingIframe = doc.createElement('iframe')
messagingIframe.style.display = 'none'
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE
doc.documentElement.appendChild(messagingIframe)
}
function init(messageHandler) {
if (WebViewJavascriptBridge._messageHandler) { throw new Error('WebViewJavascriptBridge.init called twice') }
WebViewJavascriptBridge._messageHandler = messageHandler
var receivedMessages = receiveMessageQueue
receiveMessageQueue = null
for (var i=0; i<receivedMessages.length; i++) {
_dispatchMessageFromObjC(receivedMessages[i])
}
}
function send(data, responseCallback) {
_doSend({ data:data }, responseCallback)
}
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler
}
function callHandler(handlerName, data, responseCallback) {
_doSend({ handlerName:handlerName, data:data }, responseCallback)
}
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime()
responseCallbacks[callbackId] = responseCallback
message['callbackId'] = callbackId
}
sendMessageQueue.push(message)
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE
}
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue)
sendMessageQueue = []
return messageQueueString
}
function _dispatchMessageFromObjC(code , messageJSON) {
setTimeout(function _timeoutDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON)
var messageHandler
if (message.responseId) {
var responseCallback = responseCallbacks[message.responseId]
if (!responseCallback) { return; }
responseCallback(code , message.responseData)
delete responseCallbacks[message.responseId]
} else {
var responseCallback
if (message.callbackId) {
var callbackResponseId = message.callbackId
responseCallback = function(responseData) {
_doSend({ responseId:callbackResponseId, responseData:responseData })
}
}
var handler = WebViewJavascriptBridge._messageHandler
if (message.handlerName) {
handler = messageHandlers[message.handlerName]
}
try {
handler(message.data, responseCallback)
} catch(exception) {
if (typeof console != 'undefined') {
console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception)
}
}
}
})
}
function _handleMessageFromObjC(code , messageJSON) {
if (receiveMessageQueue) {
receiveMessageQueue.push(messageJSON)
} else {
_dispatchMessageFromObjC(code ,messageJSON)
}
}
window.WebViewJavascriptBridge = {
init: init,
send: send,
registerHandler: registerHandler,
callHandler: callHandler,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
}
var doc = document
_createQueueReadyIframe(doc)
var readyEvent = doc.createEvent('Events')
readyEvent.initEvent('WebViewJavascriptBridgeReady')
readyEvent.bridge = WebViewJavascriptBridge
doc.dispatchEvent(readyEvent)
})();
这波操作就是简单的搬轮子,前端在线上已经适配了安卓和IOS,再让他们适配Flutter显然不太现实,所以才去这种方式,模仿IOS走桥接逻辑
WebViewJavascriptBridge最大的优势在于封装好了“回调的回调”,即前端调起桥接,Flutter可以立马通过这个桥接把回复扔回去,而不需要再去考虑如何回复
_bridge.registerHandler('query', (data, responseCallback) {
if(responseCallback != null) {
List methods = supportJSMethods;
responseCallback(200, {'cmds':methods});//直接扔回去
}
});
只是提供一个思路,因为我估计其他前端不一定这么使用WebViewJavascriptBridge,仅作参考。