Flutter之WebView加载html字符串及高度自适应

10,833 阅读4分钟

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战

Flutter 官方提供了 webview_flutter 插件用于加载 html 页面,网上有很多使用该插件的 WebView 控件通过 url 加载 html 页面的资料教程,但是加载 html 字符串的资料则相对较少。本篇文章主要讲解如何使用 webview_flutter 插件提供的 WebView 加载 html 字符串。

使用场景

什么时候在开发中会用到使用 WebView 来加载 html 字符串呢?这里举一个笔者在真实项目中遇到的需求,PC 端使用富文本编辑器编辑提交的内容需要在 App 中进行展示,因服务端存储/返回给 App 的就是一段 html 字符串,此时 App 就需要用到 WebView 来对这段 html 字符串内容进行加载显示。

WebView 加载 Html 字符串

添加依赖

在项目的 pubspec.yaml 文件中添加 webview_flutter 插件的依赖:

dependencies:
  webview_flutter: ^3.0.0

加载 html 字符串

导入 webview_flutter 的包:

import 'package:webview_flutter/webview_flutter.dart';

然后使用时直接创建一个 WebView 对象即可。

WebView 构造参数有一个 initialUrl 参数,可以通过该参数传入一个 url 地址加载 html 页面,如下:

WebView(
  initialUrl: "https://juejin.cn",
  javascriptMode: JavascriptMode.unrestricted,
)

initialUrl 传入掘金的地址,然后将 javascriptMode 设置为 JavascriptMode.unrestricted 即开启 js ,因为网页中使用了 js 如果不开启则会导致页面加载不完全。运行效果如下图:

image-20220123170418022

但是 WebView 的构造函数中并没有加载 html 字符串的参数,所以无法在创建 WebView 的时候直接加载 html 字符串,需要用到 WebViewController 来进行加载 html 字符串。

WebViewController 提供了 loadHtmlString 或者 loadUrl 方法来加载 html 字符串, WebViewController 需要通过 WebViewonWebViewCreated 回调获取,如下:

WebView(
  javascriptMode: JavascriptMode.unrestricted,
  onWebViewCreated: (WebViewController controller){
    //获取到 WebViewController
  },
)

loadHtmlString

通过 WebViewControllerloadHtmlString 方法加载 html 字符串,参数传入 html 字符串即可,如下:

String html = """
    <p> Hello WebView 掘金</p>
  """;

WebView(
  javascriptMode: JavascriptMode.unrestricted,
  onWebViewCreated: (WebViewController controller){
    controller.loadHtmlString(html);
  },
)

效果如下:

image-20220123172649840

内容确实加载出来了,但是文字特别小,那是因为在 iOS 上需要在 html 里加上 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 否则默认字体会特别小。修改如下:

  String html = """
   <!DOCTYPE html>
        <html>
        <head><meta name="viewport" content="width=device-width, initial-scale=1.0"></head>
          <body>
            <p> Hello WebView 掘金</p>
          </body>
        </html>
  """;

效果如下:

image-20220123173120564

除此之外,loadHtmlString 还有一个可选参数 baseUrl ,作用是当 html 字符串中使用到了相对路径的 url 时,设置了 baseUrl 后 WebView 加载 html 请求相应 url 时就会带上 baseUrl,如 html 字符串中有显示图片,图片地址写的是相对路径,此时就可以使用 baseUrl 参数来解决,如下:

 String html = """
   <!DOCTYPE html>
        <html>
        <head><meta name="viewport" content="width=device-width, initial-scale=1.0"></head>
          <body>
            <p> Hello WebView 掘金</p>
            <img src="/obj/static/xitu_juejin_web/e08da34488b114bd4c665ba2fa520a31.svg"/>
          </body>
        </html>
  """;

WebView(
  javascriptMode: JavascriptMode.unrestricted,
  onWebViewCreated: (WebViewController controller){
    controller.loadHtmlString(html, baseUrl: "https://lf3-cdn-tos.bytescm.com");
  },
)

效果如下:

image-20220123173742549

如图,稀土掘金的 logo 就通过这种方式加载出来了。html 字符串中的图片地址并不是完整的地址,如果不设置 baseUrl 则会加载失败。

loadUrl

除了使用 loadHtmlString 外, 还可以使用 loadUrl 来加载 html 字符串,看到这里可能有些人就会有疑问,loadUrl 不是加载 url 的么怎么也能用来加载 html 字符串? 确实是可以的,但是需要通过 Uri.dataFromString 进行转换。如下:

WebView(
  javascriptMode: JavascriptMode.unrestricted,
  onWebViewCreated: (WebViewController controller){
    controller.loadUrl(Uri.dataFromString(html, mimeType: "text/html", encoding: utf8,).toString());
  },
)

通过 Uri.dataFromString 转换同样可实现加载 html 字符串,需要注意的是 Uri.dataFromString 除了 html 字符串的参数外,还需要设置 mimeType"text/html"encodingutf8 ,如果不设置 encoding 加载时可能会报错,而不设置 mimeType 则会直接把 html 代码加载出来。

WebView 高度自适应

WebView 默认无法做到高度自适应,即根据 html 内容高度自适应,当在 Column 等控件中使用 WebView 而不手动设置固定高度时则会报错。

如果要做到高度自适应,则需要用到 js 方法,在 html 中通过 js 监听页面大小的变化,然后获取页面高度再将高度传递到 Flutter 中,在 Flutter 中获取到高度后再动态改变 WebView 的高度。

实现步骤如下:

  • 给 WebView 添加一个 JavascriptChannel 用于 js 与 Flutter 通信
  • 在 html 中添加 script 使用 ResizeObserver 监听 body 元素的大小变化,在变化回调里调用上一步添加的 JavascriptChannel 发送页面高度的消息
  • 在 Flutter 中接收到消息时,获取 js 发送过来的高度值,然后更新 WebView 的高度

代码如下:

  String html = """
   <!DOCTYPE html>
        <html>
        <head><meta name="viewport" content="width=device-width, initial-scale=1.0"></head>
          <body>
            <p> Hello WebView 掘金</p>
            <img src="https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/e08da34488b114bd4c665ba2fa520a31.svg"/>
          </body>
        </html>
        <script>
          const resizeObserver = new ResizeObserver(entries =>
          Resize.postMessage(document.documentElement.scrollHeight.toString()) )
          resizeObserver.observe(document.body)
        </script>
  """;

SizedBox(
  height: webViewHeight,
  child: WebView(
    javascriptMode: JavascriptMode.unrestricted,
    onWebViewCreated: (WebViewController controller){
      controller.loadHtmlString(html, baseUrl: "https://lf3-cdn-tos.bytescm.com");
    },
    javascriptChannels: {
      JavascriptChannel(name: "Resize", onMessageReceived: (JavascriptMessage message) {
        double height = double.parse(message.message);
        setState(() {
          webViewHeight = height;
        });
      })
    }
  ),
)

如上,给 WebView 外面包了一层 SizeBox 用于限制 WebView 的高度,同时给 WebView 添加了名为 ResizeJavascriptChannel , 在 html 字符串中添加了 script 使用 js 的 ResizeObserver 监听页面元素大小变化,在变化回调里获取元素的 scrollHeight 值并将其发送到 Flutter,在 Flutter 的 JavascriptChannel 消息回调里获取高度值并更新 SizeBox 的高度。从而实现了 WebView 的高度自适应。

完整代码

class WebViewPage extends StatefulWidget {
  const WebViewPage({Key? key}) : super(key: key);

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

class _WebViewPageState extends State<WebViewPage> {

  double webViewHeight = 0;
  String html = """
   <!DOCTYPE html>
        <html>
        <head><meta name="viewport" content="width=device-width, initial-scale=1.0"></head>
          <body>
            <p> Hello WebView 掘金</p>
            <img src="/obj/static/xitu_juejin_web/e08da34488b114bd4c665ba2fa520a31.svg"/>
          </body>
        </html>
        <script>
          const resizeObserver = new ResizeObserver(entries =>
          Resize.postMessage(document.documentElement.scrollHeight.toString()) )
          resizeObserver.observe(document.body)
        </script>
  """;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("WebView"),),
      body: SizedBox(
        height: webViewHeight,
        child: WebView(
          javascriptMode: JavascriptMode.unrestricted,
          onWebViewCreated: (WebViewController controller){
            controller.loadHtmlString(html, baseUrl: "https://lf3-cdn-tos.bytescm.com");
          },
          javascriptChannels: {
            JavascriptChannel(name: "Resize", onMessageReceived: (JavascriptMessage message) {
              double height = double.parse(message.message);
              setState(() {
                webViewHeight = height;
              });
            })
          }
        ),
      ),
    );
  }

}