【Flutter混编】基于WebViewJavascriptBridge的flutter与H5桥接方案

阎善
2023-12-01

应用场景

目前使用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)
})();

Ex

这波操作就是简单的搬轮子,前端在线上已经适配了安卓和IOS,再让他们适配Flutter显然不太现实,所以才去这种方式,模仿IOS走桥接逻辑

WebViewJavascriptBridge最大的优势在于封装好了“回调的回调”,即前端调起桥接,Flutter可以立马通过这个桥接把回复扔回去,而不需要再去考虑如何回复

    _bridge.registerHandler('query', (data, responseCallback) {
      if(responseCallback != null) {
        List methods = supportJSMethods;
        responseCallback(200, {'cmds':methods});//直接扔回去
      }
    });

只是提供一个思路,因为我估计其他前端不一定这么使用WebViewJavascriptBridge,仅作参考。

 类似资料: