这是我参与更文挑战的第9天,活动详情查看: 更文挑战
所以要一锅端了?
很顺利的解决了iOS端,Swift与H5的交互问题,公司觉得你这光解决iOS的不行啊,最好是写一个通用的WebView壳子,能满足日常的iOS与Android双端WebView加载H5调试。
这不是要一锅端的节奏?
既然是双端调试,那么必然会想到使用跨平台方案来写这个壳子,基于技术背景,Flutter当仁不让的成为了第一选择,而选择使用的Flutter中的WebView插件就是webview_flutter!
别说,我为了这个突然的需求,也反反复复看了webview_flutter的官方文档和代码,自己写了Demo来验证。
简单介绍一下webview_flutter插件
A Flutter plugin that provides a WebView widget.
On iOS the WebView widget is backed by a WKWebView; On Android the WebView widget is backed by a WebView.
一个为Flutter提供WebView组件的插件。
在iOS端是基于原生的WKWebView,而在Android则是基于系统的WebView。
从这段简介可以看出,虽然这是一个Flutter插件,但是实际上桥接的都是基于iOS与Android平台系统层面的WebView组件,这样做的好处就是在每一端都做到使用原生,只要中间通信层逻辑一致并完好,那么就可以展平了双端WebView中H5与Flutter通信的能力。
Flutter与JS互调使用与讲解
在这里我们先精简一下官方给出的example例子。
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class WebViewExample extends StatefulWidget {
@override
_WebViewExampleState createState() => _WebViewExampleState();
}
class _WebViewExampleState extends State<WebViewExample> {
final Completer<WebViewController> _controller =
Completer<WebViewController>();
@override
void initState() {
super.initState();
if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter WebView example'),
),
// We're using a Builder here so we have a context that is below the Scaffold
// to allow calling Scaffold.of(context) so we can show a snackbar.
body: Builder(builder: (BuildContext context) {
return WebView(
initialUrl: 'https://www.baidu.com',
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (WebViewController webViewController) {
_controller.complete(webViewController);
},
onProgress: (int progress) {
print("WebView is loading (progress : $progress%)");
},
javascriptChannels: <JavascriptChannel>{
_toasterJavascriptChannel(context),
},
navigationDelegate: (NavigationRequest request) {
if (request.url.startsWith('https://www.youtube.com/')) {
print('blocking navigation to $request}');
return NavigationDecision.prevent;
}
print('allowing navigation to $request');
return NavigationDecision.navigate;
},
onPageStarted: (String url) {
print('Page started loading: $url');
},
onPageFinished: (String url) async {
final webViewController = await _controller.future;
final callback = await webViewController.evaluateJavascript("alert('Hello world');");
print(callback);
},
gestureNavigationEnabled: true,
);
}),
);
}
JavascriptChannel _toasterJavascriptChannel(BuildContext context) {
return JavascriptChannel(
name: 'SeasonCallback',
onMessageReceived: (JavascriptMessage message) {
// ignore: deprecated_member_use
Scaffold.of(context).showSnackBar(
SnackBar(content: Text(message.message)),
);
});
}
}
复制代码
这段代码最为核心的部分就是以下这段,请详细看看我写的注释,详细又重要!!!
如果在网页里面看不懂的,可以考虑下载webview_flutter的代码,一点点对照的看,并与前两篇我文章。
return WebView(
/// 需要加载的网页URL
initialUrl: 'https://www.baidu.com',
/// JS的模式
javascriptMode: JavascriptMode.unrestricted,
/// WebView生成成功时,会回调一个webViewController,它非常的重要,后面会继续说
onWebViewCreated: (WebViewController webViewController) {
/// 接受该控制器
_controller.complete(webViewController);
},
/// webView加载的回调进度,相当于Swift中对WKWebView中的estimatedProgress的KVO
onProgress: (int progress) {
print("WebView is loading (progress : $progress%)");
},
/// JS通道,这里相当于Flutter侧监听JS方法的集合,和Swift中通过userContentController注册监听JS句柄一致
javascriptChannels: <JavascriptChannel>{
/// 这个函数后面单独说明
_toasterJavascriptChannel(context),
},
/// 这里相当于Swift中WKNavigationDelegate代理方法中的
/// func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void)方法,
/// 就是用于拦截跳转的,和拦截微信和支付宝跳转类似
navigationDelegate: (NavigationRequest request) {
/// 这里做了跳转拦截,如果跳转的地址是油管,那么将阻止加载
if (request.url.startsWith('https://www.youtube.com/')) {
print('blocking navigation to $request}');
return NavigationDecision.prevent;
}
print('allowing navigation to $request');
/// 其他的跳转允许
return NavigationDecision.navigate;
},
/// 监听网页开始加载,相当于Swift中WKNavigationDelegate代理方法中的
/// func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!)方法
onPageStarted: (String url) {
print('Page started loading: $url');
},
/// 监听网页加载完毕,相当于Swift中WKNavigationDelegate代理方法中的
/// func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)方法
onPageFinished: (String url) async {
/// 通过Completer拿webView控制器
final webViewController = await _controller.future;
/// 通过webViewController运行JS方法,这里是调用一个JS的弹窗,并且异步获取JS的回调,这个回调可能为空
final callback = await webViewController.evaluateJavascript("alert('Hello world');");
print(callback);
},
/// 是否支持手势侧滑
gestureNavigationEnabled: true,
);
复制代码
可以看到,如果理解了Swift中的WKWebView的WKNavigationDelegate代理方法含义,Flutter中的WebView无非是换了些名称,将delegate风格换成了callback风格而已的初始化函数。
而_toasterJavascriptChannel
就是具体化的如果注册监听JS方法句柄:
JavascriptChannel _toasterJavascriptChannel(BuildContext context) {
return JavascriptChannel(
/// 注册一个监听句柄为SeasonCallback
/// 和Swift中func add(_ scriptMessageHandler: WKScriptMessageHandler, name: String)方法一致
name: 'SeasonCallback',
/// 监听的SeasonCallback回调,获取JS传到Flutter侧的参数,和Swift中的WKScriptMessageHandler代理回调一致,
/// func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)
/// 然后JS就可以调用Flutter侧的函数了
onMessageReceived: (JavascriptMessage message) {
/// Flutter接受到JS的message,并调起Flutter函数
Scaffold.of(context).showSnackBar(
SnackBar(content: Text(message.message)),
);
});
}
复制代码
回忆一下上一篇文章中的3个需求,如果在Flutter需要实现,应该怎么写呢?理解了上一篇文章并看懂了这篇文章的注释,应该不在话下。
总结
-
Flutter中的webview_flutter本质上还是调用了双端的原生WebView组件,所以对应的一致性较好,如果了解原生与JS的互调,那么Flutter与JS互调依葫芦画瓢即可。
-
Swift中的多个代理回调在Flutter中都通过Callback完成,这样看起来紧凑,虽然可能在编码中不易于看清楚,但是比较符合Flutter的声明式编程风格,需要努力适应。
webview_flutter的使用注意事项与坑点:
注意事项
iOS端需要在info.plist文件中添加配置与权限:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>io.flutter.embedded_views_preview</key>
<true/>
复制代码
Android端:
- 1.在路径android/app/build.gradle添加最小SDK支持:
android {
defaultConfig {
minSdkVersion 19
}
}
复制代码
- 2.安卓端开启混合视图
import 'dart:io';
import 'package:webview_flutter/webview_flutter.dart';
class WebViewExample extends StatefulWidget {
@override
WebViewExampleState createState() => WebViewExampleState();
}
class WebViewExampleState extends State<WebViewExample> {
@override
void initState() {
super.initState();
/// 就是下面这段代码
// Enable hybrid composition.
if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
}
@override
Widget build(BuildContext context) {
return WebView(
initialUrl: 'https://flutter.cn',
);
}
}
复制代码
- 3.开启http选项和隐私权限,注意是在
项目/android/app/src/main/AndroidManifest.xml
文件中进行添加
<!-- 需要在application中添加android:usesCleartextTraffic="true",即是否可以使用明文传输,非SSL -->
<application
android:label="web_view"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
<!-- 权限根据项目需要配置-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- 相机的权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 麦克风的权限 -->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<!-- 写sd卡的权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- 读sd卡权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
复制代码
坑点
如果你使用webview_flutter在Android端加载H5,而H5中正好需要调用系统相机或者是相册的话,这个JS是无法得到相应的,而iOS端却可以。
android webview 里点击<input type=’file’>
没有反应。
出现这个问题的原因是,官方的webview_flutter插件没有针对这一功能做适配。
详细可以看flutter webview android h5 上传文件失败解决办法。
如果需要Android端有这个能力,需要在添加权限的同时,改写插件在Android侧的实现。
当然有另外一个插件flutter_webview_plugin可以实现Android上传这个功能,但是这个flutter_webview_plugin不管从易用性还是功能性,都不及webview_flutter。
我好希望有个一个大佬给教我改写webview_flutter插件,支持Android端的上传功能,我按照网上的方法改写了,然后崩溃了。。。
明天继续
明天会讲解Swift中,准确是OC中WebViewJavaScriptBridge的使用与注意实现,大家加油!