在进行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);
})();''';
参考资料:
Android JsBridge 原理解析
jsbridge