发布时间:2021年4月20日-8分钟阅读
Flutter InAppWebView JavaScript双向通信。
在这个深入的教程中,我将解释如何使用我的flutter_inappwebview插件(在写这篇文章的时候,最新的版本是5.3.2)从Dart(Flutter WebView)到JavaScript进行通信,反之亦然。
有3种主要方式来实现这种2-way通信。
- JavaScript处理程序。
- Web消息通道。
- Web消息监听器。
JavaScript处理程序
JavaScript处理程序的概念类似于原生的Android WebView JavaScript接口和iOS WKWebView JavaScript消息处理程序的,但它提供了一种跨平台的方式。
Android和iOS一起走向跨平台的方式。
对于每一个JavaScript Handler,你可以定义它的名字和一个回调,当JavaScript端调用它的时候就会被调用。同时,回调可以以Promise类型向JavaScript端返回数据。
要添加一个JavaScript处理程序,可以使用InAppWebViewController.addJavaScriptHandler方法。如果你需要在网页加载时就管理JavaScript处理程序,这个方法应该在创建InAppWebView时调用,就像这样。
onWebViewCreated: (controller) {
// register a JavaScript handler with name "myHandlerName"
controller.addJavaScriptHandler(handlerName: 'myHandlerName', callback: (args) {
// print arguments coming from the JavaScript side!
print(args);
// return data to the JavaScript side!
return {
'bar': 'bar_value', 'baz': 'baz_value'
};
});
},
回调的返回类型将使用dart:convert库的jsonEncode函数自动进行JSON编码。所以,你可以把所有可编码的JSON类型发回给JavaScript。
而在JavaScript端,要执行回调处理程序并向Flutter发送数据,你需要使用window.flutter_inappwebview.callHandler(handlerName, ...args)方法,其中handlerName是一个字符串,代表你要调用的处理程序名称,args是你可以向Flutter端发送的可选参数。
需要注意的是,如果你想要一个不同的名字,而不是window.flutter_inappwebview,你可以简单地把它包在另一个JavaScript函数或对象里面。例如:window.myCustomObj = { callHandler: window.flutter_inappwebview.callHandler }; 然后,你可以用同样的方式使用window.myCustomObj.callHandler。
另外,你也可以用这种方式来包装整个特定的处理程序。
const myHandlerName = function(...args) {
return window.flutter_inappwebview.callHandler('myHandlerName', ...args);
};
// and then use it
myHandlerName();
为了在JavaScript文件或<script> HTML标签内正确调用window.flutter_inappwebview.callHandler,你需要等待并监听flutterInAppWebViewPlatformReady JavaScript事件。一旦平台准备好处理callHandler方法,这个事件就会被派发。你也可以使用一个全局标志变量,该变量在flutterInAppWebViewPlatformReady事件被派遣时设置,并在调用window.flutter_inappwebview.callHandler方法之前使用它。
下面是一个JavaScript示例代码。
// execute inside the "flutterInAppWebViewPlatformReady" event listener
window.addEventListener("flutterInAppWebViewPlatformReady", function(event) {
const args = [1, true, ['bar', 5], {foo: 'baz'}];
window.flutter_inappwebview.callHandler('myHandlerName', ...args);
});
// or using a global flag variable
var isFlutterInAppWebViewReady = false;
window.addEventListener("flutterInAppWebViewPlatformReady", function(event) {
isFlutterInAppWebViewReady = true;
});
// then, somewhere in your code
if (isFlutterInAppWebViewReady) {
const args = [1, true, ['bar', 5], {foo: 'baz'}];
window.flutter_inappwebview.callHandler('myHandlerName', ...args);
}
相反,如果你是从Dart(Flutter)端评估JavaScript代码,你可以直接在onLoadStop事件上(或之后)调用window.flutter_inappwebview.callHandler,而不需要监听flutterInAppWebViewPlatformReady JavaScript事件,因为,那时它已经被发射了。例如
onLoadStop: (controller, url) async {
await controller.evaluateJavascript(source: """
const args = [1, true, ['bar', 5], {foo: 'baz'}];
window.flutter_inappwebview.callHandler('myHandlerName', ...args);
""");
},
这里是一个通信的例子。
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
if (Platform.isAndroid) {
await AndroidInAppWebViewController.setWebContentsDebuggingEnabled(true);
}
runApp(new MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> {
InAppWebViewGroupOptions options = InAppWebViewGroupOptions(
android: AndroidInAppWebViewOptions(
useHybridComposition: true,
),);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("JavaScript Handlers")),
body: SafeArea(
child: Column(children: <Widget>[
Expanded(
child: InAppWebView(
initialData: InAppWebViewInitialData(
data: """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
</head>
<body>
<h1>JavaScript Handlers</h1>
<script>
window.addEventListener("flutterInAppWebViewPlatformReady", function(event) {
window.flutter_inappwebview.callHandler('handlerFoo')
.then(function(result) {
// print to the console the data coming
// from the Flutter side.
console.log(JSON.stringify(result));
window.flutter_inappwebview
.callHandler('handlerFooWithArgs', 1, true, ['bar', 5], {foo: 'baz'}, result);
});
});
</script>
</body>
</html>
"""
),
initialOptions: options,
onWebViewCreated: (controller) {
controller.addJavaScriptHandler(handlerName: 'handlerFoo', callback: (args) {
// return data to the JavaScript side!
return {
'bar': 'bar_value', 'baz': 'baz_value'
};
});
controller.addJavaScriptHandler(handlerName: 'handlerFooWithArgs', callback: (args) {
print(args);
// it will print: [1, true, [bar, 5], {foo: baz}, {bar: bar_value, baz: baz_value}]
});
},
onConsoleMessage: (controller, consoleMessage) {
print(consoleMessage);
// it will print: {message: {"bar":"bar_value","baz":"baz_value"}, messageLevel: 1}
},
),
),
]))),
);
}
}
与JavaScript通信的另一种方式是使用InAppWebViewController.evaluateJavascript方法来评估一些JavaScript代码。
你可以设置一个消息事件监听器(与postMessage一起使用)或一个自定义的事件监听器(详见CustomEvent),比如。
// message event listener
window.addEventListener("message", (event) => {
console.log(event.data);
}, false);
// or custom event listener
window.addEventListener("myCustomEvent", (event) => {
console.log(event.detail);
}, false);
然后,你就可以随时随地分派自定义的JavaScript事件。
// using postMessage method
window.postMessage({foo: 1, bar: false});
// or dispatching a custom event
const event = new CustomEvent("myCustomEvent", {
detail: {foo: 1, bar: false}
});
window.dispatchEvent(event);
所以,你可以在运行时使用InAppWebViewController.evaluateJavascript方法来设置这些事件监听器,或者在Web应用本身内部,使用同样的方法来调度这些事件,例如。
onLoadStop: (controller, url) async {
await controller.evaluateJavascript(source: """
window.addEventListener("myCustomEvent", (event) => {
console.log(JSON.stringify(event.detail));
}, false);
""");
await Future.delayed(Duration(seconds: 5));
controller.evaluateJavascript(source: """
const event = new CustomEvent("myCustomEvent", {
detail: {foo: 1, bar: false}
});
window.dispatchEvent(event);
""");
},
onConsoleMessage: (controller, consoleMessage) {
print(consoleMessage);
// it will print: {message: {"foo":1,"bar":false}, messageLevel: 1}
},
网络消息通道
另一种方法是使用Web消息通道,它是HTML5消息通道的代表。更多细节请参见通道消息传递API。
它允许你创建一个新的消息通道,并通过其两个WebMessagePort属性来发送数据。
- port1,这是第一个WebMessagePort。
- port2,这是第二个WebMessagePort。
这都是关于连接的。是的。
要创建一个Web消息通道,你需要使用InAppWebViewController.createWebMessageChannel方法(官方的本地Android WebView API可以在这里找到)。这个方法应该在页面加载时被调用,例如,当onLoadStop事件被触发时,否则,WebMessageChannel将无法工作。
Android的注意事项。只有当AndroidWebViewFeature.isFeatureSupported对AndroidWebViewFeature.CREATE_WEB_MESSAGE_CHANNEL返回true时,才可以调用这个方法。
下面是它的一个例子。
onLoadStop: (controller, url) async {
if (!Platform.isAndroid || await AndroidWebViewFeature.isFeatureSupported(AndroidWebViewFeature.CREATE_WEB_MESSAGE_CHANNEL)) {
var webMessageChannel = await controller.createWebMessageChannel();
var port1 = webMessageChannel!.port1;
var port2 = webMessageChannel.port2;
}
},
创建通道的Dart方使用port1,而端口另一端的JavaScript方使用port2--你向port2发送消息,并使用InAppWebViewController.postWebMessage方法将端口与要发送的消息以及要转移所有权的对象(在这里是指端口本身)一起转移到另一个浏览环境。
当这些可转移的端口对象被转移时,它们在之前的上下文中被 "阉割 "了,即它们之前属于的那个上下文。例如,当一个端口被发送到JavaScript时,就不能再用于在Dart端发送或接收消息。此外,与 HTML5 Spec 不同的是,如果发生过以下情况,则不能转移端口。
- 设置了一个消息回调。
- 一个消息被发布在上面。
我知道你在想什么。"胡说八道......来吧,让我看看代码!"。别担心,我们就快到了!
一个被转移的端口不能被应用程序关闭,因为所有权也被转移了。
为了从Dart方面监听端口上的消息,你需要使用WebMessagePort.setWebMessageCallback方法设置WebMessageCallback。然后你可以通过使用WebMessagePort.postMessage方法向原始文件发送消息来进行回应。
当你想停止在通道中发送消息时,你可以调用WebMessagePort.close来关闭端口。关闭的端口不能被转移,也不能被重新打开以发送消息。
为了能够监听来自JavaScript方面的消息,你需要首先 "捕获 "来自Dart方面的端口。 这里终于为你提供了预期的通信实例。
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
if (Platform.isAndroid) {
await AndroidInAppWebViewController.setWebContentsDebuggingEnabled(true);
}
runApp(new MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> {
InAppWebViewGroupOptions options = InAppWebViewGroupOptions(
android: AndroidInAppWebViewOptions(
useHybridComposition: true,
),);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("Web Message Channels")),
body: SafeArea(
child: Column(children: <Widget>[
Expanded(
child: InAppWebView(
initialData: InAppWebViewInitialData(
data: """
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebMessageChannel Test</title>
</head>
<body>
<!-- when you click this button, it will send a message to the Dart side -->
<button id="button" onclick="port.postMessage(input.value);" />Send</button>
<br />
<input id="input" type="text" value="JavaScript To Native" />
<script>
// variable that will represents the port used to communicate with the Dart side
var port;
// listen for messages
window.addEventListener('message', function(event) {
if (event.data == 'capturePort') {
// capture port2 coming from the Dart side
if (event.ports[0] != null) {
// the port is ready for communication,
// so you can use port.postMessage(message); wherever you want
port = event.ports[0];
// To listen to messages coming from the Dart side, set the onmessage event listener
port.onmessage = function (event) {
// event.data contains the message data coming from the Dart side
console.log(event.data);
};
}
}
}, false);
</script>
</body>
</html>
"""),
initialOptions: options,
onConsoleMessage: (controller, consoleMessage) {
print("Message coming from the Dart side: ${consoleMessage.message}");
},
onLoadStop: (controller, url) async {
if (!Platform.isAndroid || await AndroidWebViewFeature.isFeatureSupported(AndroidWebViewFeature.CREATE_WEB_MESSAGE_CHANNEL)) {
// wait until the page is loaded, and then create the Web Message Channel
var webMessageChannel = await controller.createWebMessageChannel();
var port1 = webMessageChannel!.port1;
var port2 = webMessageChannel.port2;
// set the web message callback for the port1
await port1.setWebMessageCallback((message) async {
print("Message coming from the JavaScript side: $message");
// when it receives a message from the JavaScript side, respond back with another message.
await port1.postMessage(WebMessage(data: message! + " and back"));
});
// transfer port2 to the webpage to initialize the communication
await controller.postWebMessage(message: WebMessage(data: "capturePort", ports: [port2]), targetOrigin: Uri.parse("*"));
}
},
),
),
])
)
),
);
}
}
网络消息监听器
网络消息监听器类似于JavaScript处理程序和网络消息通道。它允许在WebMessageListener将监听的每一帧中注入一个JavaScript对象。
一个Web消息监听器的例子。
要添加一个Web Message Listener,你需要使用InAppWebViewController.addWebMessageListener方法。这个方法应该在使用它的网页被加载之前被调用,例如,当onWebViewCreated事件被触发时。
Android的注意事项。与Web消息通道类似,只有当AndroidWebViewFeature.isFeatureSupported返回true时,AndroidWebViewFeature.WEB_MESSAGE_LISTENER才应该调用这个方法。
child: InAppWebView(
onWebViewCreated: (controller) async {
// add first all of your web message listeners
if (!Platform.isAndroid || await AndroidWebViewFeature.isFeatureSupported(AndroidWebViewFeature.WEB_MESSAGE_LISTENER)) {
await controller.addWebMessageListener(WebMessageListener(
jsObjectName: "myObject",
allowedOriginRules: Set.from(["https://*.example.com"]),
onPostMessage: (message, sourceOrigin, isMainFrame, replyProxy) {
replyProxy.postMessage("Got it!");
},
));
}
// then load your URL
await controller.loadUrl(urlRequest: URLRequest(url: Uri.parse("https://www.example.com")));
},
),
注入的JavaScript对象将在全局范围内被命名为WebMessageListener.jsObjectName。这将在此调用后的每次导航中,在原点符合WebMessageListener.allowedOriginRules的任何框架中注入JavaScript对象,当页面开始加载时,JavaScript对象将立即可用。
每个WebMessageListener.allowedOriginRules条目必须遵循格式SCHEME "://" [ HOSTNAME_PATTERN [ ":" PORT ]],每个部分在本表中解释。
为了安全起见,强烈建议使用HTTPS方案,因为当框架的原点与允许的原点中的任何一个匹配时,JavaScript对象将被注入。允许HTTP来源会使注入的对象暴露给任何潜在的基于网络的攻击者。 如果提供一个通配符 "*",它将注入JavaScript对象到所有的框架。只有当应用程序希望任何第三方网页能够使用注入的对象时,才应该使用通配符。当使用通配符时,应用程序必须将收到的信息视为不可信的,并仔细验证任何数据。
你可以多次调用这个方法来注入多个JavaScript对象!
每个注入的JavaScript对象将有以下方法/属性。
- postMessage(message[, MessagePorts]) 在Android上,postMessage(message) 在iOS上,message是一个字符串。
- onmessage。要接收从Flutter App端发布的消息,请为这个属性指定一个函数。这个函数应该接受一个单一的 "事件 "参数(onmessage = function(event) { ... }). "event "有一个 "data "属性,它是来自Flutter App端的消息字符串。
- addEventListener(type, listener)。为了与DOM EventTarget的addEventListener兼容,它接受type和listener参数,其中type只能是 "消息 "类型,listener只能是一个JavaScript函数,具有onmessage属性函数的相同功能。
- removeEventListener(type, listener)。为了与DOM EventTarget的removeEventListener兼容,它接受type和listener参数。它被用来删除使用addEventListener方法添加的事件监听器。
通信必须首先在JavaScript端初始化,发布消息,所以Flutter App将有一个JavaScriptReplyProxy对象来响应。
JavaScript端。
// Web page (in JavaScript)
myObject.onmessage = function(event) {
// prints "Got it!" when we receive the app's response.
console.log(event.data);
}
myObject.postMessage("I'm ready!");
Flutter方面。
// Flutter App
child: InAppWebView(
onWebViewCreated: (controller) async {
if (!Platform.isAndroid || await AndroidWebViewFeature.isFeatureSupported(AndroidWebViewFeature.WEB_MESSAGE_LISTENER)) {
await controller.addWebMessageListener(WebMessageListener(
jsObjectName: "myObject",
onPostMessage: (message, sourceOrigin, isMainFrame, replyProxy) {
// do something about message, sourceOrigin and isMainFrame.
replyProxy.postMessage("Got it!");
},
));
}
await controller.loadUrl(urlRequest: URLRequest(url: Uri.parse("https://www.example.com")));
},
),
总结
现在,在本教程结束后,你可以开始尝试不同的双向通信方法,并选择最适合你和适合你需求的方法。 今天的内容就到此为止!
一如既往,感谢所有支持这个项目的人 💙
通过www.DeepL.com/Translator(免费版)翻译