flutter webview 与 前端vue通信

夏侯枫
2023-12-01

flutter有3个比较好的webview插件

  • 1. webview_flutter : 官方维护的 WebView 插件,特性基于原生和 Flutter SDK 封装,继承 StatefulWidget,因此支持内嵌于 flutter Widget 树中.
    适用于:单个静态文件,远程webdi地址。缺点:不支持本地服务,不支持加载一个完整本地的html项目,比如vue项目。

  • 2. flutter_webview_plugin : flutter社区进行维护,易用程度目前高于webview_flutter 。特性是基于原生 WebView 封装的 Flutter 插件,将原生的一些基本使用 API 封装好提供给 Flutter 调用,因此并不能内嵌于 Flutter Widget 树中。
    在界面的跳转时必须得先释放掉,返回后又要重新初始化。
    适用于:支持本地服务,加载一个完整本地的html项目,比如vue项目。

  • 3. flutter_inappwebview : 个人开发者维护。基于原生和 Flutter SDK 封装,优点是内置了一个本地服务,缺点,更新太慢,bug修复很慢。

加载vue项目

  1. 第一步,通过webview 加载vue项目,项目里包含图片、字体,js,css文件,首先要启动一个localserver(本地服务)
  2. 第二步,通过localserver去加载vue打包后的dist文件

结合 flutter_webview_plugin 插件最佳

  mounted() {
    /// 给flutter app调用。vue的方法不会暴露给app使用 需要把方法名暴露给window
    window.forFlutterInit = this.forFlutterInit;
    window.forFlutterRender = this.forFlutterRender;
  },
  methods: {
	forFlutterInit(){
    	console.log("想做什么都行");
    },
    forFlutterRender(){
    	console.log("第二个方法,想做什么都行");
    },
  }

1. webview_flutter用法

# pubspec.yaml
...
webview_flutter: 0.3.22+1
...
 
class TestWebView extends StatefulWidget {
  final props;
  TestWebView({
    this.props,
  });

  @override
  _TestWebViewState createState() => _TestWebViewState();
}

class _TestWebViewState extends State<TestWebView> {
  WebViewController _controller;

  _loadHtmlFromAssets() async {
    String fileText = await rootBundle.loadString('assets/webview/index_test.html');
    await _controller
        .loadUrl(Uri.dataFromString(fileText, mimeType: 'text/html', encoding: Encoding.getByName('utf-8')).toString());
  }

  JavascriptChannel _toasterJavascriptChannel(BuildContext context) {
    return JavascriptChannel(
      name: 'Toaster',
      onMessageReceived: (JavascriptMessage message) {
        Scaffold.of(context).showSnackBar(
          SnackBar(content: Text(message.message)),
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: MediaQuery.of(context).size.width,
      // height: 500, // 去掉高度尝试
      child: WebView(
        initialUrl: 'https://www.ifredom.com',
        javascriptMode: JavascriptMode.unrestricted,
        onWebViewCreated: (WebViewController webViewController) {
          // _controller.complete(webViewController);
          _controller = webViewController;
          _loadHtmlFromAssets();
        },
        javascriptChannels: <JavascriptChannel>[
          _toasterJavascriptChannel(context),
        ].toSet(),
        navigationDelegate: (NavigationRequest request) {
          // 拦截请求
          if (request.url.contains('youtube')) {
            print('重新开一个 webview to $request}');
            Navigator.of(context).push(new MaterialPageRoute(builder: (_) {
              return TestWebView();
            }));
            return NavigationDecision.prevent;
          }
          return NavigationDecision.navigate;
        },
        onPageStarted: (String url) {
          print('Page 开始 loading: $url');
        },
        onPageFinished: (String url) {
          print('Page 结束 loading: $url');
  		  // flutter 调用js方法
  		  String script = "forFlutterInit('flutter传入的值')";
          _controller.evaluateJavascript(script).then((result) {
            print(result);
            setState(() {
              // ...
            });
          });
        },
        gestureNavigationEnabled: true,
      ),
    );
  }
}

test.html 用于测试,需要提前在pubspec.yaml中添加

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset=utf-8>
  <meta http-equiv=X-UA-Compatible content="IE=edge">
  <meta name=viewport content="width=device-width,initial-scale=1">
  <link rel=icon href=favicon.ico>
  <title>测试</title>
</head>

<body>
  html内容页面
  <button onclick="callFlutter()">点击</button>
  <div id="p1">Flutter 调用了 JS.</div>

  <script>
    function forFlutterInit(message) {
    	// html页面获取flutter 传入参数
      document.getElementById("p1").innerHTML = message;
    }
    function callFlutter() {
    	// html页面调用flutter 方法
      Toaster.postMessage("html页面 调用Flutter方法");
    }
  </script>
</body>

</html>

2. flutter_webview_plugin用法

# pubspec.yaml
...
flutter_webview_plugin: 0.3.11
...

第一步. localserver.dart 用于启动本地服务,在文尾。


class MusicScoreWebView extends StatefulWidget {
  @override
  _MusicScoreWebViewState createState() => _MusicScoreWebViewState();
}

class _MusicScoreWebViewState extends State<MusicScoreWebView> {
  final flutterWebViewPlugin = FlutterWebviewPlugin();
  CopyInAppLocalhostServer localhostServer = locator<CopyInAppLocalhostServer>();
  double _viewProgress = 0;
  String _url  = "";
  
  
  Future _initLocalServe() async {
    print("启动 server/webview 服务");
    await localhostServer.start();
    await addListenerWebview(); // 如果不需要中途更改html内容,可比不添加此监听方法。监听WebView的加载事件
  }
  
  Future _dispose() async {
    flutterWebViewPlugin.dispose();  //必须关闭
    await localhostServer.close();
    print("关闭 server/webview 服务");
  }
  
  Future<void> addListenerWebview({String name}) async {
    double _viewProgress = 0;
    
    flutterWebViewPlugin.onProgressChanged.listen((viewProgress) async {
      print("加载进度 $viewProgress");
      _viewProgress = viewProgress;
    });
    
    flutterWebViewPlugin.onStateChanged.listen((viewState) async {
      print("状态改变了: ${viewState.type}");

      if (viewState.type == WebViewState.finishLoad) {
        print("onStateChanged 页面加载完成");

        if (_viewProgress == 1.0) {
          String _testData = "123456"; 
          String _initScript = "forFlutterInit('$_testData')";
          await flutterWebViewPlugin.evalJavascript(_initScript).then((result) async {
            print('调用 forFlutterInit 方法返回值: $result');
          });
        }
      }
    });
  }
 
  @override
  void initState() {
    super.initState();
    _initLocalServe();
  }
  
  @override
  void dispose() {
    _dispose()
    super.dispose();
  }
  
  
  @override
  Widget build(BuildContext context) {
    final Set<JavascriptChannel> jsChannels = [
      JavascriptChannel(
        name: 'FlutterToast',
        onMessageReceived: (JavascriptMessage message) {
          showToast(message.message);
        },
      ),
    ].toSet();

    return SizedBox(
      width: MediaQuery.of(context).size.width,
      // height: 500, // 去掉高度尝试
      child: WebviewScaffold(
        url: "http://localhost:8085/assets/index.html",
        javascriptChannels: jsChannels,
        mediaPlaybackRequiresUserGesture: false,
        // appBar: AppBar(
        //   title: const Text('Widget WebView'),
        // ),
        withZoom: true,
        withLocalStorage: true,
        hidden: true,
        initialChild: Container(
          child: const Center(child: Text('正在加载中.....')),
        ),
        key: Key("webview"),
        debuggingEnabled: true,
      ),
    );
  }
  }

3. flutter_inappwebview用法

此插件参数过多,并且需要配置andorid, 更多参数使用方法,详见官方文档

# pubspec.yaml
...
flutter_inappwebview 4.0.0+4
...
  InAppLocalhostServer localhostServer = new InAppLocalhostServer();
  
  Widget _buildWebviewScaffold() {
     return InAppWebView(
             initialUrl: "http://localhost:8080/assets/index.html",
             initialHeaders: {},
             initialOptions: InAppWebViewGroupOptions(
                 inAppWebViewOptions: InAppWebViewOptions(
                   debuggingEnabled: true,
                 )
             ),
             onWebViewCreated: (InAppWebViewController controller) {
				print("做点什么");
             },
             onLoadStart: (InAppWebViewController controller, String url) {
				print("做点什么");
             },
             onLoadStop: (InAppWebViewController controller, String url) {
				print("做点什么");
             },
      );
   }

其他。启动本地服务

CopyInAppLocalhostServer.start();

CopyInAppLocalhostServer.close();

// localserver.dart
import 'dart:async';
import 'dart:io';

import 'package:flutter/services.dart' show rootBundle;
import 'package:mime/mime.dart';

class CopyInAppLocalhostServer {
  // final _log = Logger("CopyInAppLocalhostServer");
  HttpServer _server;
  int _port = 8080;

  CopyInAppLocalhostServer({int port = 8080}) {
    this._port = port;
  }
  Future<void> start() async {
    if (this._server != null) {
      throw Exception('Server already started on http://localhost:$_port');
    }

    var completer = Completer();

    runZoned(() {
      HttpServer.bind('127.0.0.1', _port).then((server) {
        print('本地服务 http://localhost:' + _port.toString());

        this._server = server;
        server.listen((HttpRequest request) async {
          var body = List<int>();
          var path = request.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(e.toString());
            await request.response.close();
            return;
          }

          var contentType = ['text', 'html'];
          if (!request.requestedUri.path.endsWith('/') && request.requestedUri.pathSegments.isNotEmpty) {
            var mimeType = lookupMimeType(request.requestedUri.path, headerBytes: body);
            if (mimeType != null) {
              contentType = mimeType.split('/');
            }
          }

          request.response.headers.contentType = ContentType(contentType[0], contentType[1], charset: 'utf-8');
          request.response.add(body);
          await request.response.close();
        });

        completer.complete();
      });
    }, onError: (e, stackTrace) => print('Error: $e $stackTrace'));
    return completer.future;
  }

  ///Closes the server.
  Future<void> close() async {
    if (this._server != null) {
      await this._server.close(force: true);
      print('本地服务 on http://localhost:$_port closed');
      this._server = null;
    }
  }
}

如果文章对你有用,感谢右上角 >>>点赞 | 收藏 <<<

 类似资料: