Flutter第十六章(WebView)

版权声明:本文为作者原创书籍。转载请注明作者和出处,未经授权,严禁私自转载,侵权必究!!!

情感语录: 心若计较,时时都有怨言;心若宽容,处处都是晴天。

由于个人原因导致好久没更新文章了,在第十九章前的内容都是19年前所编写,最新的内容会在后面持续更新。 本篇内容带来的是 WebView 在Flutter 中的使用。在 Flutter 中第三方的WebView是非常多的,如:
tweet_webviewinteractive_webviewflutter_webview_plugin 等。但这个些插件本章不做介绍,喜欢的童鞋可自行研究,本章主要介绍的是官方的 webview 那么WebView 究竟可以拿来做什么呢? WebView 可以解析DOM 元素,展示html页面的控件,它和浏览器展示页面的原理是相同的,所以可以把它当做浏览器看待。

一、WebView 的引入

1、在pubspec.yaml文件中添加依赖
dependencies:
  fluttertoast: ^3.0.3
  flutter:
    sdk: flutter

  #添加 WebView 插件
  webview_flutter: ^0.3.15+1

本人使用的当前最新版本 0.3.15,读者想体验最新版本请在使用时参看最新版本号进行替换,特别需要注意的是 添加插件时一定的注意代码的缩进问题,多一个空格或者少一个都将导致引入不成功。

2、安装依赖库

执行 flutter packages get 命令;AS 开发工具直接右上角 packages get也可。

3、在需要使用的地方导包引入
 import 'package:webview_flutter/webview_flutter.dart';
4、IOS 配置文件配置

在IOS目录下 Runner 包下的 Info.plist 文件中添加如下配置:

<key>io.flutter.embedded_views_preview</key>
<string>YES</string>

请严格按照官方文档进行配置,否则在设备上可能出现运行错误。

二、WebView 构造函数

  const WebView({
    Key key,
    this.onWebViewCreated,
    this.initialUrl,
    this.javascriptMode = JavascriptMode.disabled,
    this.javascriptChannels,
    this.navigationDelegate,
    this.gestureRecognizers,
    this.onPageFinished,
    this.debuggingEnabled = false,
    this.userAgent,
    this.initialMediaPlaybackPolicy =
        AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
  })
常用属性:
    属性                             描述
    
    onWebViewCreated                WebView创建完毕时的回调
    
    initialUrl                      WebView加载的地址
    
    javascriptMode                  是否开启支持 JS 模式(默认不开启)
    
    javascriptChannels              配置 JS 调用flutter 
    
    navigationDelegate              路由委托(可拦截需要特殊处理的地址等)
    
    gestureRecognizers              手势处理(一般不用)
    
    onPageFinished                  页面加载完成时的回调
    
    initialMediaPlaybackPolicy      页面中的媒体播放权限配置

上面说到 WebView 可以看做是一个浏览器,下面 就使用 它来装载下网页试试呢? 试试就试试 直接撸 O(∩_∩)O

实例代码:

    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    import 'package:webview_flutter/webview_flutter.dart';

    class WebViewPage extends StatefulWidget{

      @override
      State<StatefulWidget> createState() {
        return WebViewPageState();
      }
    }

    class WebViewPageState extends State<WebViewPage> {

      String _title = "webview";

      @override
      Widget build(BuildContext context) {
          
         // 使用 IOS 风格
        return CupertinoPageScaffold(
          navigationBar: CupertinoNavigationBar(
            //添加一个标题
            middle: Text("$_title"),
          ),

          child: SafeArea(
              child: WebView(
                initialUrl: "http://www.baidu.com",
              )
          ),
        );
      }
    }

这里使用到了 SafeArea 组件,这个组件是前面章节中没介绍过的,它的作用主要是解决一些异型屏幕的适配问题,比如在常规屏幕上 你的内容文字能正常显示,但放在刘海屏上可能就被遮挡住了,因此使用 SafeArea 可解决此类问题。 它的使用也是非常简单,这里不做过多介绍。

实例效果
WebView1.gif

在创建 WebView 后 只是简单的 配置 initialUrl 属性就将百度的所有内容装载了进来,在开发中很多页面是需要使用网页的方式进行显示,因此选用 WebView 可极大较轻开发成本,这也是它的魅力之处,简单除暴。

下面来看下稍微复杂点的案例,这里会使用到上面的几个回调函数。

实例代码
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter_learn/util/ToastUtil.dart';
    import 'package:webview_flutter/webview_flutter.dart';

    class WebViewPage extends StatefulWidget{

      @override
      State<StatefulWidget> createState() {
        return WebViewPageState();
      }
    }

    class WebViewPageState extends State<WebViewPage> {

      WebViewController _controller;
      String _title = "webview";

      @override
      Widget build(BuildContext context) {

        // 使用 IOS 风格
        return CupertinoPageScaffold(

          navigationBar: CupertinoNavigationBar(

          //添加一个标题
            middle: Text("$_title"),
          ),

          child: SafeArea(

              child: WebView(
                initialUrl: "http://www.baidu.com",
                //开启Js 支持
                javascriptMode: JavascriptMode.unrestricted,
                onWebViewCreated: (controller) {
                  //拿到controller
                  _controller = controller;
                },
                onPageFinished: (url) {
                  //获取网页的标题来显示
                  _controller.evaluateJavascript("document.title").then((result) {
                    setState(() {
                      _title = result;
                    });
                  }
                  );
                  print("输出当前地址:" + url);
                },
                navigationDelegate: (NavigationRequest request) {
                  //拦截 百度账号登录跳转
                  if (request.url.startsWith(
                      "https://wappass.baidu.com/passport/?login")) {
                    ToastUtil.show("请求被拦截到诺  哈哈");

                    return NavigationDecision.prevent;
                  }
                  return NavigationDecision.navigate;
                },
              )
          ),
        );
      }
    }

这段代码也是非常简单,首先在 WebView创建完毕后 通过 onWebViewCreated 回调函数拿到 WebViewController ,接下来在 页面加载完成时(onPageFinished被触发) 通过 WebViewController 的 evaluateJavascript() 方法可以拿到 JS 的信息。最后在 路由委托中(navigationDelegate) 处理了下百度账号登录页面跳转的拦截。

实例效果
WebView2.gif

温馨提示:

NavigationDecision.prevent:阻止路由,返回此枚举类型 即阻止当前页面跳转。
NavigationDecision.navigate:允许路由,返回此枚举类型 即允许所有的请求跳转。

上面的所有演示都只是对页面的一些装载显示过程而已,在实际开发中可能并不是这么简单,往往会涉及到和 JavaScript 通信问题,相互传值等。接下来所介绍的内容才是本章节中最为重要的知识点。端正姿势小板凳上坐好 O(∩_∩)O

三、WebView 和 JavaScript 相互通信

WebView 和 JS 的通信常常分两种情况:第一种是 JS或者html 文件放在本地即 App内部。第二种是需要通信的 JS 放在服务器端的。本章主要介绍的是第一种,其目的是因为放在本地都会了,放在服务器端的就肯定会了。同时使用本地的方式 会引出一个知识点 AssetBundle ,这也是前面章节所没介绍过的。

1、工程中新建 assets 目录

在工程中 新建 assets 目录,然后创建用于测试的 html 文件。结构大致如下:

assets.png

2、JS 向 Flutter 传值

2.1 编写 html 代码,添加一个按钮,点击按钮后向 Flutter 发送数据。
<html lang="zh-CN">

    <head>
        <title>测试网页</title>
    </head>

    <body>

    <p>这是一段很长很长的故事..... </p>

    </body>

    <!-- 点击按钮将  信息传给 flutter   -->
    <button onclick="flutterTest.postMessage('来自js的传值!')">发送数据给客户端</button>

</html>
2.2 编写 flutter 代码,接收 js传过来的值
  javascriptChannels: <JavascriptChannel>[
      JavascriptChannel(

          // 双方约定好的 协议
          name: "flutterTest",
          onMessageReceived: (JavascriptMessage message) {

            print("输出参数: ${message.message}");
            ToastUtil.show(message.message);

          }
      ),
    ].toSet(),

下面来看下运行效果:

js传值flutter.gif

温馨提示:

① 其中 flutterTest 为 Flutter 和 JS 双方协商好的协议值,你可以随意更改,但任何一方修改,另一方也都必须修改。

② JS 向 Flutter 发送数据通过 协议值+postMessage("value") 方式进行。

3、Flutter 向 JS 传值

3.1 编写 html 代码,添加一个方法提供给 Flutter 调用
    <html lang="zh-CN">

        <p id='testContent'>hello world</p>

        <head>
            <title>测试网页</title>
        </head>

        <body>

        <p>这是一段很长很长的故事..... </p>

        </body>


        <!-- 提供给flutter 调用的方法   -->

        <script>

            function flutterCallJs(message){
                document.getElementById("testContent").innerHTML = message
            }
        </script>

        <!-- 点击按钮将  信息传给 flutter   -->
        <button onclick="flutterTest.postMessage('来自js的传值!')">发送数据给客户端</button>

    </html>

3.2、Flutter 端代调用 JS 提供的方法

  ///FloatingActionButton
  floatingActionButton: FloatingActionButton(
    elevation: 0,
    child: Icon(
      Icons.send,
      size: 30,
    ),
    onPressed: () {
      //点击触发 调用 js 方法
      _controller.evaluateJavascript("flutterCallJs('这是客户端传过来的值')");

    },
  ),
  floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,

运行效果:

Flutter传值 JS.gif

在Flutter 中 evaluateJavascript 方法成功触发了 JS 的方法并将值携带了过去,可以看到 id 为 testContent 元素标签的内容也被修改了,说明传值是OK的。

温馨提示:

① 其中 flutterCallJs 为 JS 接收 Flutter 传值的方法,该方法由 JS 端先定义好提供给Flutter。

② Flutter 通过 evaluateJavascript 方法可以直接调用 JS 提供的方法。

3.3、Flutter 端完整代码如下:

    import 'dart:convert';

    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    import 'package:flutter_learn/util/ToastUtil.dart';
    import 'package:webview_flutter/webview_flutter.dart';

    class WebViewPage extends StatefulWidget{

      @override
      State<StatefulWidget> createState() {
        return WebViewPageState();
      }
    }

    class WebViewPageState extends State<WebViewPage> {

      WebViewController _controller;
      String _title = "webview";

      @override
      Widget build(BuildContext context) {

        return Scaffold(

          ///FloatingActionButton
          floatingActionButton: FloatingActionButton(
            elevation: 0,
            child: Icon(
              Icons.send,
              size: 30,
            ),
            onPressed: () {
              //点击触发 调用 js 方法
              _controller.evaluateJavascript("flutterCallJs('这是客户端传过来的值')");

            },
          ),
          floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,

         appBar: AppBar(
            title: Text(_title),

            // 控制 WebView 返回(依次返回界面)
            leading: IconButton(
              icon: Icon(Icons.arrow_back,color: Colors.white,),
              onPressed:(){

                _controller.canGoBack().then((value){

                  if(value){
                    _controller.goBack();
                  }else{
                     Navigator.pop(context);
                  }
                });
              },
            )
           ),

          body: SafeArea(

              child: WebView(
                
                initialUrl: "",
                //开启Js 支持
                javascriptMode: JavascriptMode.unrestricted,
                onWebViewCreated: (controller) {
                  //拿到controller
                  _controller = controller;

                  _loadHtmlFromAssets();
                },
                onPageFinished: (url) {

                  //获取网页的标题来显示
                  _controller.evaluateJavascript("document.title").then((result) {
                    setState(() {
                      _title = result;
                    });
                  });
                  print("输出当前地址:" + url);
                },

                javascriptChannels: <JavascriptChannel>[
                  JavascriptChannel(

                      // 双方约定好的 协议
                      name: "flutterTest",
                      onMessageReceived: (JavascriptMessage message) {

                        print("输出参数: ${message.message}");
                        ToastUtil.show(message.message);

                      }
                  ),
                ].toSet(),
              )
          ),
        );
      }

    //  从本地加载html文件,需要使用异步操作
      _loadHtmlFromAssets() async {

        String fileText = await rootBundle.loadString('assets/html/JavaScriptTest.html');

        _controller.loadUrl(Uri.dataFromString(fileText, mimeType: 'text/html', encoding: Encoding.getByName('utf-8')).toString());

      }

    }

Flutter端需要注意以下要点:

initialUrl 属性可以不写,或者赋值空字符串即可。

javascriptMode 属性必须指定为不受限模式(JavascriptMode.unrestricted) 否则不能通信。

javascriptChannels 配置 JS 通道,用于 JS向 flutter发送数据。其中 name 属性是双方协商好的协议值。 传递过来的值被 onMessageReceived 函数的 JavascriptMessage 接收。

④ Flutter 中读取 assets 文件是通过 AssetBundle 对象加载的,WebViewController 在加载本地网页时和原生其实一样的,这里只是多了一个 Uri来进行包装。

⑤ 如果你的JS 文件在 服务端 直接配置 initialUrl 属性即可,无需要 ④ 中的操作。

⑥ 如果是复杂的数据类型请封装成json字符串,确保在 Android 和 IOS 都能支持。

写在最后

笔者在写这篇文章时是当时最新版本(0.3.15)相比之前版本解决了很多问题,但还是存在很多不足之处。如果你的需求不是特别复杂使用官方的即可,跟着官方持续跟进。

好了本章节到此结束,又到了说再见的时候了,如果你喜欢请留下你的小红星;你们的支持才是创作的动力,如有错误,请热心的你留言指正, 谢谢大家观看,下章再会 O(∩_∩)O

实例源码地址:https://github.com/zhengzaihong/flutter_learn/blob/master/lib/page/webview/WebViewPage.dart

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容