赞
踩
在进行flutter 开发的时候,我们需要使用webview 打开h5 的页面,但是在flutter 中并没有提供类似Webview 这样的widget ,所以我们只有用platformview 的方式 ‘桥接’原生的webview .
浏览flutter pub 发现,官方提供了一个Flutter plugin 【webview_flutter】
代码如下(示例):
dependencies:
webview_flutter: ^3.0.2
代码如下(示例):
import 'package:webview_flutter/webview_flutter.dart'; /// webview 是否加载错误,加载错误则加载错误页面 bool webviewError = false; Widget _buildBody(BuildContext context) { return IndexedStack( index: webviewError ? 1 : 0, children: [ Column( children: [ /// 进度条 _buildProgressbar(), _buildWebview(context), ], ), Column( children: [ _buildProgressbar(), /// 错误页面 _buildError()!, ], ) ], ); } ///*************************************** Widget _buildWebview(BuildContext context) { return Expanded( flex: 1, child: WebView( initialCookies: cookieHelper.getCookies(widget.webviewUrl), javascriptMode: JavascriptMode.unrestricted, onWebViewCreated: (controller) { _controller = controller; // _controller?.loadUrl(widget.webviewUrl); DefaultAssetBundle.of(context) .loadString('assets/demo.html') .then((value) => _controller?.loadHtmlString(value)); }, onProgress: (int progress) { _handleProgress(progress); }, javascriptChannels: HashSet<JavascriptChannel>(), navigationDelegate: (NavigationRequest request) { String url = Uri.decodeComponent(request.url); return NavigationDecision.navigate; }, onPageStarted: (String url) { /// start }, onPageFinished: (String url) { /// end bridgeHelper.injectJsBridge(); gestureNavigationEnabled: true, backgroundColor: const Color(0x00000000), onWebResourceError: (WebResourceError error) { /// error }, ), ); }
稍微提一下,在上面 buildBody 方法中为啥使用 IndexedStack 主要是因为无论是webview 加载成功与否都需要保证webview 在widget 树中,不然后面拿到的webViewController 容易 为空。
JavascriptChannel({
@required this.name, // js 调用时的变量名,
// 如name="Print", js可以通过 Print.postMessage(msg) 调用flutter
// 请求会在 onMessageReceived 函数中处理
@required this.onMessageReceived, // 处理js 请求
// typedef void JavascriptMessageHandler(JavascriptMessage message);
// message.message 即 js 调用时传递的msg
// 函数没有返回值
}) : assert(name != null),
assert(onMessageReceived != null),
assert(_validChannelNames.hasMatch(name));
可以看到官方的js channel 并没有提供回调。如果h5 调用flutter 的方法并想获取返回值,则是比较麻烦的。所以我们就仿照原生Android JsBridge 库来封装一套。
封装模仿的是Android 的jsbridge 库。
对于这个库的实现原理大家可以看下源码。
主要流程:(下图来自网络,侵删)
在flutter webview 加载url 完成,在 onPageFinished 回调中注入javascript .
await _controller?.runJavascript('javascript:$kWebviewJsBridge');
当我们在h5 页面上点击按钮,调用flutter 方法的时候,就会触发 flutter webview 的 navigationDelegate 方法回调.
我们在 navigationDelegate 回调里进行数据的处理,以及回调的添加:
navigationDelegate: (NavigationRequest request) {
/// decode
String url = Uri.decodeComponent(request.url);
debugPrint('navigationDelegate decode $url');
if (url.startsWith(yyReturunData)) {
bridgeHelper.handlerReturnData(url);
return NavigationDecision.prevent;
} else if (url.startsWith(yyOverrideSchema)) {
bridgeHelper.flushMessageQueue();
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
首先需要对拦截到的进行 urlDecode 处理。
根据我们在第一步注入的javascript 可知,当我们点击h5 按钮,调用的js 方法是:
kWebviewJsBridge
//sendMessage add message, 触发native处理 sendMessage
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message.callbackId = callbackId;
}
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
所以 url startWith 【 yy:// 】,在navigationDelegate 回调里会走 flushMessageQueue 方法,
JsBridgeHelper
void flushMessageQueue() async { String functionName = _parseFunctionName(jsFetchQueueFromFlutter); debugPrint("flushMessageQueue: functionName = $functionName"); responseCallbacks[functionName] = (data) { debugPrint('flushMessageQueue:data = $data'); if (null == data) return; List<JsMessage>? msgList = JsMessage.toArrayList(data); if (null == msgList || msgList.isEmpty) return; for (var msg in msgList) { String? responseId = msg.responseId; if (null != responseId && responseId.isNotEmpty) { CallBackFunction? function = responseCallbacks[responseId]; String? responseData = msg.responseData; function?.call(responseData); responseCallbacks.remove(responseId); } else { CallBackFunction? responseFunction; String? callbackId = msg.callbackId; if (null != callbackId && callbackId.isNotEmpty) { responseFunction = (newData) { JsMessage m = JsMessage() ..responseData = newData ..responseId = callbackId; _queueMessage(m); }; } BridgeHandler? handler; String? handlerName = msg.handlerName; String? data = msg.data; if (null != handlerName && handlerName.isNotEmpty) { handler = messageHandlers[handlerName]; } debugPrint("flushMessageQueue: , handlerName = $handlerName"); handler?.call(data, responseFunction); } } }; debugPrint('flushMessageQueue: runJavascript: $jsFetchQueueFromFlutter'); await _controller?.runJavascript(jsFetchQueueFromFlutter); }
在上一步中,方法调用最后,通过 webview control 执行了 我们开始注入的javascript , js 调用方法为:
kWebviewJsBridge
// 提供给native调用,该函数作用:获取sendMessageQueue返回给native,由于android不能直接获取返回的内容,所以使用url shouldOverrideUrlLoading 的方式返回内容
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
//add by hq
if (isIphone()) {
return messageQueueString;
//android can't read directly the return data, so we can reload iframe src to communicate with java
} else if (isAndroid()) {
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}
}
在方法的最后,js 执行了刷新,所以又回到 navigationDelegate 回调,这次的 url startWith 【yy://return/】,所以就到了
JsBridgeHelper
void handlerReturnData(String url) {
String functionName = _getFunctionFromReturnUrl(url);
debugPrint("handlerReturnData: functionName = $functionName");
CallBackFunction? f = responseCallbacks[functionName];
String? data = _getDataFromReturnUrl(url);
if (null != f) {
f.call(data);
responseCallbacks.remove(functionName);
}
}
在这里把回调函数执行,f.call();
其实在一开始,我们就先注册了 N 个与h5 约定好的 js 方法,放到了 定义的map 中:
JsBridgeHelper
Map<String, CallBackFunction> responseCallbacks = {};
Map<String, BridgeHandler> messageHandlers = {};
在流程2 中我们解析拿到的h5 数据,然后进行的处理。从flutter 中拿到的参数,最终通过f.call() 的方式回调给了h5.
如何回调的呢,当然是执行javascript 了。
JsBridgeHelper
void _queueMessage(JsMessage m) {
try {
String messageJson = jsonEncode(m.toJson());
String js = sprintf(jsHandleMessageFromFlutter, [messageJson]);
_controller?.runJavascript(js);
debugPrint("queueMessage: js = $js");
} catch (e) {
debugPrint('queueMessage: $e');
}
}
然后就会执行到开始我们注入的 javascript 里面的方法:
kWebviewJsBridge
//提供给native调用,receiveMessageQueue 在会在页面加载完后赋值为null,所以
function _handleMessageFromNative(messageJSON) {
console.log(messageJSON);
if (receiveMessageQueue) {
receiveMessageQueue.push(messageJSON);
} else {
_dispatchMessageFromNative(messageJSON);
}
}
至此到这里就执行完一整个流程了。
这篇主要是介绍了如何在官方提供的webview plugin 中没有js 回调的情况下,自己如何添加的过程。
当然也参考了原生的 jsbridge 库。
kWebviewJsBridge 完整版。(其实这个就是 原生 jsbridge 里面就有。demo.html 也有。)
const String kWebviewJsBridge=''' //notation: js file can only use this kind of comments //since comments will cause error when use in webview.loadurl, //comments will be remove by java use regexp (function() { if (window.WebViewJavascriptBridge) { return; } var messagingIframe; var sendMessageQueue = []; var receiveMessageQueue = []; var messageHandlers = {}; var CUSTOM_PROTOCOL_SCHEME = 'yy'; var QUEUE_HAS_MESSAGE = '__QUEUE_MESSAGE__/'; var responseCallbacks = {}; var uniqueId = 1; var base64encodechars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; function base64encode(str) { if (str === undefined) { return str; } var out, i, len; var c1, c2, c3; len = str.length; i = 0; out = ""; while (i < len) { c1 = str.charCodeAt(i++) & 0xff; if (i == len) { out += base64encodechars.charAt(c1 >> 2); out += base64encodechars.charAt((c1 & 0x3) << 4); out += "=="; break; } c2 = str.charCodeAt(i++); if (i == len) { out += base64encodechars.charAt(c1 >> 2); out += base64encodechars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xf0) >> 4)); out += base64encodechars.charAt((c2 & 0xf) << 2); out += "="; break; } c3 = str.charCodeAt(i++); out += base64encodechars.charAt(c1 >> 2); out += base64encodechars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xf0) >> 4)); out += base64encodechars.charAt(((c2 & 0xf) << 2) | ((c3 & 0xc0) >> 6)); out += base64encodechars.charAt(c3 & 0x3f); } return out; } function _createQueueReadyIframe(doc) { messagingIframe = doc.createElement('iframe'); messagingIframe.style.display = 'none'; doc.documentElement.appendChild(messagingIframe); } function isAndroid() { var ua = navigator.userAgent.toLowerCase(); var isA = ua.indexOf("android") > -1; if (isA) { return true; } return false; } function isIphone() { var ua = navigator.userAgent.toLowerCase(); var isIph = ua.indexOf("iphone") > -1; if (isIph) { return true; } return false; } //set default messageHandler function init(messageHandler) { if (WebViewJavascriptBridge._messageHandler) { throw new Error('WebViewJavascriptBridge.init called twice'); } WebViewJavascriptBridge._messageHandler = messageHandler; var receivedMessages = receiveMessageQueue; receiveMessageQueue = null; for (var i = 0; i < receivedMessages.length; i++) { _dispatchMessageFromNative(receivedMessages[i]); } } function send(data, responseCallback) { _doSend({ data: data }, responseCallback); } function registerHandler(handlerName, handler) { messageHandlers[handlerName] = handler; } function callHandler(handlerName, data, responseCallback) { _doSend({ handlerName: handlerName, data: data }, responseCallback); } //sendMessage add message, 触发native处理 sendMessage function _doSend(message, responseCallback) { if (responseCallback) { var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime(); responseCallbacks[callbackId] = responseCallback; message.callbackId = callbackId; } sendMessageQueue.push(message); messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; } // 提供给native调用,该函数作用:获取sendMessageQueue返回给native,由于android不能直接获取返回的内容,所以使用url shouldOverrideUrlLoading 的方式返回内容 function _fetchQueue() { var messageQueueString = JSON.stringify(sendMessageQueue); sendMessageQueue = []; //add by hq if (isIphone()) { return messageQueueString; //android can't read directly the return data, so we can reload iframe src to communicate with java } else if (isAndroid()) { messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString); } } //提供给native使用, function _dispatchMessageFromNative(messageJSON) { setTimeout(function() { var message = JSON.parse(messageJSON); var responseCallback; //java call finished, now need to call js callback function if (message.responseId) { responseCallback = responseCallbacks[message.responseId]; if (!responseCallback) { return; } responseCallback(message.responseData); delete responseCallbacks[message.responseId]; } else { //直接发送 if (message.callbackId) { var callbackResponseId = message.callbackId; responseCallback = function(responseData) { _doSend({ responseId: callbackResponseId, responseData: responseData }); }; } var handler = WebViewJavascriptBridge._messageHandler; if (message.handlerName) { handler = messageHandlers[message.handlerName]; } //查找指定handler try { handler(message.data, responseCallback); } catch (exception) { if (typeof console != 'undefined') { console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception); } } } }); } //提供给native调用,receiveMessageQueue 在会在页面加载完后赋值为null,所以 function _handleMessageFromNative(messageJSON) { console.log(messageJSON); if (receiveMessageQueue) { receiveMessageQueue.push(messageJSON); } else { _dispatchMessageFromNative(messageJSON); } } var WebViewJavascriptBridge = window.WebViewJavascriptBridge = { init: init, send: send, registerHandler: registerHandler, callHandler: callHandler, _fetchQueue: _fetchQueue, _handleMessageFromNative: _handleMessageFromNative }; var doc = document; _createQueueReadyIframe(doc); var readyEvent = doc.createEvent('Events'); readyEvent.initEvent('WebViewJavascriptBridgeReady'); readyEvent.bridge = WebViewJavascriptBridge; doc.dispatchEvent(readyEvent); })();''';
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。