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

news2024/11/24 19:34:19

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

二、使用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,
  })

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

WebView(
  initialUrl: 'https://www.laileshuo.com',
  javascriptMode: JavascriptMode.unrestricted,
)

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

WebView(
  initialUrl: 'https://www.laileshuo.com',
  onWebViewCreated: (WebViewController webViewController) {
    _controller = webViewController;
  },
);

2.2、JavascriptChannel

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

JavascriptChannel({
    required this.name,
    required this.onMessageReceived,
  })

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

javascriptChannels: <JavascriptChannel>{
        _jsChannelManager.javascriptChannel!,
      },

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);
}

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()
''';

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)
        });
    }
''';

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

_jsChannelManager中的代码

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

webview的onWebViewCreated,webview创建后

  onWebViewCreated: (controller) {
    LoggerManager().debug("onWebViewCreated");
    // 注入jsReady
    _jsChannelManager.injectJavascriptReady();
  },

在webview的onPageFinished将kWebviewJavascriptBridge代码注入

onPageFinished: (String url) {
    // 网页加载完成
    LoggerManager().debug('onPageFinished url: $url');
    // 注入
    _jsChannelManager.injectBridgeJavascript();
  },

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;
  }
}

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;
  }
}

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

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;
  }
}

三、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>

四、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();
  }
}

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,
          ),
        ),
      ),
    );
  }
}

六、运行效果图

在这里插入图片描述

在这里插入图片描述

五、小结

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/747085.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【IMX6ULL驱动开发学习】20. input子系统(按键实现ls命令)

一、input子系统相关结构体 二、input子系统实例实现&#xff1a; static struct input_dev *key_input;...... ....../* 1.申请空间 */ key_input input_allocate_device(); if(key_input NULL){printk("input alloc failed\n");return -ENOMEM; } /* 2.设置事件类…

Go实现WebSocket

Go语言标准包里面没有提供对WebSocket的支持&#xff0c;但是在由官方维护的go.net子包中有对这个的支持&#xff0c;你可以通过如下的命令获取该包&#xff1a; go get golang.org/x/net/websocket WebSocket分为客户端和服务端&#xff0c;接下来我们将实现一个简单的例子:…

云进销存是什么意思,云进销存系统该如何选?

云进销存是指基于云计算技术的一种进销存管理系统&#xff0c;云进销存系统可以帮助企业或中小商户实时监控库存情况、优化采购和销售流程、提高物流效率&#xff0c;以及生成各类报表和统计分析&#xff0c;从而提升企业或商户的运营效率和管理水平。 一、云进销存对中小商户有…

C#,数值计算——不完全 Beta 函数(incomplete beta function)的源代码

Incomplete Beta Function The incomplete beta function (also called the Euler Integral) is a generalized β-function; An independent integral (with integral bounds from 0 to x) replaces the definite integral. The formula is: Where: 0 ≤ x ≤ 1, a, b > 0…

Pro白嫖esri数据

最近用Pro比较多,想跟大家谈谈一些关于Pro的 技巧。在谈之前,我想问大家一个问题,你真的了解ArcGIS Pro吗? 我想大多数刚刚接触Pro的用户应该是把Pro当做像Map一样的数据处理分析工具,只是简单的从其他地方下载数据来加入工程进行处理和分析 或许在你眼里的Pro和Map仅有…

在Orangepi上使用raspberry的dashboard

树莓派实验室整了一个比较酷的dashboard&#xff0c;可以用来显示树莓派状态&#xff0c;主要内容是基于js和php来实现&#xff0c;因为orangepi的用户名和密码都是一个套路&#xff0c;首先想到能不能移植。 https://www.rstk.cn/news/860562.html?actiononClick 首先需要做…

Docker把公共镜像推送到harbor私服的流程(企业级)

如果构建项目时&#xff0c;使用了k8s docker Jenkins的模式。 那么我们在docker构建镜像时&#xff0c;如果需要使用了Nodejs&#xff0c;那么我们必须得从某个资源库中拉取需要的Nodejs。 在企业里&#xff0c;正常都会把自己项目涉及的库都放在harbor私服里。 下面讲一下&…

数据分类分级

数据分类是数据管理的第一步&#xff0c;是数据治理的先行条件。当前&#xff0c;数据应用方兴未艾。“数据”作为新的生产要素资源&#xff0c;支撑供给侧结构性改革、驱动制造业转型升级的作用日益显现&#xff0c;正成为推动质量变革、效率变革、动力变革的新引擎。但与此同…

python3+requests+unittest实战系列【二】

前言&#xff1a;上篇文章python3requestsunittest&#xff1a;接口自动化测试&#xff08;一&#xff09;已经介绍了基于unittest框架的实现接口自动化&#xff0c;但是也存在一些问题&#xff0c;比如最明显的测试数据和业务没有区分开&#xff0c;接口用例不便于管理等&…

ROS中bag的录制、播放和使用

文章目录 前言一、bag录制二、bag信息查看三、bag播放四、bag的使用&#xff08;以A-LOAM为例&#xff09; 前言 传感器获取到的信息&#xff0c;有时我们可能需要实时处理&#xff0c;有时可能只是采集数据&#xff0c;事后分析&#xff0c;比如: 机器人导航实现中&#xff0…

Tomcat 基础

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 一、概述 二、安装 三、 目录结构 四、启停 五、配置文件 1. server.xml (1) Server (2) Listener (3) GlobalNamingResources (4) Service 01.Connector (1) port…

硬件故障恢复出文件之后数据库故障处理---惜分飞

客户那边硬件故障(raid损坏磁盘超过了极限,导致raid offline),通过硬件恢复出来数据文件,然后尝试自行恢复,我接手的时候大量数据文件resetlogs scn异常. 重建控制文件报错 WARNING: Default Temporary Tablespace not specified in CREATE DATABASE command Default Tempora…

Git安装详解(写吐了,看完不后悔)

Git 是一个非常流行的分布式版本控制系统&#xff0c;它帮助开发者管理和跟踪项目中的代码变化。通俗地说&#xff0c;可以认为 Git 就像是一个代码的时间机器&#xff0c;它记录了项目从开始到结束的每一次代码变动。 无论你是个人开发者还是团队成员&#xff0c;掌握 Git 都能…

三防平板在工业生产中的物料追溯与供应链管理

科技的不断发展和技术的不断进步&#xff0c;越来越多的企业开始关注物料追溯和供应链管理的重要性。特别是在工业生产中&#xff0c;确保物料的安全性和可追溯性对于提高生产效率和产品质量至关重要。10.1寸三防平板采用新一代英特尔Jasper Lake平台处理器赛扬RN5100&#xff…

SpringBoot2+Vue2实战(十七)vue集成markdown实现多级评论功能

新建数据库表 Article Data TableName("sys_article") public class Article implements Serializable {private static final long serialVersionUID 1L;TableId(value "id", type IdType.AUTO)private Integer id;/*** 标题*/private String name;/…

FreerRTOS(二值信号量和计数型信号量)

什么是信号量&#xff1f; 信号量&#xff08;Semaphore&#xff09;&#xff0c;是在多任务环境下使用的一种机制&#xff0c;是可以用来保证两个或多个关键代 码段不被并发调用。 信号量这个名字&#xff0c;我们可以把它拆分来看&#xff0c;信号可以起到通知信号的作用&…

机器学习 day26(多标签分类,Adam算法,卷积层)

多标签分类 多标签分类&#xff1a;对于单个输入特征&#xff0c;输出多个不同的标签y多类分类&#xff1a;对于单个输入特征&#xff0c;输出单个标签y&#xff0c;但y的可能结果有多个 为多标签分类构建神经网络模型 我们可以构建三个不同的神经网络模型来分别预测三个不…

C++14新特性扫盲探究

闲暇之时&#xff0c;聊到C14&#xff0c;实际上C14相对之前的11并没有太大的改动&#xff0c;或者说更像C11标准基础上的查漏补缺&#xff0c;C14之后&#xff0c;还有17、20甚至23&#xff0c;所以说&#xff0c;C14更像个过渡版本。 下面粗略聊聊C14新特性&#xff1a; 语言…

解决Ubuntu下arm-none-linux-gnueabihf-gcc -v :未找到命令

问题&#xff1a;arm-none-linux-gnueabihf-gcc -v arm-none-linux-gnueabihf-gcc&#xff1a;未找到命令 学习MP135开发板搭建环境之后没gcc不可用&#xff0c;网上找了很多教程都没法解决 解决方法&#xff1a; 1、重启&#xff1a;&#xff08;我试了没用&#xff09; 2、…

Element分页组件自定义样式

样式效果 页面代码 <el-paginationsize-change"handleSizeChange"current-change"handleCurrentChange":current-page"page.page":page-sizes"[10, 20, 30, 40]":page-size"page.size"layout"total, sizes, prev, …