关于在Flutter Web中加载html

9,600 阅读3分钟

  最近碰到一个需求,需要在Flutter Web项目中加载html。在手机端我们可以使用webview_plugin插件加载,但是这个插件在web上面是无效的。那么,在Flutter Web中,该如何加载html呢?Flutter为我们提供了一个专门用于web的控件:HtmlElementView。(这里说点题外话,HtmlElementView的注释文档里面有写,构建html是昂贵操作,如果有等效的Flutter实现可以替换,那么请尽量不要这么搞。)

HtmlElementView的基本使用规则

  使用这个控件必须要注意,使用之前需要注册!!!
  可以在initState()的时候注册,当然你也可以在runApp()的时候就全局注册好(个人不建议):

import 'dart:ui' as ui;
ui.platformViewRegistry.registerViewFactory(
        'hello-world-html',
        (int viewId) => HtmlElement();

  ui.platformViewRegistry会报静态错误,不用管,完全不影响你代码跑起来。解释下registerViewFactory()方法中传的两个参数:第一个个人理解是给你需要插入的html片段取一个id名称,第二个参数则是对应的真正用来加载html的元素。这里的HtmlElement()是一个基类,实际书写的时候应该传入IFrameElementHtmlHtmlElementMediaElementBodyElement等等你实际需要的元素。
  使用HtmlElementView时,只需要将已注册元素对应的id传进去就可以了:

HtmlElementView(
    viewType: 'hello-world-html',
    )

加载URL

  加载URL可以使用IFrameElement(url请写全了,不要漏掉https):

ui.platformViewRegistry.registerViewFactory(
        'hello-world-html',
        (int viewId) => IFrameElement()
          ..style.border = 'none'
          ..src = 'https://www.baidu.com');

加载本地html文件

ui.platformViewRegistry.registerViewFactory(
        'hello-world-html',
        (int viewId) => IFrameElement()
          ..style.border = 'none'
          ..src = '/assets/test2.html');

加载html string

  • 加载比较复杂的混合型html(篇幅比较短的)

  如果是各种标签混合的html,可以使用HtmlHtmlElement

ui.platformViewRegistry.registerViewFactory(
        'hello-world-html',
        (int viewId) => HtmlHtmlElement()
          ..style.border = 'none'
          ..setInnerHtml(html));

  这里要注意有坑,因为实际项目中使用到的html,含有<img><a href>之类的标签是很正常的,但是如果你按照上面的方式加载,图片、超链接之类的都是没法显示的,因为默认的HtmlHtmlElement是把这些标签禁用掉的(估计是为了性能吧,毕竟开篇就说了,Flutter其实不希望你直接加载html标签的),你需要手动开启允许加载复杂标签。具体做法是传入一个validator参数:

final NodeValidator _validator = NodeValidatorBuilder.common()
    ..allowElement('img', attributes: ['src'], uriPolicy: _AllowUriPolicy())
    ..allowElement('a', attributes: ['href'], uriPolicy: _AllowUriPolicy());
    
ui.platformViewRegistry.registerViewFactory(
        'hello-world-html',
        (int viewId) => HtmlHtmlElement()
          ..style.border = 'none'
          ..setInnerHtml(html,validator: _validator));
         
class _AllowUriPolicy implements UriPolicy {
  @override
  bool allowsUri(String uri) {
    return true;
  }
}
  • 加载比较复杂的混合型html(篇幅比较长的)

  如果是长篇大论的html,如果你用HtmlHtmlElement加载,就会发现页面无法滚动。需要通过与ScrollView组合展示,但是要注意,HtmlElementView的高度需要计算:

ui.platformViewRegistry.registerViewFactory(
        'hello-world-html',
        (int viewId) => HtmlElementView()
          ..style.border = 'none'
          ..innerHtml = content);

  关于计算:可以通过scrollOffset获取高度,但是这个高度获取会有一个问题,第一次进入页面时的高度是准确的,但是如果你调整了窗口的大小,高度就会变得不准确了。暂时性的解决办法是监听到窗口大小变化时,强行重新push本页面(在Flutter Web中,调整窗口大小本身就是会重构当前页面的,包括widget和数据都会重构,但是这和重新push似乎是不一样的,会带有某些缓存数据?导致通过scrollOffset获取的HtmlElementView高度会无线增大)。如果你有更好的解决方案,欢迎留言告诉我。
  万能的IFrameElement也可以加载混合型html,使用srcdoc的方式加载,但是IFrameElement如果和ScrollView组合的话会比较麻烦,这玩意儿高度好像不太好算(希望会算的大佬教教我):

ui.platformViewRegistry.registerViewFactory(
        'hello-world-html',
        (int viewId) => IFrameElement()
          ..style.border = 'none'
          ..srcdoc = content);
  • 加载单个标签的html

  建议使用轻量型的MediaElementBodyElementFormElementMenuElement等等。