当前位置:   article > 正文

flutter开发实战-实现webview与Javascript通信JSBridge_flutter js 通信

flutter js 通信

flutter开发实战-实现webview与H5中Javascript通信JSBridge

在开发中,使用到webview,flutter实现webview是使用原生的插件实现,常用的有webview_flutter与flutter_inappwebview
这里使用的是webview_flutter,在iOS上,WebView小部件由WKWebView支持。在Android上,WebView小部件由WebView支持。

在这里插入图片描述

这里使用的是webview_flutter的3.0.4版本,不同版本代码变化还是挺大的。

一、引webview_flutter

在工程中pubspec.yaml引入webview_flutter

  # 浏览器
  webview_flutter: ^3.0.4
  webview_cookie_manager: ^2.0.6
  • 1
  • 2
  • 3

二、使用webview

2.1、webview

webview的属性

const WebView({
    Key? key,
    this.onWebViewCreated,
    this.initialUrl,
    this.initialCookies = const <WebViewCookie>[],
    this.javascriptMode = JavascriptMode.disabled,
    this.javascriptChannels,
    this.navigationDelegate,
    this.gestureRecognizers,
    this.onPageStarted,
    this.onPageFinished,
    this.onProgress,
    this.onWebResourceError,
    this.debuggingEnabled = false,
    this.gestureNavigationEnabled = false,
    this.userAgent,
    this.zoomEnabled = true,
    this.initialMediaPlaybackPolicy =
        AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
    this.allowsInlineMediaPlayback = false,
    this.backgroundColor,
  })
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

flutter webview和JS交互,需要JavaScript开启。
flutter webview中的javascriptMode参数启用或禁用 JavaScript。默认情况下WebView的 JavaScript是禁用的,所以要想启用的话,可以使用JavascriptMode.unrestricted

WebView(
  initialUrl: 'https://www.laileshuo.com',
  javascriptMode: JavascriptMode.unrestricted,
)
  • 1
  • 2
  • 3
  • 4

flutter webview提供WebViewController来获取webview信息以及控制webview的刷新、loadUrl、前进、后退等功能。

WebView(
  initialUrl: 'https://www.laileshuo.com',
  onWebViewCreated: (WebViewController webViewController) {
    _controller = webViewController;
  },
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2.2、JavascriptChannel

JavascriptChannel用于接收在web视图中运行的JavaScript代码发出的消息,提供了name与onMessageReceived。

JavascriptChannel({
    required this.name,
    required this.onMessageReceived,
  })
  • 1
  • 2
  • 3
  • 4

我们需要在Webview的javascriptChannels属性设置javascriptChannel!

javascriptChannels: <JavascriptChannel>{
        _jsChannelManager.javascriptChannel!,
      },
  • 1
  • 2
  • 3

2.3、Cookie

在使用webview的cookie时候,使用initialCookies设置cookie列表
这里我们定义了JSCookieConfig来设置需要设置的cookie

// 处理注入到webview的cookie,设置cookie通过webview_cookie_manager设置所需要的cookie列表
// Cookie:不同应用对应不同的key,value为token
class JSCookieConfig {
  JSCookieConfig() {
    eventListener();
  }

  // cookie
  final WebviewCookieManager cookieManager = WebviewCookieManager();

  List<WebViewCookie> initialCookies() {
    LoggerManager().debug("initialCookies ApiAuth().token:${ApiAuth.getToken()}");
    List<WebViewCookie> cookies = [
      WebViewCookie(
          name: "app_authorization",
          value: ApiAuth.getToken(),
          domain: ".ifour.cn"),
      WebViewCookie(
          name: "token", value: ApiAuth.getToken(), domain: ".ifour.cn"),
    ];

    return cookies;
  }

  Future<void> setCookies() async {
    // final mainCookie = Cookie('app_authorization', 'ApiAuth().token')..domain = 'ifour.cn';
    // final h5_tokenCookie = Cookie('token', 'ApiAuth().token')..domain = 'ifour.cn';
    //
    // await cookieManager.setCookies([
    //   mainCookie,
    //   h5_tokenCookie
    // ]);

    await cookieManager.setCookies([
      Cookie("app_authorization", ApiAuth.getToken())
        ..domain = '.ifour.cn'
        ..httpOnly = false,
      Cookie("token", ApiAuth.getToken())
        ..domain = '.ifour.cn'
        ..httpOnly = false,
    ]);
  }

  Future<void> clear() async {
    await cookieManager.clearCookies();
  }

  void eventListener() {
    AppEventBus().on(kUserLoginChanged, this, (arg) {
      setCookies();
    });
  }

// 注入cookie
// String cookieJS =
//     "document.cookie ='app_authorization=${ApiAuth().token};domain=.ifour.cn;path=/'";
//
// _jsChannelManager.injectJavascript(cookieJS);
}
  • 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
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

2.4、注入JS

JSBridge实现webview上原生与h5的通信,js可以调用native,native也可以调用js,实现通信。
其主要是通过拦截 URL 请求来达到 native 端和 webview 端相互通信的效果,常用的是WebviewJavascriptBridge
这里我们使用代码将WebviewJavascriptBridge的JS代码注入到flutter webview中。

flutter使用的WebviewJavascriptBridge的代码

const String kWebviewJavascriptBridge = '''
function preprocessorJS() {
    if (window.AppJSBridge) {
		return;
	}

	if (!window.onerror) {
		window.onerror = function(msg, url, line) {
			console.log("AppJSBridge: ERROR:" + msg + "@" + url + ":" + line);
		}
	}

	// var messagingIframe;
	var sendMessageQueue = [];
	var messageHandlers = {};
	
	var CUSTOM_PROTOCOL_SCHEME = 'https';
	var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
	
	var responseCallbacks = {};
	var uniqueId = 1;
	var dispatchMessagesWithTimeoutSafety = true;

	function registerHandler(handlerName, handler) {
		messageHandlers[handlerName] = handler;
	}
	
	function callHandler(handlerName, data, responseCallback) {
		if (arguments.length == 2 && typeof data == 'function') {
			responseCallback = data;
			data = null;
		}
		_doSend({ handlerName:handlerName, data:data }, responseCallback);
	}
    
    function call(handlerName, data, responseCallback) {
        if (arguments.length == 2 && typeof data == 'function') {
            responseCallback = data;
            data = null;
        }
        _doSend({ handlerName:handlerName, data:data }, responseCallback);
    }
    
	function disableJavscriptAlertBoxSafetyTimeout() {
		dispatchMessagesWithTimeoutSafety = false;
	}
	
	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;
		// 通过JavaScriptChannel注入的全局对象
		window.JSAppSDK.postMessage(JSON.stringify(message))
	}

	function _fetchQueue() {
		var messageQueueString = JSON.stringify(sendMessageQueue);
		sendMessageQueue = [];
		return messageQueueString;
	}

	function _dispatchMessageFromObjC(messageJSON) {
		if (dispatchMessagesWithTimeoutSafety) {
			setTimeout(_doDispatchMessageFromObjC);
		} else {
			 _doDispatchMessageFromObjC();
		}
		
		// 打印log
		_consoleLog("AppJSBridge: messageJSON:" + messageJSON);
		
		function _doDispatchMessageFromObjC() {
			var message = JSON.parse(messageJSON);
			var messageHandler;
			var responseCallback;
			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({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
					};
				}
				
				var handler = messageHandlers[message.handlerName];
				if (!handler) {
					_consoleLog("AppJSBridge: WARNING: no handler for message from ObjC:", message);
				} else {
					handler(message.data, responseCallback);
				}
			}
		}
	}
	
	function _handleMessageFromObjC(messageJSON) {
        _dispatchMessageFromObjC(messageJSON);
	}

	// messagingIframe = document.createElement('iframe');
	// messagingIframe.style.display = 'none';
	// messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
	// document.documentElement.appendChild(messagingIframe);

	registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);
	
	// setTimeout(_callWVJBCallbacks, 0);
	// function _callWVJBCallbacks() {
	// 	var callbacks = window.WVJBCallbacks;
	// 	delete window.WVJBCallbacks;
	// 	for (var i=0; i<callbacks.length; i++) {
	// 		callbacks[i](AppJSBridge);
	// 	}
	// }
	
    window.AppJSBridge = {
      registerHandler: registerHandler,
      callHandler: callHandler,
          call: call,
      disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
      _fetchQueue: _fetchQueue,
      _handleMessageFromObjC: _handleMessageFromObjC,
      _consoleLog: _consoleLog,
    };
    
    	// 打印log
	  function _consoleLog(message) {
	    // 显示来自flutter的回调
	    var logJSON = { 'message':message, 'logType':1 }
		  callHandler("log", JSON.stringify(logJSON));
	  }
    
    window.WeixinJSBridge = window.AppJSBridge;
    
    // 创建事件
    var event = document.createEvent('Event');
    
    // 定义事件名为'build'.
    event.initEvent('AppJSBridgeReady', true, true);

    event.bridge = window.AppJSBridge;
    
    // 触发对象可以是任何元素或其他事件目标
    document.dispatchEvent(event);
}

preprocessorJS()
''';
  • 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
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158

setupWebViewJavascriptBridge与setupWebViewJavascriptBridge判断window.AppJSBridge是否存在,通过监听AppJSBridgeReady来实现window.AppJSBridge初始化,之后js中就可以使用window.AppJSBridge中的registerHandler、callHandler等方法了。

const String kWebviewJsBridgeReady = '''
    window.onerror = function(err) {
        log('window.onerror: ' + err)
    }

    function setupWebViewJavascriptBridge(callback) {
        if (window.AppJSBridge) {
            return callback(AppJSBridge);
        } else {
            document.addEventListener('AppJSBridgeReady', function() {
                callback(AppJSBridge);
            },false);
        }

        // if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
        // window.WVJBCallbacks = [callback];
        // var WVJBIframe = document.createElement('iframe');
        // WVJBIframe.style.display = 'none';
        // WVJBIframe.src = 'https://__bridge_loaded__';
        // document.documentElement.appendChild(WVJBIframe);
        // setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
    }

    setupWebViewJavascriptBridge(function(bridge) {
        bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
            var responseData = { 'Javascript Says':'Right back atcha!' }
            responseCallback(responseData)
        });

        bridge.registerHandler('JSHandler', function(data, responseCallback) {
            var responseData = { 'Javascript Says':'Right back atcha!' }
            responseCallback(responseData)
        });
    }
''';
  • 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

在webview的onWebViewCreated将kWebviewJsBridgeReady代码注入,进行监听window.AppJSBridge是否可用。
注入的代码webController的runJavascript方法

_jsChannelManager中的代码

  // 注入js
  void injectJavascriptReady() async {
    await webController?.runJavascript('javascript:$kWebviewJsBridgeReady');
  }
  • 1
  • 2
  • 3
  • 4

webview的onWebViewCreated,webview创建后

  onWebViewCreated: (controller) {
    LoggerManager().debug("onWebViewCreated");
    // 注入jsReady
    _jsChannelManager.injectJavascriptReady();
  },
  • 1
  • 2
  • 3
  • 4
  • 5

在webview的onPageFinished将kWebviewJavascriptBridge代码注入

onPageFinished: (String url) {
    // 网页加载完成
    LoggerManager().debug('onPageFinished url: $url');
    // 注入
    _jsChannelManager.injectBridgeJavascript();
  },
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2.5、实现JSChannelManager管理处理H5与flutter webview通信

JSChannelManager中使用JavascriptChannel来接收h5端的JS消息。
当收到H5消息的时候,flutter根据callbackId回调给H5,
实现的具体代码如下

const String kJSChannelName = "JSAppSDK";

const String kOldProtocolScheme = "wvjbscheme";
const String kNewProtocolScheme = "https";
const String kQueueHasMessage = "__wvjb_queue_message__";
const String kBridgeLoaded = "__bridge_loaded__";

class JSChannelManager {
  WebViewController? webController;

  BuildContext? context;

  JavascriptChannel? javascriptChannel;

  // 存储的消息messageHandler
  Map<String, dynamic> messageHandlers = {};

  // 存储的回调callback, responseCallback
  Map<String, dynamic> responseCallbacks = {};

  // 开启的消息队列,发送的消息均会存储到该队列中
  List<JSMessage>? startupMessageQueue = [];

  // 消息的标识
  int _uniqueId = 0;

  JSChannelManager() {
    javascriptChannel = JavascriptChannel(
      name: kJSChannelName,
      onMessageReceived: (JavascriptMessage message) {
        // 将JSON字符串转成Map
        LoggerManager().debug("onMessageReceived message:${message.message}");
        flutterFlushMessageQueue();
      },
    );
  }

  void updateController(WebViewController controller, BuildContext context) {
    this.webController = controller;
    this.context = context;
  }

  JavascriptChannel getJSChannel() {
    return javascriptChannel!;
  }

  // 处理消息队列
  void flutterFlushMessageQueue() async {
    // 获取h5发送的列表
    // 处理H5存的消息队列发送的MessageQueue
    String? messageQueueString = await webController
        ?.runJavascriptReturningResult(webViewJavascriptFetchQueyCommand());
    LoggerManager().debug("flutterFlushMessageQueue:${messageQueueString}");
    flushMessageQueue(messageQueueString);
  }

  // 处理来自H5的消息列表
  void flushMessageQueue(String? messageQueueString) {
    if (!(messageQueueString != null && messageQueueString.isNotEmpty)) {
      return;
    }

    LoggerManager().debug(
        "flushMessageQueue messageQueueString:${messageQueueString}");

    dynamic? aFromH5Messages = jsonDecode(messageQueueString);

    LoggerManager().debug(
        "flushMessageQueue 1111 aFromH5Messages:${aFromH5Messages}, type:${aFromH5Messages.runtimeType}");

    if (aFromH5Messages != null && aFromH5Messages is String) {
      aFromH5Messages = jsonDecode(aFromH5Messages);
    }
    LoggerManager().debug(
        "flushMessageQueue 222 aFromH5Messages:${aFromH5Messages}, type:${aFromH5Messages.runtimeType}");
    if (aFromH5Messages != null && aFromH5Messages is List) {
      for (dynamic aMsgJson in aFromH5Messages) {
        if (aMsgJson is Map<String, dynamic>) {
          JSMessage jsMessage = JSMessage.fromJson(aMsgJson);
          LoggerManager().debug(
              "flushMessageQueue aFromH5Messages aMsgJson:${aMsgJson} jsMessage:${jsMessage}");

          // 从H5获取或者接收到的消息,如果responseId不为空,则为flutter调用H5方法,H5给flutter的回调
          if (jsMessage.responseId != null &&
              jsMessage.responseId!.isNotEmpty) {
            // 如果responseId不为空,则为flutter调用H5方法,H5给flutter的回调
            ResponseCallback? responseCallback =
                responseCallbacks[jsMessage.responseId];
            if (responseCallback != null) {
              // 处理H5返回给flutter的回调
              responseCallback(jsMessage.responseData);
            }
          } else {
            ResponseCallback? responseCallback;
            // 如果responseId为空时候,则是来自H5发送的flutter的消息
            // 获取H5传过来的标识callbackId
            String? callbackId = jsMessage.callbackId;
            if (callbackId != null && callbackId.isNotEmpty) {
              // 接收到来自H5的消息
              JSMessage aMessage = JSMessage();
              aMessage.copy(aNewMessage: aMessage, aOldMessage: jsMessage);

              responseCallback = (dynamic responseData) {
                // flutter回调给H5
                // 将H5传过来的callbackId作为responseId回调传递给H5
                aMessage.responseId = callbackId;
                aMessage.responseData = responseData;
                _queueMessage(aMessage);
              };
            } else {
              responseCallback = (dynamic responseData) {
                // callbackId为空,不做任何处理
              };
            }

            // 从flutter已经注册Register方法中找出对应的方法
            JSBridgeHandler? jsBridgeHandler =
                messageHandlers[jsMessage.handlerName];
            if (jsBridgeHandler != null) {
              // 在flutter该handlerName的方法已经注册register
              jsBridgeHandler(jsMessage.data, responseCallback);
            } else {
              // 在flutter该handlerName没有注册,则不做任何处理
            }
          }
        }
      }
    }
  }

  // 处理从H5收到的消息
  void _dispatchMessage(JSMessage message) async {
    String messageJSON = jsonEncode(message.toJson());

    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 =
        webViewJavascriptHandleMessageFromObjCCommand(messageJSON);
    await webController?.runJavascript(javascriptCommand);
  }

  // 注入js
  void injectJavascript(String javascript) async {
    await webController?.runJavascript(javascript);
  }

  // 注入js
  void injectJavascriptReady() async {
    await webController?.runJavascript('javascript:$kWebviewJsBridgeReady');
  }

  // 注入js
  void injectBridgeJavascript() async {
    await webController?.runJavascript('javascript:$kWebviewJavascriptBridge');

    LoggerManager().debug("injectJavascript");

    // 处理flutter发送的消息队列
    if (startupMessageQueue != null && startupMessageQueue!.isNotEmpty) {
      List<JSMessage> tmpList = startupMessageQueue!;
      startupMessageQueue = null;
      for (JSMessage message in tmpList) {
        _dispatchMessage(message);
      }
    }
  }

  // 向H5发送消息
  void _sendData(String handleName,
      {dynamic? data, ResponseCallback? responseCallback}) {
    String callbackId = "flutter_cb_${++_uniqueId}";

    JSMessage jsMessage = JSMessage();
    jsMessage.callbackId = callbackId;
    jsMessage.handlerName = handleName;
    jsMessage.data = data;

    // 将callbackId存储到responseCallbacks中,callbackId会被H5通过responseId返回
    if (responseCallback != null) {
      responseCallbacks[callbackId] = responseCallback;
    }

    _queueMessage(jsMessage);
  }

  // 将发送给H5的消息存到startupMessageQueue中
  void _queueMessage(JSMessage jsMessage) {
    if (startupMessageQueue != null) {
      startupMessageQueue!.add(jsMessage);
    }

    _dispatchMessage(jsMessage);
  }

  // 判断是否可以注入url
  bool isWebViewJavascriptBridgeURL(String url) {
    if (!isSchemeMatch(url)) {
      return false;
    }

    return isBridgeLoadedURL(url) || isQueueMessageURL(url);
  }

  bool isSchemeMatch(String url) {
    String lowerUrl = url.toLowerCase();
    LoggerManager().debug("isSchemeMatch lowerUrl:${lowerUrl}");
    return (lowerUrl.startsWith(kNewProtocolScheme) ||
        lowerUrl.startsWith(kOldProtocolScheme));
  }

  bool isQueueMessageURL(String url) {
    String lowerUrl = url.toLowerCase();
    LoggerManager().debug("isQueueMessageURL lowerUrl:${lowerUrl}");
    return (isSchemeMatch(url) && (lowerUrl.contains(kQueueHasMessage)));
  }

  bool isBridgeLoadedURL(String url) {
    String lowerUrl = url.toLowerCase();
    LoggerManager().debug("isBridgeLoadedURL lowerUrl:${lowerUrl}");
    return (isSchemeMatch(url) && (lowerUrl.contains(kBridgeLoaded)));
  }

  // 注入js的command
  String webViewJavascriptCheckCommand() {
    return "typeof window.AppJSBridge == \'object\';";
  }

  String webViewJavascriptFetchQueyCommand() {
    return "AppJSBridge._fetchQueue();";
  }

  String webViewJavascriptHandleMessageFromObjCCommand(String messageJSON) {
    return "AppJSBridge._handleMessageFromObjC('${messageJSON}');";
  }

  // 判断AppJSBridge
  Future<String?> checkJavascriptBridge() async {
    String? result = await webController
        ?.runJavascriptReturningResult(webViewJavascriptCheckCommand());
    LoggerManager().debug("checkJavascriptBridge result:${result}");
    return result;
  }

  /// flutter开放出去的方法,flutter调用H5方法统一使用该callHandler
  /// callHandler
  void callHandler(String handleName,
      {dynamic? data, ResponseCallback? responseCallback}) {
    if (handleName.isNotEmpty) {
      _sendData(handleName, data: data, responseCallback: responseCallback);
    }
  }

  /// flutter注册方法
  /// flutter注册方法,提供给H5调用
  void registerHandler(String handleName, JSBridgeHandler jsBridgeHandler) {
    if (handleName.isNotEmpty) {
      messageHandlers[handleName] = jsBridgeHandler;
    }
  }

  // 移除注册的方法
  void removeHandler(String handleName) {
    if (handleName.isNotEmpty) {
      messageHandlers.remove(handleName);
    }
  }

  // 重置,将responseCallbacks、startupMessageQueue重置
  void reset() {
    startupMessageQueue = [];
    responseCallbacks = {};
    _uniqueId = 0;
  }
}
  • 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
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281

2.6、JSChannelRegister:appBridge调用的方法,flutter注册的方法

JSChannelRegister实现处理flutter注册的方法,提供相应的方法,H5端的JS可以方便调用。

// appBridge调用的方法,flutter注册的方法
class JSChannelRegister {
  late JSChannelManager _jsChannelManager;

  // 支付
  final ChannelPayPlatform _channelPayPlatform = ChannelPayPlatform();

  // 打开app等
  final ChannelLauncher _channelLauncher = ChannelLauncher();

  // 弹窗
  final ChannelDialog _channelDialog = ChannelDialog();

  // 扫码或者识别二维码
  final ChannelQrScanner _channelQrScanner = ChannelQrScanner();

  JSChannelRegister({required JSChannelManager jsChannelManager}) {
    _jsChannelManager = jsChannelManager;
  }

  // 注册handlers
  void registerHandlers({JSChannelRegisterHandler? jsChannelRegisterHandler}) {
    // 设置标题
    _jsChannelManager.registerHandler(JSChannelRegisterMethod.setTitle,
        (data, responseCallback) {
      if (data != null && data is String) {
        String title = data;
        if (jsChannelRegisterHandler != null) {
          jsChannelRegisterHandler(JSChannelRegisterMethod.setTitle, title);
        }
      }
    });

    // 获取用户昵称
    _jsChannelManager.registerHandler(JSChannelRegisterMethod.getUsername,
        (data, responseCallback) {
      UserModel userModel =
          Provider.of<UserModel>(OneContext().context!, listen: false);
      String userNickName = userModel.userNickName ?? "";
      if (responseCallback != null) {
        responseCallback(userNickName);
      }
    });

    // 获取定位
    _jsChannelManager.registerHandler(JSChannelRegisterMethod.getLoc,
        (data, responseCallback) {
      // TODO 获取定位
    });

    // 获取App名称
    _jsChannelManager.registerHandler(JSChannelRegisterMethod.getAppName,
        (data, responseCallback) {
      PackageInfo.fromPlatform().then((packageInfo) {
        String appName = "${packageInfo.appName}";
        if (responseCallback != null) {
          responseCallback(appName);
        }
      });
    });

    // 获取版本号
    _jsChannelManager.registerHandler(JSChannelRegisterMethod.getVersion,
        (data, responseCallback) {
      PackageInfo.fromPlatform().then((packageInfo) {
        String version = "${packageInfo.buildNumber}";
        String versionCode = version.replaceAll(".", "");
        if (responseCallback != null) {
          responseCallback(versionCode);
        }
      });
    });

    // 获取用户id
    _jsChannelManager.registerHandler(JSChannelRegisterMethod.getUserId,
        (data, responseCallback) {
      UserModel userModel =
          Provider.of<UserModel>(OneContext().context!, listen: false);
      String userId = userModel.userId ?? "";
      if (responseCallback != null) {
        responseCallback(userId);
      }
    });

    // 获取用户登录认证token
    _jsChannelManager.registerHandler(JSChannelRegisterMethod.getAuthorization,
        (data, responseCallback) {
      UserModel userModel =
          Provider.of<UserModel>(OneContext().context!, listen: false);
      String token = userModel.token ?? "";
      if (responseCallback != null) {
        responseCallback(token);
      }
    });

    // 调用支付(微信支付/支付宝支付)原生
    _jsChannelManager.registerHandler(JSChannelRegisterMethod.setPayPlatform,
        (data, responseCallback) {
      _channelPayPlatform.openUniPay(data, responseCallback);
    });

    // 打开扫一扫
    _jsChannelManager.registerHandler(JSChannelRegisterMethod.openScan,
        (data, responseCallback) {
      // 打开扫一扫界面
      _channelQrScanner.openScanner(
          JSChannelRegisterMethod.openScan, data, responseCallback);
    });

    // 打开扫一扫
    _jsChannelManager.registerHandler(JSChannelRegisterMethod.scanQrCode,
        (data, responseCallback) {
      // 打开扫一扫界面
      _channelQrScanner.openScanner(
          JSChannelRegisterMethod.scanQrCode, data, responseCallback);
    });

    // 打系统电话
    _jsChannelManager.registerHandler(JSChannelRegisterMethod.callTelPhone,
        (data, responseCallback) {
      _channelLauncher.openLauncher(
          JSChannelRegisterMethod.callTelPhone, data, responseCallback);
    });

    // 发送短信
    _jsChannelManager.registerHandler(JSChannelRegisterMethod.sendSms,
        (data, responseCallback) {
      _channelLauncher.openLauncher(
          JSChannelRegisterMethod.sendSms, data, responseCallback);
    });

    // 对话框 showDialog
    _jsChannelManager.registerHandler(JSChannelRegisterMethod.showDialog,
        (data, responseCallback) {
      _channelDialog.openShowDialog(data, responseCallback);
    });

    // 底部选择框
    _jsChannelManager.registerHandler(JSChannelRegisterMethod.showCheckBox,
        (data, responseCallback) {
      _channelDialog.openShowSheetBox(data, responseCallback);
    });

    // 保存图片到相册
    _jsChannelManager.registerHandler(JSChannelRegisterMethod.saveImage,
        (data, responseCallback) {
      // 保存图片到相册
      if (data != null && data is String && data.isNotEmpty) {
        FlutterLoadingHud.showLoading(message: "保存中...");
        SaveToAlbumUtil.saveImage(data, onCallback: (bool result, String message) {
          FlutterLoadingHud.dismiss();
          if (result) {
            // 保存成功
            FlutterLoadingHud.showToast(message: message);
          } else {
            // 保存失败
            FlutterLoadingHud.showToast(message: message);
          }
        });
      }
    });

    // 识别二维码
    _jsChannelManager.registerHandler(JSChannelRegisterMethod.detectorQRCode,
        (data, responseCallback) {
      // 识别图片中的二维码
      _channelQrScanner.openScanner(
          JSChannelRegisterMethod.detectorQRCode, data, responseCallback);
    });

    // 打开App
    _jsChannelManager.registerHandler(JSChannelRegisterMethod.openApp,
        (data, responseCallback) {
      _channelLauncher.openLauncher(
          JSChannelRegisterMethod.openApp, data, responseCallback);
    });

    // log
    _jsChannelManager.registerHandler(JSChannelRegisterMethod.log,
        (data, responseCallback) {
      Map<String, dynamic> dataJson = jsonDecode(data);
      int loggerType = dataJson["logType"];
      String message = dataJson["message"];
      if (LoggerMode.debug == loggerType) {
        LoggerManager().debug("registerHandlers log data: ${message}");
      } else if (LoggerMode.verbose == loggerType) {
        LoggerManager().verbose("registerHandlers log data: ${message}");
      } else if (LoggerMode.info == loggerType) {
        LoggerManager().info("registerHandlers log data: ${message}");
      } else if (LoggerMode.warning == loggerType) {
        LoggerManager().warning("registerHandlers log data: ${message}");
      } else if (LoggerMode.error == loggerType) {
        LoggerManager().error("registerHandlers log data: ${message}");
      }
    });
  }

  // 处理是否跳转,true可跳转,false不可跳转
  bool navigationDecision(NavigationRequest request) {
    ///在页面跳转之前调用 isForMainFrame为false,页面不跳转.导致网页内很多链接点击没效果
    String url = Uri.decodeComponent(request.url);
    LoggerManager().debug('navigationDelegate decode $url');
    String telPrefix = "tel://";
    String smsPrefix = "sms://";
    String appPrefix = "app://";
    if (url.startsWith(telPrefix)) {
      String data = url.substring(telPrefix.length);
      _channelLauncher.openLauncher(
          JSChannelRegisterMethod.callTelPhone, data, null);
      // 不可跳转
      return false;
    }

    if (url.startsWith(smsPrefix)) {
      String data = url.substring(smsPrefix.length);
      _channelLauncher.openLauncher(
          JSChannelRegisterMethod.sendSms, data, null);
      // 不可跳转
      return false;
    }

    if (url.startsWith(appPrefix)) {
      // app://close
      _channelLauncher.openappUrl(url);
      return false;
    }

    if (url == "about:blank") {
      // 空页面进行跳转
      return true;
    }

    // 可跳转
    return true;
  }
}
  • 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
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236

使用JSChannelRegister,处理相应的callback


  void initState() {
    // TODO: implement initState
    super.initState();
    _isDisposed = false;

    _jsChannelRegister = JSChannelRegister(jsChannelManager: _jsChannelManager);
    _jsChannelRegister.registerHandlers(
        jsChannelRegisterHandler: (handlerName, data) {
      if (JSChannelRegisterMethod.setTitle == handlerName) {
        setWebPageTitle(data);
      }
    });
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

2.7、JSMessage:H5和flutter交互的消息体

class JSMessage {
  // {handlerName: getSessionID, data: , callbackId: cb_2_1665631238605}
  // handlerName
  String? handlerName;

  // data
  // flutter发送给H5的data,参数
  dynamic? data;

  /// callbackId,
  /// H5发送给flutter的callbackId,
  /// flutter处理后将调用 AppJSBridge._handleMessageFromObjC('%@');
  /// H5从responseCallbacks中根据callbackId找到callback回调方法进行执行
  String? callbackId;

  /// responseId
  /// flutter发送给H5的responseId,
  /// responseId和callbackId是一样的
  /// 如果是H5调用flutter时候,从H5过来的callbackId作为responseId回调给H5
  /// 如果是flutter调用H5,从flutter过来的callbackId作为responseId回调给flutter
  String? responseId;

  /// 回调的数据
  /// 如果是H5调用flutter时候,从flutter传给H5的responseData作为回调数据
  /// 如果是flutter调用H5,从H5传给flutter的responseData作为回调数据
  dynamic? responseData;

  JSMessage();

  JSMessage.fromJson(Map<String, dynamic> json) {
    callbackId = json['callbackId'];
    data = json['data'];
    handlerName = json['handlerName'];
    responseId = json['responseId'];
    responseData = json['responseData'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['callbackId'] = this.callbackId;
    data["data"] = this.data;
    data["handlerName"] = this.handlerName;
    data['responseId'] = this.responseId;
    data['responseData'] = this.responseData;
    return data;
  }

  void copy({required JSMessage aNewMessage, required JSMessage aOldMessage}) {
    aNewMessage.callbackId = aOldMessage.callbackId;
    aNewMessage.data = aOldMessage.data;
    aNewMessage.handlerName = aOldMessage.handlerName;
    aNewMessage.responseId = aOldMessage.responseId;
    aNewMessage.responseData = aOldMessage.responseData;
  }
}
  • 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
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

三、H5前端

我这里使用的是本地Html文件,在JS中调用window.AppJSBridge中的方法,如callHandler、registerHandler。
Html示例代码

<!DOCTYPE html>
<html>
 <head> 
  <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0" /> 
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <style type="text/css">
                body{
                    background: #f5faff;
                }
            .button{
                width: 100%;
                line-height: 38px;
                text-align: center;
                font-weight: bold;
                color: #fff;
                text-shadow:1px 1px 1px #333;
                margin:0 auto;
            }
            .button:nth-child(6n){
                margin-right: 0;
            }
            .button.gray{
                color: #8c96a0;
                text-shadow:1px 1px 1px #fff;
                border:1px solid #dce1e6;
                box-shadow: 0 1px 2px #fff inset,0 -1px 0 #a8abae inset;
                background: -webkit-linear-gradient(top,#f2f3f7,#e4e8ec);
                background: -moz-linear-gradient(top,#f2f3f7,#e4e8ec);
                background: linear-gradient(top,#f2f3f7,#e4e8ec);
            }
            </style>
  <title>
   JSBridge调用示例,常用方法调用
  </title>
 </head> 
 <body>

  <button type="button" class="button gray" id="getUsername">getUsername</button>
  <button type="button" class="button gray" id="getLoc">getLoc</button>
  <button type="button" class="button gray" id="getVersion">getVersion</button>
  <button type="button" class="button gray" id="scanQrCode">scanQrCode</button>
  <button type="button" class="button gray" id="setMenuItems">setMenuItems</button>
  <button type="button" class="button gray" id="callTelPhone">callTelPhone</button>
  <button type="button" class="button gray" id="webImagePreview">webImagePreview</button>
  <button type="button" class="button gray" id="showCheckBox">showCheckBox</button>
  <button type="button" class="button gray" id="showDialog">showDialog</button>
  <button type="button" class="button gray" id="saveImage">saveImage</button>

  <button type="button" class="button gray" id="openApp">打开其他App</button>


  <script>


        var imgURL = 'http://tupian.qqjay.com/tou3/2016/0726/fc4fe6f04843172bd6dbfeb5b6fe0686.jpg';
        var title = '分享券'
        var desc = '分享券描述内容'
        var url = 'http://www.laileshuo.com'

        var wxSharedObject = {
            thumb: imgURL,
            title: title,
            desc: desc,
            url: url
        };
        var appSharedObject = {
            thumb: imgURL,
            title: title,
            desc: desc,
            url: url
        };
            
        var getUsername=document.getElementById("getUsername");
        getUsername.addEventListener('click',function(){
            AppJSBridge.callHandler('getUsername', '',  function(response) {
                window.alert(response)
            });
        });

        
        var getLoc=document.getElementById("getLoc");
        getLoc.addEventListener('click',function(){
            AppJSBridge.callHandler('getLoc', '',  function(response) {
                window.alert(response)
            });
        });

        var getVersion=document.getElementById("getVersion");
        getVersion.addEventListener('click',function(){
            AppJSBridge.callHandler('getVersion', '',  function(response) {
                window.alert(response)
            });
        });


        var scanQrCode=document.getElementById("scanQrCode");
        scanQrCode.addEventListener('click',function(){
            AppJSBridge.callHandler('scanQrCode', '',  function(response) {
                window.alert(response)
            });
        });


        var setMenuItems=document.getElementById("setMenuItems");
        setMenuItems.addEventListener('click',function(){
            AppJSBridge.callHandler('setMenuItems', 'wxinFreind,wxinTime,weibo,refresh',  function(response) {
                
            });
        });


        var callTelPhone=document.getElementById("callTelPhone");
        var telPhone = '10086,10086';
        callTelPhone.addEventListener('click',function(){
            AppJSBridge.callHandler('callTelPhone', telPhone,  function(response) {
                                            // log('JS got response', response)
            });
        });


        var webImagePreview=document.getElementById("webImagePreview");
        var previewData = {
            'imgs' : [                                              //图片列表数组
                'http://7sbytg.com1.z0.glb.clouddn.com/yz2.png',
                'http://7sbytg.com1.z0.glb.clouddn.com/yz2.png'
            ],
            'index' : '0'                                           //进入预览时显示第几个图片
        };
        webImagePreview.addEventListener('click',function(){
            AppJSBridge.callHandler('webImagePreview', JSON.stringify(previewData),  function(response) {

            });
        });


        var showCheckBox=document.getElementById("showCheckBox");
        var bottomBox = {
            'optionList' : ['删除', '兑换', '其他']       //选项列表,选项列表对应自己的index
        };
        showCheckBox.addEventListener('click',function(){
            AppJSBridge.callHandler('showCheckBox', JSON.stringify(bottomBox),  function(response) {
                window.alert(response)
            });
        });


        var showDialog=document.getElementById("showDialog");
        var dialog = {
            'title' : '标题',             // Dialog标题
            'message' : '对话框内容',      // Dialog内容,可选
            'ok' : '确定',                // 确认按钮的文字,可选,不填时不显示该按钮
            'cancel' : '取消'             // 取消按钮的文字,可选,不填时不显示该按钮
        };
        showDialog.addEventListener('click',function(){
            AppJSBridge.callHandler('showDialog', JSON.stringify(dialog),  function(response) {
                                            // log('JS got response', response)
            });
        });


        var saveImage=document.getElementById("saveImage");
        saveImage.addEventListener('click',function(){
            AppJSBridge.callHandler('saveImage', 'https://c-ssl.duitang.com/uploads/item/201611/12/20161112230928_vJEQy.jpeg',  function(response) {
                
            });
        });

        var openApp=document.getElementById("openApp");
        openApp.addEventListener('click',function(){
            AppJSBridge.callHandler('openApp', 'weixin',  function(response) {

            });
        });
        
        if (window.AppJSBridge) {
            var dialog = {
                'title' : '标题',             // Dialog标题
                'message' : '对话框内容',      // Dialog内容,可选
                'ok' : '确定',                // 确认按钮的文字,可选,不填时不显示该按钮
                'cancel' : '取消'             // 取消按钮的文字,可选,不填时不显示该按钮
            };
            AppJSBridge.callHandler('showDialog', JSON.stringify(dialog),  function(response) {
              // log('JS got response', response)
            });
        }

        
        document.addEventListener('AppJSBridgeReady', function() {
              AppJSBridge.registerHandler('JSAPPHandler', function(data, responseCallback) {
                 var responseData = { 'Javascript Says':'Right back atcha!' }
                 responseCallback(responseData)
             });
                                
              var dialog = {
                  'title' : '标题',             // Dialog标题
                  'message' : '对话框内容',      // Dialog内容,可选
                  'ok' : '确定',                // 确认按钮的文字,可选,不填时不显示该按钮
                  'cancel' : '取消'             // 取消按钮的文字,可选,不填时不显示该按钮
              };
              AppJSBridge.callHandler('showDialog', JSON.stringify(dialog),  function(response) {
                // log('JS got response', response)
              });
        }, false);
        
        //WKWebView 可用
        document.addEventListener('visibilitychange', () => {
              if (document.hidden) {
                  // 页面被挂起
                  window.alert(document.visibilityState)
              } else {
                  // 页面呼出
                  window.alert(document.visibilityState)
              }
        })
        
    </script>
 </body>
</html>
  • 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
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218

四、flutter的webView_page页面打开对应的Html页面

这里使用的JSChannelManager、JSCookieConfig、JSChannelRegister等flutter

WebViewPage

class WebViewPage extends StatefulWidget {
  const WebViewPage({
    Key? key,
    this.arguments,
  }) : super(key: key);

  final Object? arguments;

  
  State<WebViewPage> createState() => _WebViewPageState();
}

class _WebViewPageState extends State<WebViewPage> {
  String title = "";
  String? url;

  // WebViewController
  WebViewController? _webViewController;

  double webProgress = 0.0;

  
  void initState() {
    // TODO: implement initState
    if (widget.arguments != null && widget.arguments is Map) {
      Map obj = widget.arguments as Map;
      url = obj["url"];
    }

    LoggerManager().debug("_WebViewPageState arguments:${widget.arguments}");

    LoggerManager().debug("_WebViewPageState url:${url}");

    super.initState();
  }

  
  void dispose() {
    // TODO: implement dispose
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: WebAppBar(
        toolbarHeight: 44.0,
        backgroundColor: Theme.of(context).primaryColor,
        centerWidget: Text(
          title,
          textAlign: TextAlign.center,
          overflow: TextOverflow.ellipsis,
          style: TextStyle(
            fontSize: 17,
            color: ColorUtil.hexColor(0xffffff),
            fontWeight: FontWeight.w600,
            fontStyle: FontStyle.normal,
            decoration: TextDecoration.none,
          ),
        ),
        leadingWidget: Row(
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            IconButton(
              padding: EdgeInsets.all(0.0),
              onPressed: () {
                webViewGoBack();
              },
              icon: Icon(
                Icons.arrow_back_ios,
                color: Colors.white,
                size: 24.0,
              ),
            ),
            IconButton(
              padding: EdgeInsets.all(0.0),
              onPressed: () {
                navigatorBack();
              },
              icon: Icon(
                Icons.close_rounded,
                color: Colors.white,
                size: 30.0,
              ),
            ),
          ],
        ),
        trailingWidget: Row(
          mainAxisAlignment: MainAxisAlignment.end,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            SizedBox(
              width: 28.0,
            ),
            IconButton(
              padding: EdgeInsets.all(0.0),
              onPressed: () {
                webViewReload();
              },
              icon: Icon(
                Icons.refresh_outlined,
                color: Colors.white,
                size: 28.0,
              ),
            ),
          ],
        ),
      ),
      body: Stack(
        children: [
          WebViewSkeleton(
            url: url ?? "",
            onWebResourceError: (WebResourceError error) {
              if (mounted) {
                // TODO onWebResourceError
              }
            },
            onWebProgress: (int progress) {
              if (mounted) {
                // TODO onWebProgress
                double precent = progress / 100.0;
                if (precent > 1.0) {
                  precent = 1.0;
                }

                if (precent < 0.0) {
                  precent = 0.0;
                }

                setState(() {
                  webProgress = precent;
                  LoggerManager().debug("webProgress:${webProgress}");
                });
              }
            },
            onLoadFinished: (String? url) {
              if (mounted) {
                // TODO onLoadFinished
              }
            },
            onWebTitleLoaded: (String? webTitle) {
              if (mounted) {
                setState(() {
                  title = webTitle ?? "";
                });
              }
            },
            onWebViewCreated: (WebViewController controller) {
              _webViewController = controller;
            },
          ),
          buildProgressIndicator(context),
        ],
      ),
    );
  }
  
  Widget buildProgressIndicator(BuildContext context) {
    return (webProgress != 1.0)
        ? LinearProgressIndicator(
      backgroundColor: Colors.transparent,
      valueColor:
      AlwaysStoppedAnimation(ColorUtil.hexColor(0x3b93ff)),
      value: webProgress,
      minHeight: 2,
    )
        : Container();
  }

  void navigatorBack() {
    NavigatorPageRouter.pop();
  }

  void webViewGoBack() {
    _webViewController?.canGoBack().then((res) {
      // 是否能返回上一级
      LoggerManager().debug("controller.canGoBack res: $res");
      if (true == res) {
        _webViewController?.goBack();
      } else {
        navigatorBack();
      }
    });
  }

  void webViewReload() {
    _webViewController?.reload();
  }
}
  • 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
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190

WebViewSkeleton

class WebViewSkeleton extends StatefulWidget {
  const WebViewSkeleton({
    Key? key,
    required this.url,
    required this.onWebProgress,
    required this.onWebResourceError,
    required this.onLoadFinished,
    this.onWebTitleLoaded,
    required this.onWebViewCreated,
  }) : super(key: key);

  final String url;
  final Function(int progress) onWebProgress;
  final Function(WebResourceError error) onWebResourceError;
  final Function(String? url) onLoadFinished;
  final Function(String? webTitle)? onWebTitleLoaded;
  final Function(WebViewController controller) onWebViewCreated;

  static GlobalKey<_WebViewSkeletonState> getGlobalKey() => GlobalKey();

  
  State<WebViewSkeleton> createState() => _WebViewSkeletonState();
}

class _WebViewSkeletonState extends State<WebViewSkeleton> {
  // WebViewController
  WebViewController? _webController;

  // JS与Flutter调用的message Queue
  final JSChannelManager _jsChannelManager = JSChannelManager();

  // cookie
  final JSCookieConfig _jsCookieConfig = JSCookieConfig();

  // flutter注册供H5调用的方法
  late JSChannelRegister _jsChannelRegister;

  // 尝试3次,每次间隔2秒
  int _loadTitleTimes = 0;

  bool _isDisposed = false;

  
  void initState() {
    // TODO: implement initState
    super.initState();
    _isDisposed = false;

    _jsChannelRegister = JSChannelRegister(jsChannelManager: _jsChannelManager);
    _jsChannelRegister.registerHandlers(
        jsChannelRegisterHandler: (handlerName, data) {
      if (JSChannelRegisterMethod.setTitle == handlerName) {
        setWebPageTitle(data);
      }
    });
  }

  
  void dispose() {
    // TODO: implement dispose
    _isDisposed = true;
    _jsChannelManager.reset();
    _webController?.clearCache();
    // _jsCookieConfig.clear();
    super.dispose();
  }

  // flutter调用H5方法
  void callJSMethod() {
    _jsChannelManager.callHandler("JSAPPHandler", data: {"id": "a18c9fe0d"},
        responseCallback: (dynamic responseData) {
      LoggerManager().debug("callJSMethod responseData:${responseData}");
      FlutterLoadingHud.showToast(message: jsonEncode(responseData));
    });
  }

  void webPageLoadedStart() {
    _loadTitleTimes = 0;
  }

  Future<void> getWebPageTitle({required String url}) async {
    if (_isDisposed) {
      return;
    }

    String? title = await _webController?.getTitle();
    LoggerManager().debug("getWebPageTitle:${title}");
    if (title != null && title.isNotEmpty) {
      LoggerManager().debug("webTitle a:${title}");
      setWebPageTitle(title);
    } else {
      try {
        String? result = await _webController
            ?.runJavascriptReturningResult('window.document.title');
        LoggerManager().debug("webTitle document.url:${result}");
        if (result != null && result.isNotEmpty) {
          setWebPageTitle(result);
        } else {
          result = await _webController?.runJavascriptReturningResult(
              'window.document.getElementsByTagName("title")[0]');
          LoggerManager()
              .debug("webTitle document.getElementsByTagName:${result}");
          setWebPageTitle(result);
        }
      } catch (e) {
        print("getWebPageTitle:${e.toString()}");
        // 最多尝试三次
        if (_loadTitleTimes < 3) {
          Future.delayed(Duration(seconds: 2), () {
            _loadTitleTimes++;
            getWebPageTitle(url: url);
          });
        }
      }
    }
  }

  // 设置页面标题
  void setWebPageTitle(data) {
    if (widget.onWebTitleLoaded != null) {
      widget.onWebTitleLoaded!(data);
    }
  }

  // 返回
  void goBack() {
    _webController?.canGoBack().then((res) {
      // 是否能返回上一级
      LoggerManager().debug("controller.canGoBack res: $res");
      if (true == res) {
        _webController?.goBack();
      }
    });
  }

  // 刷新
  void reload() {
    _webController?.reload();
  }

  
  Widget build(BuildContext context) {
    return buildWebView(context);
  }

  Widget buildWebView(BuildContext context) {
    UserModel userModel = Provider.of<UserModel>(context, listen: false);
    LoggerManager().debug("ApiAuth().token:${ApiAuth.getToken()}");
    return WebView(
      debuggingEnabled: true,
      initialUrl: widget.url,
      javascriptMode: JavascriptMode.unrestricted,
      userAgent: "app-yjxdh-webview",
      initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
      allowsInlineMediaPlayback: true,
      initialCookies: _jsCookieConfig.initialCookies(),
      onWebViewCreated: (controller) {
        LoggerManager().debug("onWebViewCreated");
        _jsCookieConfig.setCookies();

        // controller.loadUrl(url);此时也可以初始化一个url
        controller.canGoBack().then((res) {
          // 是否能返回上一级
          LoggerManager().debug("controller.canGoBack res: $res");
        });
        controller.currentUrl().then((url) {
          // 返回当前url
          LoggerManager().debug("controller.currentUrl url: $url");
        });
        controller.canGoForward().then((res) {
          //是否能前进
          LoggerManager().debug("controller.canGoForward res: $res");
        });
        _webController = controller;
        _jsChannelManager.updateController(controller, context);

        String filePre = "file://";
        if (widget.url.startsWith(filePre)) {
          String html = widget.url.substring(filePre.length);
          DefaultAssetBundle.of(context)
              .loadString('assets/htmls/${html}')
              .then((value) => _webController?.loadHtmlString(value));
        } else {
          if (widget.url.startsWith("http://") ||
              widget.url.startsWith("https://")) {
            _webController?.loadUrl(widget.url, headers: {
              'Referer': widget.url,
            });
          }
        }

        // 注入jsReady
        _jsChannelManager.injectJavascriptReady();

        widget.onWebViewCreated(controller);
      },
      onProgress: (int progress) {
        widget.onWebProgress(progress);
      },
      javascriptChannels: <JavascriptChannel>{
        _jsChannelManager.javascriptChannel!,
      },
      navigationDelegate: (NavigationRequest request) {
        bool canNavigate = _jsChannelRegister.navigationDecision(request);
        // 允许路由替换
        return canNavigate
            ? NavigationDecision.navigate
            : NavigationDecision.prevent;
      },
      onPageStarted: (String url) {
        // 网页开始加载
        webPageLoadedStart();
        LoggerManager().debug('onPageStarted url: $url');
      },
      onPageFinished: (String url) {
        // 网页加载完成
        LoggerManager().debug('onPageFinished url: $url');

        // 注入
        _jsChannelManager.injectBridgeJavascript();

        _jsChannelManager.checkJavascriptBridge();

        // 加载完成
        widget.onLoadFinished(url);

        // 获取网页的标题
        getWebPageTitle(url: url);
      },
      gestureNavigationEnabled: true,
      backgroundColor: ColorUtil.hexColor(0xf7f7f7),
      onWebResourceError: (WebResourceError error) {
        /// error
        LoggerManager().debug("onWebResourceError:${error}");
        widget.onWebResourceError(error);
      },
    );
  }

  Widget buildButtonRow(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.end,
      crossAxisAlignment: CrossAxisAlignment.center,
      children: [
        buildButton(context),
        SizedBox(
          width: 10.0,
        ),
        buildRefreshButton(context),
      ],
    );
  }

  // 展开的按钮
  Widget buildButton(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.white,
        border: Border.all(
          color: Colors.black26,
          width: 1.0,
          style: BorderStyle.solid,
        ),
        borderRadius: BorderRadius.all(
          Radius.circular(8.0),
        ),
      ),
      child: TextButton(
        onPressed: () {
          callJSMethod();
        },
        child: Text(
          '调用JS方法菜单',
          style: TextStyle(
            fontSize: 12,
            color: Colors.black,
          ),
        ),
      ),
    );
  }

  // 刷新按钮
  Widget buildRefreshButton(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.white,
        border: Border.all(
          color: Colors.black26,
          width: 1.0,
          style: BorderStyle.solid,
        ),
        borderRadius: BorderRadius.all(
          Radius.circular(8.0),
        ),
      ),
      child: TextButton(
        onPressed: () {
          reload();
        },
        child: Text(
          '刷新WebView',
          style: TextStyle(
            fontSize: 12,
            color: Colors.black,
          ),
        ),
      ),
    );
  }
}
  • 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
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311

六、运行效果图

在这里插入图片描述

在这里插入图片描述

五、小结

flutter开发实战-webview_flutter结合javascriptbridge实现flutter与html交互,通过使用flutter webview通过javascriptBridge来进行交互、交互用到了JavascriptChannel、cookie等。代码是好久之前写的,现在文档整理的有点乱,代码中基本上都有注释。希望有对你有用的点。

学习记录,每天不停进步。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/93510
推荐阅读
相关标签
  

闽ICP备14008679号