美文网首页
flutter webview 加载本地文件的一些尝试

flutter webview 加载本地文件的一些尝试

作者: smallLabel | 来源:发表于2021-06-28 00:27 被阅读0次

最近做的项目需要用webview加载本地html,将下载到本地的文件URL传给HTML,所以学习了一下webview。以下部分内容仅包含iOS部分。
flutter使用webview组件一般有三个比较好的插件选择,分别是webview_flutterflutter_inappwebviewwebview_flutter_plus,webview_flutter_plus是webview_flutter 的扩展,可以实现加载本地html等功能。这几个插件之间有什么功能上的差别,大家可以看一些其他专门的介绍,我就不赘述了。我自己觉得flutter_inappwebview比较强大,用起来很舒服

webview_flutter_plus使用方法

  1. 把资源放到工程中,我的工程资源如图


    资源文件
  2. pubspec.yaml文件中配置资源路径,每个文件夹都要配置
    配置资源路径
  3. 按照官方示例直接设置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 步参考以上

  1. 使用方法
    InAppWebViewinitialFile参数,传入'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同等级目录下,按照上面介绍加载静态资源的方式加载沙盒文件即可。
总结一下我的使用步骤:

  1. 在APP启动时将assets/demo目录复制到沙盒路径下
  2. 指定下载资源目录与index.html同等级目录
  3. 用上面介绍的插件加载静态资源

按照上述描述直接使用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请求的方式

  1. 复制资源文件到沙盒cache目录


    image.png
  2. 指定下载目录到沙盒cache
  3. 在原生端起一个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);
    }
    
}
  1. 加载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地址: 地址

相关文章

网友评论

      本文标题:flutter webview 加载本地文件的一些尝试

      本文链接:https://www.haomeiwen.com/subject/ucffultx.html