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等。代码是好久之前写的,现在文档整理的有点乱,代码中基本上都有注释。希望有对你有用的点。文章来源:https://www.toymoban.com/news/detail-625627.html
学习记录,每天不停进步。文章来源地址https://www.toymoban.com/news/detail-625627.html
到了这里,关于flutter开发实战-实现webview与Javascript通信JSBridge的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!