字节笔记本字节笔记本

Flutter 与 JS的交互获取页面内容

2022-10-09

本文介绍了如何在Flutter应用中使用`webview_flutter`组件与JS进行交互,包括添加依赖、配置权限、WebView组件的构造方法、以及如何实现JS调用Flutter和Flutter调用JS的功能。

添加 webview_flutter 组件

在项目的 pubspec.yaml 文件中添加依赖:webview_flutter: ^3.0.0,然后执行 pub get

  • 由于加载WebView需要使用网络,所以还需要在android中添加网络权限。打开目录android/app/src/main/AndroidManifest.xml,然后添加如下代码即可。
<uses-permission android:name="android.permission.INTERNET"/>
  • 由于iOS在9.0版本默认开启了Https,所以要运行Http的网页,还需要在ios/Runner/Info.plist文件中添加如下代码。
<key>io.flutter.embedded_views_preview</key>
<string>YES</string>

webview_flutter 组件的构造方法的简单介绍

WebView({
    Key key,
    this.onWebViewCreated,             //WebView创建完成之后的回调
    this.initialUrl,                               // 初始化 URL
    this.javascriptMode = JavascriptMode.disabled,    //JS执行模式,默认是不调用
    this.javascriptChannels,             // JS可以调用Flutter 的通道
    this.navigationDelegate,            // 路由委托,可以使用它执行拦截操作
    this.gestureRecognizers,          // 手势监听相关
    this.onPageStarted,                 //开始加载页面回调
    this.onPageFinished,              // 页面加载完成的回调
    this.onWebResourceError,     //资源加载失败回调
    this.debuggingEnabled = false,
    this.gestureNavigationEnabled = false,
    this.userAgent,
    this.initialMediaPlaybackPolicy =
        AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
  })

使用 Webview 加载网页时,很多时候需要与JS进行交互,即JS调用FlutterFlutter调用JS

查看官方文档,发现提供的能力,在实际开发中可以参考这些功能。包含:网络请求、Cookies相关、缓存相关、加载html、加载失败页面等等。

Future<void> _onShowUserAgent(
      WebViewController controller, BuildContext context) async {
    // Send a message with the user agent string to the Toaster JavaScript channel we registered
    // with the WebView.
    await controller.runJavascript(
        'Toaster.postMessage("User Agent: " + navigator.userAgent);');
  }

  Future<void> _onListCookies(
      WebViewController controller, BuildContext context) async {
    final String cookies =
        await controller.runJavascriptReturningResult('document.cookie');
    // ignore: deprecated_member_use
    Scaffold.of(context).showSnackBar(SnackBar(
      content: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          const Text('Cookies:'),
          _getCookieList(cookies),
        ],
      ),
    ));
  }

  Future<void> _onAddToCache(
      WebViewController controller, BuildContext context) async {
    await controller.runJavascript(
        'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";');
    // ignore: deprecated_member_use
    Scaffold.of(context).showSnackBar(const SnackBar(
      content: Text('Added a test entry to cache.'),
    ));
  }

  Future<void> _onListCache(
      WebViewController controller, BuildContext context) async {
    await controller.runJavascript('caches.keys()'
        '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))'
        '.then((caches) => Toaster.postMessage(caches))');
  }

  Future<void> _onClearCache(
      WebViewController controller, BuildContext context) async {
    await controller.clearCache();
    // ignore: deprecated_member_use
    Scaffold.of(context).showSnackBar(const SnackBar(
      content: Text('Cache cleared.'),
    ));
  }

  Future<void> _onClearCookies(BuildContext context) async {
    final bool hadCookies = await cookieManager.clearCookies();
    String message = 'There were cookies. Now, they are gone!';
    if (!hadCookies) {
      message = 'There are no cookies.';
    }
    // ignore: deprecated_member_use
    Scaffold.of(context).showSnackBar(SnackBar(
      content: Text(message),
    ));
  }

  Future<void> _onNavigationDelegateExample(
      WebViewController controller, BuildContext context) async {
    final String contentBase64 =
        base64Encode(const Utf8Encoder().convert(kNavigationExamplePage));
    await controller.loadUrl('data:text/html;base64,$contentBase64');
  }

  Future<void> _onSetCookie(
      WebViewController controller, BuildContext context) async {
    await CookieManager().setCookie(
      const WebViewCookie(
          name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything'),
    );
    await controller.loadUrl('https://httpbin.org/anything');
  }

  Future<void> _onDoPostRequest(
      WebViewController controller, BuildContext context) async {
    final WebViewRequest request = WebViewRequest(
      uri: Uri.parse('https://httpbin.org/post'),
      method: WebViewRequestMethod.post,
      headers: <String, String>{'foo': 'bar', 'Content-Type': 'text/plain'},
      body: Uint8List.fromList('Test Body'.codeUnits),
    );
    await controller.loadRequest(request);
  }

  Future<void> _onLoadLocalFileExample(
      WebViewController controller, BuildContext context) async {
    final String pathToIndex = await _prepareLocalFile();

    await controller.loadFile(pathToIndex);
  }

  Future<void> _onLoadFlutterAssetExample(
      WebViewController controller, BuildContext context) async {
    await controller.loadFlutterAsset('assets/www/index.html');
  }

  Future<void> _onLoadHtmlStringExample(
      WebViewController controller, BuildContext context) async {
    await controller.loadHtmlString(kLocalExamplePage);
  }

  Future<void> _onTransparentBackground(
      WebViewController controller, BuildContext context) async {
    await controller.loadHtmlString(kTransparentBackgroundPage);
  }

  Widget _getCookieList(String cookies) {
    if (cookies == null || cookies == '""') {
      return Container();
    }
    final List<String> cookieList = cookies.split(';');
    final Iterable<Text> cookieWidgets =
        cookieList.map((String cookie) => Text(cookie));
    return Column(
      mainAxisAlignment: MainAxisAlignment.end,
      mainAxisSize: MainAxisSize.min,
      children: cookieWidgets.toList(),
    );
  }

  static Future<String> _prepareLocalFile() async {
    final String tmpDir = (await getTemporaryDirectory()).path;
    final File indexFile = File(
        <String>{tmpDir, 'www', 'index.html'}.join(Platform.pathSeparator));

    await indexFile.create(recursive: true);
    await indexFile.writeAsString(kLocalExamplePage);

    return indexFile.path;
  }

JS调用Flutter

这种方式实现的原理主要是加载网页的时候进行拦截

下面举例实现的代码

js代码:

document.location = "js://webview?name=candy";

flutter 端代码:

navigationDelegate: (NavigationRequest request) {
  if(request.url.startsWith("js://webview")) {
    print("开始处理 ${request.url}");
    return NavigationDecision.prevent;
  }
  return NavigationDecision.navigate;
},

这里的 NavigationDecision.prevent表示阻止路由替换,NavigationDecision.navigate表示允许路由替换。

javascriptChannels 方式实现

js代码:

<button onclick="callFlutter()">callFlutter</button>
function callFlutter(){
   Toast.postMessage("js call flutter");  
}

flutter 端代码:

WebView(
javascriptChannels: <JavascriptChannel>[
_shareJavascriptChannel(context),
].toSet(),
)

JavascriptChannel _shareJavascriptChannel(BuildContext context) {
  return JavascriptChannel(
      name: 'share',
      onMessageReceived: (JavascriptMessage message) {
        print("参数: ${message.message}");
        showToast(message.message);
      });
}

Flutter 调用 JS

js代码:

function callJS(message){
document.getElementById("p1").style.visibility = message;
}

Flutter 代码

Future<void> evaluateJavascript() async {
  print('evaluateJavascript');
  _controller.runJavascript('callJS('visible');');
}

加载本地 html

截屏2022-01-11 上午9.37.12.png

截屏2022-01-11 上午9.37.49.png

html 代码

<!DOCTYPE html>
<html>
<body>
<style>*{font-size:50px;}</style>
<button onclick="callFlutter()">callFlutter</button>
<p id="p1" style="visibility:hidden;">
Flutter代码调用了JS方法.
</p>
<script src="http://code.jquery.com/jquery-2.1.4.min.js"></script>
<script src="http://cdn.amazeui.org/amazeui/2.5.0/js/amazeui.min.js"></script>
<script type="text/javascript">
function callJS(message){
document.getElementById("p1").style.visibility = message;
}
</script>
<script type="text/javascript">
function callFlutter(){
Toaster.postMessage('js call flutter');
}
</script>
</body>
</html>
Future<void> _loadHtmlFromAsset() async {
  String html = 'assets/static/test.html';
  final String path = await rootBundle.loadString(html);
  _controller.loadUrl(Uri.dataFromString(path,
      mimeType: 'text/html', encoding: Encoding.getByName('utf-8'))
      .toString());
}

完整实现代码

import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:webview_flutter/webview_flutter.dart';

class WebViewPage extends StatefulWidget {
  @override
  _WebViewPageState createState() => _WebViewPageState();
}

class _WebViewPageState extends State<WebViewPage> {
  late WebViewController _controller;
  String _title = "webview";

  //加载Html
  Future<void> _loadHtmlFromAsset() async {
    String html = 'assets/static/test.html';
    final String path = await rootBundle.loadString(html);
    _controller.loadUrl(Uri.dataFromString(path,
        mimeType: 'text/html', encoding: Encoding.getByName('utf-8'))
        .toString());
  }

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text("$_title"),
      ),
      child: SafeArea(
        child: WebView(
          //initialUrl: "https://flutterchina.club/",
          //JS执行模式 是否允许JS执行
          javascriptMode: JavascriptMode.unrestricted,
          onWebViewCreated: (controller) {
            _controller = controller;
            _loadHtmlFromAsset();
          },
          onPageFinished: (url) async{
           //调用JS方法,获取页面的标题
            String title = await _controller.runJavascriptReturningResult('document.title');
            setState(() {
              _title = title;
            });
            evaluateJavascript();
          },
          navigationDelegate: (NavigationRequest request) {
            if(request.url.startsWith("js://webview")) {
              print("开始处理 ${request.url}");
              return NavigationDecision.prevent;
            }
            return NavigationDecision.navigate;
          },
          javascriptChannels: <JavascriptChannel>{
            JavascriptChannel(
                name: "share",
                onMessageReceived: (JavascriptMessage message) {
                  print("参数: ${message.message}");
                  //实际应用中要通过map通过key获取
                  String callbackname = message.message;
                  String data = "收到消息调用了";
                  String script = "$callbackname($data)";
                  _controller.runJavascript(script);
                }
            ),
          },
        ),
      ),
    );
  }

  Future<void> evaluateJavascript() async {
    print('evaluateJavascript');
    //这个是实现了Flutter控制了H5页面文本的显示
    _controller.runJavascript('callJS('visible');');
  }
}

加载网页

给这个属性赋值就可以了

initialUrl: 'https://flutterchina.club/',

页面是否可以回退

Future<bool> _goBack(BuildContext context) async {
  if (_controller != null && await _controller.canGoBack()) {
    _controller.goBack();
    return false;
  }
  return true;
}

官方实现了更多的功能:https://pub.dev/packages/webview_flutter/example

以上就讲解了 webView 的网页加载、JS交互、网络请求、Cookies相关、缓存相关、加载html、加载失败页面等功能。