最近做的项目需要用webview加载本地html,将下载到本地的文件URL传给HTML,所以学习了一下webview。以下部分内容仅包含iOS部分。
flutter使用webview组件一般有三个比较好的插件选择,分别是webview_flutter、flutter_inappwebview和webview_flutter_plus,webview_flutter_plus是webview_flutter 的扩展,可以实现加载本地html等功能。这几个插件之间有什么功能上的差别,大家可以看一些其他专门的介绍,我就不赘述了。我自己觉得flutter_inappwebview
比较强大,用起来很舒服
webview_flutter_plus使用方法
-
把资源放到工程中,我的工程资源如图
资源文件 - 在
pubspec.yaml
文件中配置资源路径,每个文件夹都要配置
配置资源路径 - 按照官方示例直接设置assets下html文件路径即可
import 'package:flutter/material.dart';
import 'package:webview_flutter_plus/webview_flutter_plus.dart';
void main() {
runApp(WebViewPlusExample());
}
class WebViewPlusExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: WebViewPlusExampleMainPage(),
);
}
}
class WebViewPlusExampleMainPage extends StatefulWidget {
@override
_WebViewPlusExampleMainPageState createState() =>
_WebViewPlusExampleMainPageState();
}
class _WebViewPlusExampleMainPageState
extends State<WebViewPlusExampleMainPage> {
WebViewPlusController _controller;
double _height = 1000;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('webview_flutter_plus Example'),
),
body: ListView(
children: [
SizedBox(
height: _height,
child: WebViewPlus(
javascriptChannels: null,
initialUrl: 'assets/demo/index.html',
onWebViewCreated: (controller) {
this._controller = controller;
},
onPageFinished: (url) {
_controller.getHeight().then((double height) {
print("Height: " + height.toString());
setState(() {
_height = height;
});
});
},
javascriptMode: JavascriptMode.unrestricted,
),
)
],
),
);
}
}
完成以上步骤即可正常加载工程目录下的资源文件,原理是用在项目中开启了一个http服务,通过源码可以看出。
class _Server {
static HttpServer? _server;
///Closes the server.
static Future<void> close() async {
if (_server != null) {
await _server!.close(force: true);
// print('Server running on http://localhost:$_port closed');
_server = null;
}
}
///Starts the server
static Future<int> start() async {
var completer = Completer<int>();
runZonedGuarded(() {
HttpServer.bind('localhost', 0, shared: true).then((server) {
//print('Server running on http://localhost:' + 5353.toString());
_server = server;
server.listen((HttpRequest httpRequest) async {
List<int> body = [];
String path = httpRequest.requestedUri.path;
path = (path.startsWith('/')) ? path.substring(1) : path;
path += (path.endsWith('/')) ? 'index.html' : '';
try {
body = (await rootBundle.load(path)).buffer.asUint8List();
} catch (e) {
print('Error: $e');
httpRequest.response.close();
return;
}
var contentType = ['text', 'html'];
if (!httpRequest.requestedUri.path.endsWith('/') &&
httpRequest.requestedUri.pathSegments.isNotEmpty) {
String? mimeType = lookupMimeType(httpRequest.requestedUri.path,
headerBytes: body);
if (mimeType != null) {
contentType = mimeType.split('/');
}
}
httpRequest.response.headers.contentType =
ContentType(contentType[0], contentType[1], charset: 'utf-8');
httpRequest.response.add(body);
httpRequest.response.close();
});
completer.complete(server.port);
});
}, (e, stackTrace) => print('Error: $e $stackTrace'));
return completer.future;
}
}
flutter_inappwebview使用方法
1.2 步参考以上
- 使用方法
用InAppWebView
的initialFile
参数,传入'assets/demo/index.html'
文件路径即可
以上只是加载本地静态html的一些简单方法。下面是介绍我的需求以及实现方式。
需求:本地加载html及相关js css 静态资源,下载服务器资源到沙盒目录下,将沙盒目录下的下载好的文件路径作为参数传给html。
如果按照上述方式加载静态资源,并直接将沙盒文件路径以file://
协议传参,则会出现跨域问题,因此我的思路是想办法把html文件拷贝到沙盒目录下,使得index.html
文件可以访问到下载好的资源。
先上拷贝文件目录方法,复制assets/demo
文件夹资源到library/cache
目录下
import 'dart:io';
import 'dart:convert' as convert;
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
// 拷贝demo到沙盒
Future<void> copyDemoToSandBox() async {
Directory cache = await getTemporaryDirectory();
String htmlPath = await getIndexHtmlPath();
File htmlFile = File(htmlPath);
bool exists = await htmlFile.exists();
if (exists) return;
final manifestContent = await rootBundle.loadString('AssetManifest.json');
final Map<String, dynamic> manifestMap = convert.json.decode(manifestContent);
manifestMap.keys
.where((key) => key.contains('demo/') && !key.contains('.DS_'))
.forEach((element) async {
print(element);
// 读取数据
ByteData data = await rootBundle.load(element);
List<int> bytes =
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
String dataPath = path.join(cache.path, element);
File file = File(dataPath);
await file.create(recursive: true);
await File(dataPath).writeAsBytes(bytes);
});
}
// 获取拷贝完的HTML路径
Future<String> getIndexHtmlPath() async {
Directory cache = await getTemporaryDirectory();
return cache.path + '/assets/demo/index.html';
}
接下来将下载资源目录指定到index.html
同等级目录下,按照上面介绍加载静态资源的方式加载沙盒文件即可。
总结一下我的使用步骤:
- 在APP启动时将
assets/demo
目录复制到沙盒路径下 - 指定下载资源目录与index.html同等级目录
- 用上面介绍的插件加载静态资源
按照上述描述直接使用webView,修改一下源码也可以实现,但是不推荐这种方式,所以简单说一下。
WKWebView加载本地资源方法(不推荐!!!只是参考了解
)
[_webview loadFileURL:url allowingReadAccessToURL:[NSURL fileURLWithPath:dstPath]];
第一个参数是要加载的html文件,注意是file://
开头的, 第二个参数是可访问的目录url,也是file://
开头,因为webView插件本身就是用WKWebView实现的,当我们直接传入一个file://
类型的url时,内部还是用NSUrlRequest
承接的,所以修改这里的代码,做个判断,指定访问目录为沙盒目录。
FlutterWebView.m
文件内
- (bool)loadUrl:(NSString*)url withHeaders:(NSDictionary<NSString*, NSString*>*)headers {
NSURL* nsUrl = [NSURL URLWithString:url];
if (!nsUrl) {
return false;
}
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:nsUrl];
[request setAllHTTPHeaderFields:headers];
//判断url的加载方式
if([url hasPrefix:@"http"]) {
[_webView loadRequest:request];
}else if ([url hasPrefix:@"file:"]) {
NSString *librayPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
NSString *assetPath = [NSString stringWithFormat:@"%@/assets", librayPath];
NSURL *fileUrl = [NSURL fileURLWithPath:assetPath isDirectory:true];
[_webView loadFileURL:[NSURL URLWithString:url] allowingReadAccessToURL:fileUrl];
}
return true;
}
最后再说一下我的实际实现方式,就离谱!
我是在原生端起了一个http服务,将cache
目录作为服务器根目录,所有的资源加载都采用http请求的方式
-
复制资源文件到沙盒cache目录
image.png - 指定下载目录到沙盒cache
- 在原生端起一个http服务,这里我用了
GCDWebServer
框架
image.png
image.png
// 起一个服务
- (void)startWebServer {
_webServer = [[GCDWebServer alloc] init];
NSString *cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
NSString *assetPath = [NSString stringWithFormat:@"%@/assets", cachePath];
[_webServer addGETHandlerForBasePath:@"/" directoryPath:assetPath indexFilename:nil cacheAge:3600 allowRangeRequests:YES];
_webServer.delegate = self;
NSError *error = nil;
[_webServer startWithOptions:@{GCDWebServerOption_BindToLocalhost: @YES, GCDWebServerOption_Port: @(9999)} error:&error];
if (error) {
NSLog(@"%@", error);
}
}
- 加载html,用
InAppWebView
实现,因为看了下这个框架介绍,发现文档太牛了,这时候就可以用http的方式访问html了。其实,这时在浏览器里也能直接用localhost访问
@override
Widget build(BuildContext context) {
return InAppWebView(
initialUrlRequest: URLRequest(
url: Uri.http('localhost:9999', '/demo/index.html'),
),
initialOptions: InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(useOnLoadResource: true),
),
onLoadResource:
(InAppWebViewController controller, LoadedResource resource) {
print('加载资源: ' + resource.url.toString());
},
onWebViewCreated: (InAppWebViewController controller) async {
_webController = controller;
},
onLoadStop: (InAppWebViewController controller, Uri? url) async {
// 每次下载资源文件
await widget.mapViewModel.downloadMapFile();
// 以下三个方法都是调用js方法,业务相关
// 向html指定目录
await setMapPath();
await setMapID(widget.mapViewModel.mapID);
// 加载地图
await loadMap();
},
onLoadError: (InAppWebViewController controller, Uri? url, int code,
String message) {
print('错误: ' + message);
},
onConsoleMessage:
(InAppWebViewController controller, ConsoleMessage consoleMessage) {
print('js 打印消息: ' + consoleMessage.message);
},
shouldInterceptFetchRequest:
(InAppWebViewController controller, FetchRequest fetchRequest) async {
return fetchRequest;
},
iosOnNavigationResponse: (InAppWebViewController controller,
IOSWKNavigationResponse navigationResponse) async {
return IOSNavigationResponseAction.ALLOW;
},
);
}
// 设置地图路径 map目录就是指定的下载资源目录
Future<dynamic> setMapPath() async {
String mapFileUri = 'http://localhost:9999/demo/map/';
return _webController.evaluateJavascript(
source: 'setMapPath(\'$mapFileUri\')');
}
demo地址: 地址
网友评论