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为tokenclass 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 ;
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等。代码是好久之前写的,现在文档整理的有点乱,代码中基本上都有注释。希望有对你有用的点。
学习记录,每天不停进步。
来源地址:https://blog.csdn.net/gloryFlow/article/details/131683122