本次教程使用的是Flutter官方提供的WebView组件webview_flutter 2.3.1,flutter_android 2.2.1
以下为Flutter WebView官方的介绍,在Android
采用原生的WebView
实现,在IOS
上采用WKWebView
实现。可以看出Flutter目前没有自己的WebView引擎
,可能若干年后会开发出属于Flutter的引擎,所以遇到问题多看Plugin源码。
On iOS the WebView widget is backed by a WKWebView; On Android the WebView widget is backed by a WebView.
目前Flutter WebView提供的功能较少,文档中没写到的,可以理解为暂时不支持,如果就想做,建议修改Plugin代码。如果想换内核,比如Android端换腾讯X5内核,也可以修改Plugin端代码(修改Plugin代码只会修改本地对应版本的缓存,修改不能提交到Git)。本文就有修改Plugin代码需求,请往下看。
由于我本人是Android
出身,所以更多的是从Android开发的视角来说明。
添加依赖
dependencies: webview_flutter: ^2.3.1
引用包
import 'package:webview_flutter/webview_flutter.dart';
webview_flutter
要求android minSdkVersion 19
WebView(initialUrl: "https://flutterchina.club/")
本地文件index.html
在Flutter项目的路径为./assets/index.html
。
Android WebView本身支持加载本地文件,上述路径在Android APK中的路径为android_asset/flutter_assets/assets/index.html
,所以代码如下:
String url = "";
if (Platform.isAndroid) {
url = "file:///android_asset/flutter_assets/assets/index.html";
}
...
WebView(initialUrl: url)
IOS WebView Plugin本身不支持加载本地文件,需要修改Plugin FlutterWebView.m
代码,Plugin源码如下:
- (bool)loadUrl:(NSString*)url withHeaders:(NSDictionary*)headers {
NSURL* nsUrl = [NSURL URLWithString:url];
if (!nsUrl) {
return false;
}
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:nsUrl];
[request setAllHTTPHeaderFields:headers];
[_webView loadRequest:request];
return true;
}
修改后IOS Plugin代码如下:
- (bool)loadUrl:(NSString*)url withHeaders:(NSDictionary*)headers {
NSURL* nsUrl = [NSURL URLWithString:url];
NSLog(@"webview_flutter: %@", nsUrl);
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:nsUrl];
[request setAllHTTPHeaderFields:headers];
if([url hasPrefix:@"http"]) {
[_webView loadRequest:request];
}else{
if (@available(iOS 9.0, *)) {
NSURL *findUrl = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/Frameworks/App.framework/flutter_assets/webres/%@", [[NSBundle mainBundle] bundlePath], @"index.html"]];
NSLog(@"Debug >>>> %@", findUrl);
NSString *loadUrl = [findUrl.absoluteString stringByReplacingOccurrencesOfString:@"index.html" withString:url];
NSURL * url = [NSURL URLWithString:loadUrl];
NSLog(@"Debug >>>> load url %@", url);
[_webView loadFileURL:url allowingReadAccessToURL:[url URLByDeletingLastPathComponent]];
} else {
// Fallback on earlier versions
NSLog(@"webview_flutter: loadFileUrl error");
}
}
return true;
}
Flutter代码如下:
String url = "";
if (Platform.isIOS) {
url = "file://Frameworks/App.framework/flutter_assets/assets/index.html";
}
...
WebView(initialUrl: url)
由于Flutter Dependencies 依赖版本规则问题,
webview_flutter_wkwebview
可能不定期升级,请以官方代码FlutterWebView.m为准,如果代码不一致,请按照以上思路修改代码。
使用起来很简单,看一下WebView
的完整参数,以下是整理简写的伪代码:
WebView(
onWebViewCreated : void Function(WebViewController controller),
initialUrl : String?,
javascriptMode : JavascriptMode = JavascriptMode.disabled,
javascriptChannels : Set?,
navigationDelegate : NavigationDelegate?,
gestureRecognizers : Set>?,
onPageStarted : (void Function(String url))?,
onPageFinished : (void Function(String url))?,
onProgress : (void Function(int progress))?,
onWebResourceError : (void Function(WebResourceError error))?,
debuggingEnabled : bool = false,
gestureNavigationEnabled : bool = false,
userAgent : String?,
zoomEnabled : bool = true,
initialMediaPlaybackPolicy : AutoMediaPlaybackPolicy = AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
allowsInlineMediaPlayback : bool = false,
)
onWebViewCreated: 创建WebView时调用,返回WebViewController
对象。
initialUrl: WebView加载的URL,也可以指定本地文件,如assets/index.html
Android
上的路径file:///android_asset/flutter_assets/assets/index.html
,IOS
上的路径file://Frameworks/App.framework/flutter_assets/assets/index.html
(由于IOS端不支持加载本地HTML,所以需要修改IOS端Plugin代码)。javascriptMode: 是否启用JavaScript
,默认为JavascriptMode.disabled
JavaScript
JavaScript
javascriptChannels : JavaScript
调用Flutter的方法渠道配置,常用方式。
navigationDelegate : WebView导航拦截,点击链接跳转时触发。可以通过拦截指定特征的URL,用作与JavaScript
交互。(个人不推荐使用:1. 不安全 2. HTTP1.1以下有长度限制)
目前发现设置
javascriptChannels
后,navigationDelegate
不会再触发,原因不得而知。
gestureRecognizers: 处理WebView与Wideget嵌套时的手势交互。
onPageStarted: 页面开始加载时触发,可以用来显示进度条。
onPageFinished: 页面加载结束时触发,可以隐藏进度条。
onProgress: 加载进度。
onWebResourceError: 资源加载失败时触发,返回的数据因平台而异(就是包装了原生平台的错误信息)。
debuggingEnabled: 调试开关。
Android
可以使用Chrome
调试WebView加载,使用方法IOS
可以使用Safari
调试。gestureNavigationEnabled: 是否开始手势返回功能,默认关闭,只在IOS上有效。
userAgent: HTTP请求头User Agent
配置。
zoomEnabled: 是否开启手势缩放,默认开始。
如果要关闭手势,在IOS上必须设置
javascriptMode = JavascriptMode.unrestricted
才会生效。
initialMediaPlaybackPolicy: 媒体播放设置,默认为AutoMediaPlaybackPolicy.require_user_action_for_all_media_types
。
allowsInlineMediaPlayback: 控制IOS是否允许在HTML5上嵌入媒体播放器,在Android上默认允许。
下面看一下WebViewController
的伪代码:
class WebViewController {
Future loadUrl(String url, {Map? headers})
Future currentUrl()
Future canGoBack()
Future canGoForward()
Future goBack()
Future goForward()
Future reload()
Future clearCache()
@Deprecated('Use [runJavascript] or [runJavascriptReturningResult]')
Future evaluateJavascript(String javascriptString)
Future runJavascript(String javaScriptString)
Future runJavascriptReturningResult(String javaScriptString)
Future getTitle()
Future scrollTo(int x, int y)
Future scrollBy(int x, int y)
Future getScrollX()
Future getScrollY()
}
loadUrl : 加载新页面
currentUrl : 获取当前URL
canGoBack : 是否可以回退
canGoForward : 是否可以前进
goBack : 回退(如果不可回退,就不执行任何操作)
goForward : 前进(如果不可前进,就不执行任何操作)
reload : 重新加载/刷新
clearCache : 清除缓存
evaluateJavascript : 调用JavaScript
方法,已过时,使用runJavascript/runJavascriptReturningResult代替
runJavascript : 无返回值的调用JavaScript
方法
runJavascriptReturningResult : 有返回值的调用JavaScript
方法
getTitle : 获取HTML标题
scrollTo : 滑动到X、Y位置
scrollBy : 在当前位置上滑动X、Y长度
getScrollX : 获取X轴滑动长度,单位:pixels
getScrollY : 获取Y轴滑动长度,单位:pixels
Cookie
目前只支持删除,方法有以下两个:
WebView.platform.clearCookies();
CookieManager().clearCookies();
JS代码如下,分别有一个无返回值和一个有返回值的方法。
Flutter端调用代码如下:
///调用有返回值JS方法,并打印结果
_controller
.runJavascriptReturningResult(
"flutterCallJsMethod('Flutter调用了JS')")
.then((value) {
Fluttertoast.showToast(msg: value.toString());
});
///调用无返回值JS方法
_controller
.runJavascript("flutterCallJsMethodNoResult('Flutter调用了JS')");
evaluateJavascript:方法已经弃用。
Flutter提供了简单的支持JS调用的方法和参数,也可以通过修改Plugin
实现自定义方法和参数。
Flutter端代码如下
WebView(
...
javascriptMode: JavascriptMode.unrestricted,
javascriptChannels: {
JavascriptChannel(
name: "toast",
onMessageReceived: (message) {
String result = message.message;
...
}),
},
...
)
javascriptChannels
:表示JS可以调用Flutter的对象集合
name
:表示映射的对象名
onMessageReceived
:为JS传过来的参数
以上代码在Android
端的实现为
webView.addJavascriptInterface(new JsInterface(), "toast");
...
public class JsInterface {
@JavascriptInterface
public void postMessage(String message) {
...
}
}
JavaScript调用代码如下
name:toast
: 这个值是三端公共定义的。
postMessage
: 这个方法是Flutter Plugin内部默认定义好的一个方法。之所以叫这个名字是为了更好的兼容IOS
。
IOS WebKit
提供了一个默认的name:postMessage
,参考文档
The user content controller uses this parameter to define a JavaScript function for your message handler in all frames in the specified content world.
The name of this function is window.webkit.messageHandlers..postMessage(), where corresponds to the value of this parameter.
For example, if you specify the string MyFunction, the user content controller defines the window.webkit.messageHandlers.MyFunction.postMessage() function in JavaScript.
自定义方法名和参数,需要修改Plugin代码。
JS端代码如下
Flutter端代码如下
javascriptMode: JavascriptMode.unrestricted,
javascriptChannels: {
...
JavascriptChannel(
name: "jscomm",
onMessageReceived: (message) {
dynamic result = json.decode(message.message);
String event = result["event"];
String data = result["data"];
}),
},
以上代码在Android
端的实现为
webView.addJavascriptInterface(new JsInterface(), "jscomm");
...
public class JsInterface {
@JavascriptInterface
public void toLocalEvent(String event,String data) {
}
}
修改Flutter Plugin代码:JavaScriptChannel.java
//默认实现的方法
@JavascriptInterface
public void postMessage(final String message) {
Runnable postMessageRunnable =
new Runnable() {
@Override
public void run() {
HashMap arguments = new HashMap<>();
arguments.put("channel", javaScriptChannelName);
arguments.put("message", message);
methodChannel.invokeMethod("javascriptChannelMessage", arguments);
}
};
if (platformThreadHandler.getLooper() == Looper.myLooper()) {
postMessageRunnable.run();
} else {
platformThreadHandler.post(postMessageRunnable);
}
}
//新增加的方法
@JavascriptInterface
public void toLocalEvent(final String event, final String data) {
Runnable postMessageRunnable =
new Runnable() {
@Override
public void run() {
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("event", event);
jsonObject.put("data", data);
} catch (JSONException e) {
}
HashMap arguments = new HashMap<>();
arguments.put("channel", javaScriptChannelName);
arguments.put("message", jsonObject.toString());
methodChannel.invokeMethod("javascriptChannelMessage", arguments);
}
};
if (platformThreadHandler.getLooper() == Looper.myLooper()) {
postMessageRunnable.run();
} else {
platformThreadHandler.post(postMessageRunnable);
}
}
注意:这个新增的
toLocalEvent
方法,修改的是本地缓存代码,不能提交到Git上,也就是说只有修改的那个人运行的代码有效!
以上就是这次分享的全部了,切记修改的Plugin
代码不会被提交,如果示例代码无法运行,仔细看文档。
index.html完整源码见GitHub
Flutter完整源码见GitHub