我正在参加「掘金·启航计划」
前言
Flutter与H5的结合
Flutter
是Google开发的一个跨平台的UI开发框架,同时也可以使用Dart编写本地代码,可以兼容各种移动端、Web和桌面端平台。H5是一种基于HTML
、CSS
、Javascript
的Web技术,广泛用于构建Web应用程序和网页。
在实际开发中,经常需要将Flutter
与H5
进行结合,以展现更加丰富的应用场景。其中,Flutter
中内嵌H5
页面并且实现与之通信,可以使应用更加交互性,体验性更好,同时也能极大的扩展应用的功能。
为什么要使用Flutter内嵌H5页面?
Flutter内嵌H5页面有以下几个优势:
- 可以快速实现Web应用中的某些功能,而无需将整个应用都重写为
Flutter
; - 可以出色地展示
H5
页面,以便将Web内容移植到App中; - 可以通过
H5
页面与Flutter
进行交互,获得更加流畅、快捷的用户体验。
综上所述,内嵌H5
页面是非常有必要的。
实现方式
Flutter中的WebView组件介绍
在Flutter
中,可以使用flutter_webview_plugin
库实现内嵌H5
页面的功能。这个库是Flutter
的一个用于Web
视图插件的第三方库,依赖于Android
和iOS
的原生Web View
。
安装该库:
dependencies:
flutter_webview_plugin: ^0.4.0+1
这个库提供了两种方式加载Web视图:
WebviewScaffold
:显示一个工具栏,并在屏幕上方显示一个带有Web视图的Scaffold。Webview
:直接在Widget树中包含一个Web视图。
下面是一个简单的Webview
使用示例:
import 'package:flutter/material.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
class MyWebView extends StatefulWidget {
final String title;
final String url;
MyWebView({
@required this.title,
@required this.url,
});
@override
_MyWebViewState createState() => _MyWebViewState();
}
class _MyWebViewState extends State<MyWebView> {
final flutterWebViewPlugin = FlutterWebviewPlugin();
@override
void initState() {
super.initState();
}
@override
void dispose() {
flutterWebViewPlugin.dispose(); // 组件销毁时需要释放
super.dispose();
}
@override
Widget build(BuildContext context) {
return WebviewScaffold(
appBar: AppBar(
title: Text(widget.title),
),
url: widget.url,
initialChild: Center(
child: CircularProgressIndicator(),
),
);
}
}
这里就生成了一个简单的带Appbar
的Webview
视图。
WebView与H5页面通信方式的比较
在Flutter中,有两种方式实现Flutter与H5的通信:
JavascriptChannel
:通过Javascript
通信。MethodChannel
:通过建立Binder
通道和Android
原生代码通信。
JavascriptChannel
在需求短时较为直观、实现相对简单,MethodChannel
在处理较复杂的逻辑时,更加清晰明了。
Flutter与H5交互的核心技术WebViewJavascriptChannel
Flutter
中,内嵌H5
页面并且实现通信最为常见的方式就是使用WebViewJavascriptChannel
了。
关于WebViewJavascriptChannel
,以下是相关的介绍和使用示例。
WebViewJavascriptChannel
接口定义:
abstract class WebViewJavascriptChannel {
void postMessage(String message);
}
在Flutter
中,需要创建一个WebviewJavascriptChannel
对象,并将其提供给WebView
组件的构造器,其将在H5
中调用。
下面是一个简单的使用WebViewJavascriptChannel
的示例:
import 'package:flutter/material.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
class MyWebView extends StatefulWidget {
final String title;
final String url;
MyWebView({
@required this.title,
@required this.url,
});
@override
_MyWebViewState createState() => _MyWebViewState();
}
class _MyWebViewState extends State<MyWebView> {
final flutterWebViewPlugin = FlutterWebviewPlugin();
TextEditingController controller = TextEditingController();
@override
void initState() {
super.initState();
flutterWebViewPlugin.onStateChanged.listen((WebViewStateChanged state) {
print("onStateChanged: ${state.type} ${state.url}");
});
flutterWebViewPlugin.onUrlChanged.listen((String url) {
print("onUrlChanged: $url");
setState(() {
controller.text = url;
});
});
flutterWebViewPlugin.onDestroy.listen((_) {
print("onDestroy");
});
}
@override
void dispose() {
flutterWebViewPlugin.dispose(); // 组件销毁时需要释放
super.dispose();
}
void onPageFinished(BuildContext context) {
final channel = MethodChannel('webViewJSChannel');
channel.setMethodCallHandler((MethodCall call) async {
if (call.method == 'webToFlu') {
String para = call.arguments;
//处理接收的参数
print('MethodChannel接收到的内容:' + para);
//回传数据到H5
flutterWebViewPlugin.evalJavascript(
"jsFunction('${para}', '${DateTime.now().toString()}')");
}
});
channel.invokeMethod('fluToWeb', {'msg': 'Flutter start!'});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: [
TextField(
controller: controller,
readOnly: true,
decoration: InputDecoration(
hintText: 'current url',
contentPadding: EdgeInsets.all(10.0),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4.0),
),
),
),
Expanded(
child: WebviewScaffold(
url: widget.url,
hidden: true,
initialChild: Container(
color: Colors.white,
child: Center(
child: CircularProgressIndicator(),
),
),
withJavascript: true,
hiddenPage: true,
appBar: AppBar(
title: Text(widget.title),
),
javascriptChannels: <JavascriptChannel>[
JavascriptChannel(
name: 'FlutterEvent',
onMessageReceived: (JavascriptMessage msg) {
print(msg.message);
})
].toSet(),
onPageFinished: onPageFinished,
withZoom: false,
),
),
],
),
);
}
}
上面的示例中,在页面加载完成之后,我们首先通过回调方法onPageStarted
在Flutter
中创建了一个MethodChannel
,声明了Flutter
向H5
推送方法的名称和需要传递的数据。随后在H5
中,接收到Flutter
传递的初始化参数后,就可以通过window.Flutter.postMessage()
来向Flutter
发送数据。当Flutter
接收到H5
的消息之后,就可以通过MethodChannel
写入到Flutter
侧的方法,进行处理。
下面是一个Web
页面示例:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>test channel</title>
</head>
<body>
<button id='btn'>Click me to send message to Flutter</button>
<p>Tips: 点击按钮即可通过JS打印Hello World并通过MethodChannel发送到Flutter。</p>
<script>
var channel = null;
window.addEventListener("load", function() {
channel = new WebViewJavascriptChannel(bridge);
});
function bridge(msg) {
document.getElementById('log').innerHTML = msg;
var para = "Hello World";
channel.postMessage(JSON.stringify({'type':'channelTest', 'content': para}));
}
function jsFunction(para1, para2) {
console.log('接收到的内容:' + para1);
console.log('当前时间:' + para2);
}
document.getElementById('btn').addEventListener('click', function(e) {
console.log('clicked!');
console.log(channel);
bridge('msg from h5');
}, false);
</script>
</body>
</html>
这个页面中,我们声明了通信的JavaSript Channel
,并通过按钮点击事件完成数据的发送,同时在jsFunction()
方法中,通过flutterWebViewPlugin.evalJavascript()
方法向H5页面回传数据。
实际应用
在Flutter中实现微信、支付宝等第三方授权登陆
在开发中,经常需要使用到微信、支付宝等第三方授权登陆的功能,我们可以在Flutter
中内嵌H5
页面,通过H5
页面来完成授权登陆操作,然后回传数据到Flutter
中进行后续的操作。
以下是一份简单的微信授权登陆的示例代码:
import 'package:flutter/material.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
class WechatAuthorizationPage extends StatefulWidget {
@override
_WechatAuthorizationPageState createState() =>
_WechatAuthorizationPageState();
}
class _WechatAuthorizationPageState extends State<WechatAuthorizationPage> {
final flutterWebViewPlugin = FlutterWebviewPlugin();
String _url =
"https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxe15b267e25e3f502&redirect_uri=https%3A%2F%2Fs.jianyi-ai.com%2Fapi%2Fselfdiagnose%2Fv1%2Fwechat%2Fcallback&response_type=code&scope=snsapi_userinfo&state=state&connect_redirect=1#wechat_redirect";
@override
void initState() {
super.initState();
flutterWebViewPlugin.onStateChanged.listen((WebViewStateChanged state) {
print("onStateChanged: ${state.type} ${state.url}");
});
flutterWebViewPlugin.onUrlChanged.listen((String url) {
print("onUrlChanged: $url");
});
flutterWebViewPlugin.onDestroy.listen((_) {
print("onDestroy");
});
}
@override
void dispose() {
flutterWebViewPlugin.dispose(); // 组件销毁时需要释放
super.dispose();
}
void onPageFinished(BuildContext context) {
final channel = MethodChannel('webViewJSChannel');
channel.setMethodCallHandler((MethodCall call) async {
if (call.method == 'webToFlu') {
Map para = json.decode(call.arguments);
print("onPageFinished-para=$para");
if (para.containsKey('code')) {
// do something with the code
} else {
// do something with the error
}
}
});
flutterWebViewPlugin.evalJavascript(
'setTimeout(function () { Flutter.postMessage(JSON.stringify({type:"pageLoaded", content:{}})); }, 1000)');
channel.invokeMethod('fluToWeb', {'msg': 'Flutter start!'});
}
void onStateChanged(BuildContext context, WebViewStateChanged state) {
print(
'[INFO]onStateChanged: ${state.type} ${state.url} ${state.canGoBack} ${state.canGoForward}');
if (state.type == WebViewState.finishLoad) {
flutterWebViewPlugin.evalJavascript(
'setTimeout(function () { Flutter.postMessage(JSON.stringify({type:"pageLoaded", content:{}})); }, 1000)');
}
if (state.type == WebViewState.abortLoad) {
// _progressBarVisibility = false;
}
if (state.type == WebViewState.startLoad) {
// _progressBarVisibility = true;
}
}
@override
Widget build(BuildContext context) {
return Container(
child: WebviewScaffold(
url: _url,
hidden: true,
initialChild: Container(
color: Colors.white,
child: Center(
child: CircularProgressIndicator(),
),
),
withJavascript: true,
hiddenPage: true,
appBar: AppBar(
title: Text('微信授权登陆'),
),
javascriptChannels: <JavascriptChannel>[
JavascriptChannel(
name: 'FlutterEvent',
onMessageReceived: (JavascriptMessage msg) {
print(msg.message);
})
].toSet(),
onPageFinished: onPageFinished,
withZoom: false,
//allowFileAccess: true,
userAgent:
"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/37.0.0.0 Mobile Safari/537.36",
),
);
}
}
在H5中与Flutter原生端交互:与Flutter全局变量通信、调用原生组件
在H5
页面中与Flutter
原生端进行交互也是非常重要的。在H5
中可以通过window
调用Flutter
的API
,以便获取Flutter
全局的变量或者调用原生组件进行处理。
在这里我们介绍两个示例:
1.与Flutter全局变量通信
首先需要在Flutter
中注册一个通信事件,例如我们可以创建一个名为globalEvent
的事件,用于H5
传递数据到Flutter
全局变量。
import 'package:flutter/services.dart';
const MethodChannel _channel = MethodChannel('globalEvent');
void main() {
_channel.setMethodCallHandler((MethodCall call) async {
if(call.method == 'setGlobalVariable'){
// 在这里处理H5传递的数据
String data = call.arguments;
globalVariable = data;
}
});
}
// 声明一个全局变量
String globalVariable = '';
接着,在H5
中通过window
对象调用我们注册的事件globalEvent
,将数据传递过来。
// 在H5中传递数据给Flutter
window.flutter_inject_global_variable = function(data) {
window.flutter_inject_global_variable_callback(data);
};
window.flutter_inject_global_variable_callback = function(data) {
window.flutter_inject_global_variable_callback = null;
}
// H5传递数据给Flutter全局变量
window.flutter_inject_global_variable('Hello Flutter!');
当H5调用了window.flutter_inject_global_variable
方法时,Flutter
会接收到一个名为setGlobalVariable
的事件,并将H5
传递的数据存储到全局变量globalVariable
中。
- 调用原生组件
Flutter
中可以通过MethodChannel
调用原生组件,同时需要在原生端注册一个Flutter
的通信事件,例如我们创建一个名为callNativeComponent
的事件。
import 'package:flutter/services.dart';
const MethodChannel _channel = MethodChannel('callNativeComponent');
void main() {
_channel.setMethodCallHandler((MethodCall call) async {
if(call.method == 'showDialog'){
// 在这里调用原生组件
}
});
}
// 声明一个全局变量
String globalVariable = '';
接着,在H5
中通过window
对象调用callNativeComponent
事件,使Flutter
调用原生组件进行处理。
// H5中调用原生组件
window.call_native_component = function(type, data) {
var params = JSON.stringify({
"type": type,
"data": data
});
return window.flutter_call_native_component(params);
};
// 在H5中调用原生组件
window.call_native_component('show_dialog', {'content': 'Hello Native!'});
当H5
调用window.call_native_component
方法时,Flutter接收到名为showDialog
的事件并使用原生组件处理传递的数据。原生端同时需要实现MethodChannel
的相应方法,例如showDialog
方法。
总结与展望
在本文中,我们介绍了Flutter
内嵌H5
页面的实现方式和与前端通信的技术。通过使用Flutter
的flutter_webview_plugin
插件,我们能够很方便地在Flutter
应用中内嵌H5
页面,并且通过JavaScript Channel
机制实现与前端的通信。
在前端与Flutter
应用之间实现无缝的交互可以带来很多的好处。例如,在Flutter
应用中使用H5
页面可以方便地引入第三方的页面和组件,并且可以快速构建复杂的组合界面。同时,基于Flutter
强大的性能和Flutter Widget
的可组合性,可以实现更加复杂和优雅的界面效果。
不过,在实际的开发过程中,还需要注意一些问题。例如,Flutter
和H5
之间图形渲染的差异和性能问题,以及在调试方面可能遇到的困难。同时,由于Flutter
和H5
都在不断发展中,未来仍然需要持续关注技术的变化和趋势。不过随着Flutter
应用的普及和技术的不断进步,我们相信Flutter
内嵌H5
页面将在移动应用开发中发挥更加重要和广泛的作用。
总之,在实际的项目中,应根据具体情况选择使用Flutter
内嵌H5
页面的方案。在合适的场景下,该技术能够帮助我们更好地利用各类资源,构建更加高效和优秀的移动应用。