Flutter 封装官方【webview_flutter】,增加JSBridge回调

文章目录

  • 前言
    • 1.引入库
    • 2.使用方法
    • 3.官方提供的JavascriptChannel
    • 4.封装互操作及回调
        • 流程1
        • 流程2
        • 流程3
        • 流程4
  • 总结


前言

在进行flutter 开发的时候,我们需要使用webview 打开h5 的页面,但是在flutter 中并没有提供类似Webview 这样的widget ,所以我们只有用platformview 的方式 ‘桥接’原生的webview .
浏览flutter pub 发现,官方提供了一个Flutter plugin 【webview_flutter】


1.引入库

代码如下(示例):

dependencies:
  webview_flutter: ^3.0.2

2.使用方法

代码如下(示例):

import 'package:webview_flutter/webview_flutter.dart';

 /// webview 是否加载错误,加载错误则加载错误页面
  bool webviewError = false;
  
  Widget _buildBody(BuildContext context) {
    return IndexedStack(
      index: webviewError ? 1 : 0,
      children: [
        Column(
          children: [
          /// 进度条
            _buildProgressbar(),
            _buildWebview(context),
          ],
        ),
        Column(
          children: [
            _buildProgressbar(),
            /// 错误页面
            _buildError()!,
          ],
        )
      ],
    );
  }

///***************************************

Widget _buildWebview(BuildContext context) {
    return Expanded(
      flex: 1,
      child: WebView(
        initialCookies: cookieHelper.getCookies(widget.webviewUrl),
        javascriptMode: JavascriptMode.unrestricted,
        onWebViewCreated: (controller) {
          _controller = controller;
       
          // _controller?.loadUrl(widget.webviewUrl);
          DefaultAssetBundle.of(context)
              .loadString('assets/demo.html')
              .then((value) => _controller?.loadHtmlString(value));
        },
        onProgress: (int progress) {
          _handleProgress(progress);
        },
        javascriptChannels: HashSet<JavascriptChannel>(),
        navigationDelegate: (NavigationRequest request) {
          String url = Uri.decodeComponent(request.url);
          return NavigationDecision.navigate;
        },
        onPageStarted: (String url) {
			/// start
        },
        onPageFinished: (String url) {
     		/// end
          bridgeHelper.injectJsBridge();
        gestureNavigationEnabled: true,
        backgroundColor: const Color(0x00000000),
        onWebResourceError: (WebResourceError error) {
			/// error
        },
      ),
    );
  }

稍微提一下,在上面 buildBody 方法中为啥使用 IndexedStack 主要是因为无论是webview 加载成功与否都需要保证webview 在widget 树中,不然后面拿到的webViewController 容易 为空。


3.官方提供的JavascriptChannel

  JavascriptChannel({
    @required this.name, // js 调用时的变量名,
    // 如name="Print", js可以通过 Print.postMessage(msg) 调用flutter
    // 请求会在 onMessageReceived 函数中处理
    
    @required this.onMessageReceived, // 处理js 请求
    // typedef void JavascriptMessageHandler(JavascriptMessage message);
    // message.message 即 js 调用时传递的msg
    // 函数没有返回值
    
  })  : assert(name != null),
        assert(onMessageReceived != null),
        assert(_validChannelNames.hasMatch(name));

可以看到官方的js channel 并没有提供回调。如果h5 调用flutter 的方法并想获取返回值,则是比较麻烦的。所以我们就仿照原生Android JsBridge 库来封装一套。

4.封装互操作及回调

封装模仿的是Android 的jsbridge 库。
对于这个库的实现原理大家可以看下源码。
主要流程:(下图来自网络,侵删)
Flutter 封装官方【webview_flutter】,增加JSBridge回调_第1张图片

流程1

在flutter webview 加载url 完成,在 onPageFinished 回调中注入javascript .

await _controller?.runJavascript('javascript:$kWebviewJsBridge');

当我们在h5 页面上点击按钮,调用flutter 方法的时候,就会触发 flutter webview 的 navigationDelegate 方法回调.

流程2

我们在 navigationDelegate 回调里进行数据的处理,以及回调的添加:

 navigationDelegate: (NavigationRequest request) {
 		  /// decode
          String url = Uri.decodeComponent(request.url);
          debugPrint('navigationDelegate decode $url');

          if (url.startsWith(yyReturunData)) {
            bridgeHelper.handlerReturnData(url);
            return NavigationDecision.prevent;
          } else if (url.startsWith(yyOverrideSchema)) {
            bridgeHelper.flushMessageQueue();
            return NavigationDecision.prevent;
          }
          return NavigationDecision.navigate;
        },

首先需要对拦截到的进行 urlDecode 处理。
根据我们在第一步注入的javascript 可知,当我们点击h5 按钮,调用的js 方法是:

kWebviewJsBridge

    //sendMessage add message, 触发native处理 sendMessage
    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;
    }

所以 url startWith 【 yy:// 】,在navigationDelegate 回调里会走 flushMessageQueue 方法,

JsBridgeHelper

  void flushMessageQueue() async {
    String functionName = _parseFunctionName(jsFetchQueueFromFlutter);
    debugPrint("flushMessageQueue: functionName = $functionName");
    responseCallbacks[functionName] = (data) {
      debugPrint('flushMessageQueue:data =  $data');
      if (null == data) return;
      List<JsMessage>? msgList = JsMessage.toArrayList(data);
      if (null == msgList || msgList.isEmpty) return;
      for (var msg in msgList) {
        String? responseId = msg.responseId;
        if (null != responseId && responseId.isNotEmpty) {
          CallBackFunction? function = responseCallbacks[responseId];
          String? responseData = msg.responseData;
          function?.call(responseData);
          responseCallbacks.remove(responseId);
        } else {
          CallBackFunction? responseFunction;
          String? callbackId = msg.callbackId;
          if (null != callbackId && callbackId.isNotEmpty) {
            responseFunction = (newData) {
              JsMessage m = JsMessage()
                ..responseData = newData
                ..responseId = callbackId;
              _queueMessage(m);
            };
          }
          BridgeHandler? handler;
          String? handlerName = msg.handlerName;
          String? data = msg.data;
          if (null != handlerName && handlerName.isNotEmpty) {
            handler = messageHandlers[handlerName];
          }
          debugPrint("flushMessageQueue: , handlerName = $handlerName");
          handler?.call(data, responseFunction);
        }
      }
    };
    debugPrint('flushMessageQueue: runJavascript: $jsFetchQueueFromFlutter');
    await _controller?.runJavascript(jsFetchQueueFromFlutter);
  }

流程3

在上一步中,方法调用最后,通过 webview control 执行了 我们开始注入的javascript , js 调用方法为:
kWebviewJsBridge

    // 提供给native调用,该函数作用:获取sendMessageQueue返回给native,由于android不能直接获取返回的内容,所以使用url shouldOverrideUrlLoading 的方式返回内容
    function _fetchQueue() {
        var messageQueueString = JSON.stringify(sendMessageQueue);
        sendMessageQueue = [];
        //add by hq
        if (isIphone()) {
            return messageQueueString;
            //android can't read directly the return data, so we can reload iframe src to communicate with java
        } else if (isAndroid()) {
            messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
        }
    }

在方法的最后,js 执行了刷新,所以又回到 navigationDelegate 回调,这次的 url startWith 【yy://return/】,所以就到了

JsBridgeHelper

  void handlerReturnData(String url) {
    String functionName = _getFunctionFromReturnUrl(url);
    debugPrint("handlerReturnData: functionName = $functionName");
    CallBackFunction? f = responseCallbacks[functionName];
    String? data = _getDataFromReturnUrl(url);
    if (null != f) {
      f.call(data);
      responseCallbacks.remove(functionName);
    }
  }

在这里把回调函数执行,f.call();

其实在一开始,我们就先注册了 N 个与h5 约定好的 js 方法,放到了 定义的map 中:
JsBridgeHelper

  Map<String, CallBackFunction> responseCallbacks = {};
  Map<String, BridgeHandler> messageHandlers = {};

在流程2 中我们解析拿到的h5 数据,然后进行的处理。从flutter 中拿到的参数,最终通过f.call() 的方式回调给了h5.

流程4

如何回调的呢,当然是执行javascript 了。
JsBridgeHelper


  void _queueMessage(JsMessage m) {
    try {
      String messageJson = jsonEncode(m.toJson());
      String js = sprintf(jsHandleMessageFromFlutter, [messageJson]);
      _controller?.runJavascript(js);

      debugPrint("queueMessage: js = $js");
    } catch (e) {
      debugPrint('queueMessage: $e');
    }
  }

然后就会执行到开始我们注入的 javascript 里面的方法:
kWebviewJsBridge

    //提供给native调用,receiveMessageQueue 在会在页面加载完后赋值为null,所以
    function _handleMessageFromNative(messageJSON) {
        console.log(messageJSON);
        if (receiveMessageQueue) {
            receiveMessageQueue.push(messageJSON);
        } else {
            _dispatchMessageFromNative(messageJSON);
        }
    }

至此到这里就执行完一整个流程了。

总结

这篇主要是介绍了如何在官方提供的webview plugin 中没有js 回调的情况下,自己如何添加的过程。
当然也参考了原生的 jsbridge 库。
kWebviewJsBridge 完整版。(其实这个就是 原生 jsbridge 里面就有。demo.html 也有。)

const String kWebviewJsBridge='''
//notation: js file can only use this kind of comments
//since comments will cause error when use in webview.loadurl,
//comments will be remove by java use regexp
(function() {
    if (window.WebViewJavascriptBridge) {
        return;
    }

    var messagingIframe;
    var sendMessageQueue = [];
    var receiveMessageQueue = [];
    var messageHandlers = {};

    var CUSTOM_PROTOCOL_SCHEME = 'yy';
    var QUEUE_HAS_MESSAGE = '__QUEUE_MESSAGE__/';

    var responseCallbacks = {};
    var uniqueId = 1;

    var base64encodechars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    function base64encode(str) {
        if (str === undefined) {
            return str;
        }

        var out, i, len;
        var c1, c2, c3;
        len = str.length;
        i = 0;
        out = "";
        while (i < len) {
            c1 = str.charCodeAt(i++) & 0xff;
            if (i == len) {
                out += base64encodechars.charAt(c1 >> 2);
                out += base64encodechars.charAt((c1 & 0x3) << 4);
                out += "==";
                break;
            }
            c2 = str.charCodeAt(i++);
            if (i == len) {
                out += base64encodechars.charAt(c1 >> 2);
                out += base64encodechars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xf0) >> 4));
                out += base64encodechars.charAt((c2 & 0xf) << 2);
                out += "=";
                break;
            }
            c3 = str.charCodeAt(i++);
            out += base64encodechars.charAt(c1 >> 2);
            out += base64encodechars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xf0) >> 4));
            out += base64encodechars.charAt(((c2 & 0xf) << 2) | ((c3 & 0xc0) >> 6));
            out += base64encodechars.charAt(c3 & 0x3f);
        }
        return out;
    }


    function _createQueueReadyIframe(doc) {
        messagingIframe = doc.createElement('iframe');
        messagingIframe.style.display = 'none';
        doc.documentElement.appendChild(messagingIframe);
    }

    function isAndroid() {
        var ua = navigator.userAgent.toLowerCase();
        var isA = ua.indexOf("android") > -1;
        if (isA) {
            return true;
        }
        return false;
    }

    function isIphone() {
        var ua = navigator.userAgent.toLowerCase();
        var isIph = ua.indexOf("iphone") > -1;
        if (isIph) {
            return true;
        }
        return false;
    }

    //set default messageHandler
    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++) {
            _dispatchMessageFromNative(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);
    }

    //sendMessage add message, 触发native处理 sendMessage
    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;
    }

    // 提供给native调用,该函数作用:获取sendMessageQueue返回给native,由于android不能直接获取返回的内容,所以使用url shouldOverrideUrlLoading 的方式返回内容
    function _fetchQueue() {
        var messageQueueString = JSON.stringify(sendMessageQueue);
        sendMessageQueue = [];
        //add by hq
        if (isIphone()) {
            return messageQueueString;
            //android can't read directly the return data, so we can reload iframe src to communicate with java
        } else if (isAndroid()) {
            messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
        }
    }

    //提供给native使用,
    function _dispatchMessageFromNative(messageJSON) {
        setTimeout(function() {
            var message = JSON.parse(messageJSON);
            var responseCallback;
            //java call finished, now need to call js callback function
            if (message.responseId) {
                responseCallback = responseCallbacks[message.responseId];
                if (!responseCallback) {
                    return;
                }
                responseCallback(message.responseData);
                delete responseCallbacks[message.responseId];
            } else {
                //直接发送
                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];
                }
                //查找指定handler
                try {
                    handler(message.data, responseCallback);
                } catch (exception) {
                    if (typeof console != 'undefined') {
                        console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
                    }
                }
            }
        });
    }

    //提供给native调用,receiveMessageQueue 在会在页面加载完后赋值为null,所以
    function _handleMessageFromNative(messageJSON) {
        console.log(messageJSON);
        if (receiveMessageQueue) {
            receiveMessageQueue.push(messageJSON);
        } else {
            _dispatchMessageFromNative(messageJSON);
        }
    }

    var WebViewJavascriptBridge = window.WebViewJavascriptBridge = {
        init: init,
        send: send,
        registerHandler: registerHandler,
        callHandler: callHandler,
        _fetchQueue: _fetchQueue,
        _handleMessageFromNative: _handleMessageFromNative
    };

    var doc = document;
    _createQueueReadyIframe(doc);
    var readyEvent = doc.createEvent('Events');
    readyEvent.initEvent('WebViewJavascriptBridgeReady');
    readyEvent.bridge = WebViewJavascriptBridge;
    doc.dispatchEvent(readyEvent);
})();''';

参考资料:
Android JsBridge 原理解析
jsbridge

你可能感兴趣的:(flutter,flutter,webview,android)