likes
comments
collection
share

突然插播:Flutter与JS互调

作者站长头像
站长
· 阅读数 5

所以要一锅端了?

很顺利的解决了iOS端,Swift与H5的交互问题,公司觉得你这光解决iOS的不行啊,最好是写一个通用的WebView壳子,能满足日常的iOS与Android双端WebView加载H5调试。

这不是要一锅端的节奏?

既然是双端调试,那么必然会想到使用跨平台方案来写这个壳子,基于技术背景,Flutter当仁不让的成为了第一选择,而选择使用的Flutter中的WebView插件就是webview_flutter!

别说,我为了这个突然的需求,也反反复复看了webview_flutter的官方文档和代码,自己写了Demo来验证。

简单介绍一下webview_flutter插件


A Flutter plugin that provides a WebView widget.

On iOS the WebView widget is backed by a WKWebView; On Android the WebView widget is backed by a WebView.

一个为Flutter提供WebView组件的插件。

在iOS端是基于原生的WKWebView,而在Android则是基于系统的WebView。


从这段简介可以看出,虽然这是一个Flutter插件,但是实际上桥接的都是基于iOS与Android平台系统层面的WebView组件,这样做的好处就是在每一端都做到使用原生,只要中间通信层逻辑一致并完好,那么就可以展平了双端WebView中H5与Flutter通信的能力。

Flutter与JS互调使用与讲解

在这里我们先精简一下官方给出的example例子。

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';

import 'package:webview_flutter/webview_flutter.dart';

class WebViewExample extends StatefulWidget {
  @override
  _WebViewExampleState createState() => _WebViewExampleState();
}

class _WebViewExampleState extends State<WebViewExample> {
  final Completer<WebViewController> _controller =
      Completer<WebViewController>();

  @override
  void initState() {
    super.initState();
    if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView example'),
      ),
      // We're using a Builder here so we have a context that is below the Scaffold
      // to allow calling Scaffold.of(context) so we can show a snackbar.
      body: Builder(builder: (BuildContext context) {
        return WebView(
          initialUrl: 'https://www.baidu.com',
          javascriptMode: JavascriptMode.unrestricted,
          onWebViewCreated: (WebViewController webViewController) {
            _controller.complete(webViewController);
          },
          onProgress: (int progress) {
            print("WebView is loading (progress : $progress%)");
          },
          javascriptChannels: <JavascriptChannel>{
            _toasterJavascriptChannel(context),
          },
          navigationDelegate: (NavigationRequest request) {
            if (request.url.startsWith('https://www.youtube.com/')) {
              print('blocking navigation to $request}');
              return NavigationDecision.prevent;
            }
            print('allowing navigation to $request');
            return NavigationDecision.navigate;
          },
          onPageStarted: (String url) {
            print('Page started loading: $url');
          },
          onPageFinished: (String url) async {
              final webViewController = await _controller.future;
              final callback = await webViewController.evaluateJavascript("alert('Hello world');");
              print(callback);
          },
          gestureNavigationEnabled: true,
        );
      }),
    );
  }

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

这段代码最为核心的部分就是以下这段,请详细看看我写的注释,详细又重要!!!

如果在网页里面看不懂的,可以考虑下载webview_flutter的代码,一点点对照的看,并与前两篇我文章。

return WebView(
    /// 需要加载的网页URL
    initialUrl: 'https://www.baidu.com',
    /// JS的模式
    javascriptMode: JavascriptMode.unrestricted,
    /// WebView生成成功时,会回调一个webViewController,它非常的重要,后面会继续说
    onWebViewCreated: (WebViewController webViewController) {
        /// 接受该控制器
        _controller.complete(webViewController);
    },
    /// webView加载的回调进度,相当于Swift中对WKWebView中的estimatedProgress的KVO
    onProgress: (int progress) {
        print("WebView is loading (progress : $progress%)");
    },
    /// JS通道,这里相当于Flutter侧监听JS方法的集合,和Swift中通过userContentController注册监听JS句柄一致
    javascriptChannels: <JavascriptChannel>{
        /// 这个函数后面单独说明
        _toasterJavascriptChannel(context),
    },
    /// 这里相当于Swift中WKNavigationDelegate代理方法中的
    /// func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void)方法,
    /// 就是用于拦截跳转的,和拦截微信和支付宝跳转类似
    navigationDelegate: (NavigationRequest request) {
         /// 这里做了跳转拦截,如果跳转的地址是油管,那么将阻止加载
        if (request.url.startsWith('https://www.youtube.com/')) {
          print('blocking navigation to $request}');
          return NavigationDecision.prevent;
        }
        print('allowing navigation to $request');
        /// 其他的跳转允许
        return NavigationDecision.navigate;
    },
    /// 监听网页开始加载,相当于Swift中WKNavigationDelegate代理方法中的
    /// func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!)方法
    onPageStarted: (String url) {
        print('Page started loading: $url');
    },
    /// 监听网页加载完毕,相当于Swift中WKNavigationDelegate代理方法中的
    /// func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)方法
    onPageFinished: (String url) async {
              /// 通过Completer拿webView控制器
              final webViewController = await _controller.future;
              /// 通过webViewController运行JS方法,这里是调用一个JS的弹窗,并且异步获取JS的回调,这个回调可能为空
              final callback = await webViewController.evaluateJavascript("alert('Hello world');");
              print(callback);
          },
    /// 是否支持手势侧滑
    gestureNavigationEnabled: true,
 );

可以看到,如果理解了Swift中的WKWebView的WKNavigationDelegate代理方法含义,Flutter中的WebView无非是换了些名称,将delegate风格换成了callback风格而已的初始化函数。

_toasterJavascriptChannel就是具体化的如果注册监听JS方法句柄:

JavascriptChannel _toasterJavascriptChannel(BuildContext context) {
    return JavascriptChannel(
        /// 注册一个监听句柄为SeasonCallback
        /// 和Swift中func add(_ scriptMessageHandler: WKScriptMessageHandler, name: String)方法一致
        name: 'SeasonCallback',
        /// 监听的SeasonCallback回调,获取JS传到Flutter侧的参数,和Swift中的WKScriptMessageHandler代理回调一致,
        /// func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)
        /// 然后JS就可以调用Flutter侧的函数了
        onMessageReceived: (JavascriptMessage message) {
          /// Flutter接受到JS的message,并调起Flutter函数
          Scaffold.of(context).showSnackBar(
            SnackBar(content: Text(message.message)),
          );
        });
  }

回忆一下上一篇文章中的3个需求,如果在Flutter需要实现,应该怎么写呢?理解了上一篇文章并看懂了这篇文章的注释,应该不在话下。

总结

  • Flutter中的webview_flutter本质上还是调用了双端的原生WebView组件,所以对应的一致性较好,如果了解原生与JS的互调,那么Flutter与JS互调依葫芦画瓢即可。

  • Swift中的多个代理回调在Flutter中都通过Callback完成,这样看起来紧凑,虽然可能在编码中不易于看清楚,但是比较符合Flutter的声明式编程风格,需要努力适应。

webview_flutter的使用注意事项与坑点:

注意事项

iOS端需要在info.plist文件中添加配置与权限:

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>
<key>io.flutter.embedded_views_preview</key>
<true/>

Android端:

  • 1.在路径android/app/build.gradle添加最小SDK支持:
android {
    defaultConfig {
        minSdkVersion 19
    }
}

  • 2.安卓端开启混合视图
import 'dart:io';

import 'package:webview_flutter/webview_flutter.dart';

class WebViewExample extends StatefulWidget {
  @override
  WebViewExampleState createState() => WebViewExampleState();
}

class WebViewExampleState extends State<WebViewExample> {
  @override
  void initState() {
    super.initState();
    /// 就是下面这段代码
    // Enable hybrid composition.
    if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
  }

  @override
  Widget build(BuildContext context) {
    return WebView(
      initialUrl: 'https://flutter.cn',
    );
  }
}
  • 3.开启http选项和隐私权限,注意是在项目/android/app/src/main/AndroidManifest.xml文件中进行添加
<!-- 需要在application中添加android:usesCleartextTraffic="true",即是否可以使用明文传输,非SSL -->
   <application
        android:label="web_view"
        android:icon="@mipmap/ic_launcher"
        android:usesCleartextTraffic="true">
<!-- 权限根据项目需要配置-->        
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- 相机的权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 麦克风的权限 -->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<!-- 写sd卡的权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- 读sd卡权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />

坑点

如果你使用webview_flutter在Android端加载H5,而H5中正好需要调用系统相机或者是相册的话,这个JS是无法得到相应的,而iOS端却可以。

android webview 里点击<input type=’file’>没有反应。

出现这个问题的原因是,官方的webview_flutter插件没有针对这一功能做适配。

详细可以看flutter webview android h5 上传文件失败解决办法

如果需要Android端有这个能力,需要在添加权限的同时,改写插件在Android侧的实现。

当然有另外一个插件flutter_webview_plugin可以实现Android上传这个功能,但是这个flutter_webview_plugin不管从易用性还是功能性,都不及webview_flutter。

我好希望有个一个大佬给教我改写webview_flutter插件,支持Android端的上传功能,我按照网上的方法改写了,然后崩溃了。。。

明天继续

明天会讲解Swift中,准确是OC中WebViewJavaScriptBridge的使用与注意实现,大家加油!

转载自:https://juejin.cn/post/6971605092990451726
评论
请登录