Flutter Web:Shadow Root问题

简介: 在flutter1.x版本的dev分支上可以使用flutter web,但是我们在使用第三方js sdk的时候会出现问题,比如AgoraRtc、lottie等。问题都是出现在document.getElementById,因为这些sdk中或者使用的时候需要通过这个方法获取节点来操作

document.getElementById找不到节点


在flutter1.x版本的dev分支上可以使用flutter web,但是我们在使用第三方js sdk的时候会出现问题,比如AgoraRtc、lottie等。

问题都是出现在document.getElementById,因为这些sdk中或者使用的时候需要通过这个方法获取节点来操作,比如lottie,我们封装的代码如下:


...
class LottieWidget extends StatefulWidget{
  String path;
  bool isLoop;
  bool isAutoPlay;
  double width;
  double height;
  ...
  LottieWidget(this.path, this.width, this.height, this.isAutoPlay, this.isLoop, this._animationListener);
  @override
  State<StatefulWidget> createState() {
    return _lottieWidget;
  }
  void lottiePlay() {
    _lottieWidget.lottiePlay();
  }
  void lottieStop() {
    _animationListener = null;
    _lottieWidget.lottieStop();
  }
}
class _LottieWidget extends State<LottieWidget> {
  @override
  Widget build(BuildContext context) {
    js.context["lottieLoaded"] = lottieLoaded;
    DivElement divElement = DivElement();
    divElement.id = "lottie_anim";
    StyleElement styleElement = StyleElement();
    styleElement.type = "text/css";
    styleElement.innerHtml = """
          html,
          body {
          }
          """;
    divElement.append(styleElement);
    var script = """
    var lottieAnim = document.getElementById("lottie_anim");
    var lottieObj = lottie.loadAnimation({
    container:lottieAnim,
    renderer: 'svg',
    loop:${widget.isLoop},
    autoplay:${widget.isAutoPlay},
    path:"assets/${widget.path}"
    });
    // 动画播放完成触发
    lottieObj.addEventListener('complete', lottieLoaded);
    var lottiePlay = function(){
      lottieObj.play();
    }
    var lottiePause = function(){
        lottieObj.pause();       
    }
    var lottieStop = function() {
        lottieObj.stop();
    }
    """;
    ScriptElement scriptElement = new ScriptElement();
    scriptElement.innerHtml = script;
    divElement.append(scriptElement);
    String _divId = "lottieanim" + DateTime.now().toIso8601String();
    ui.platformViewRegistry.registerViewFactory(
      _divId,
      (int viewId) => divElement,
    );
    Widget _iframeWidget = HtmlElementView(
      key: UniqueKey(),
      viewType: _divId,
    );
    return SizedBox(child: _iframeWidget, width: widget.width, height: widget.height,);
  }
  void lottiePlay() {
    js.context.callMethod("lottiePlay");
  }
  void lottieStop() {
    js.context.callMethod("lottieStop");
  }
  void lottiePause() {
    js.context.callMethod("lottiePause");
  }
  // 动画播放完成触发
  void lottieLoaded() {
    print("loaded");
    widget._animationListener?.call();
  }
  @override
  void dispose() {
    super.dispose();
    lottieStop();
    widget._animationListener = null;
  }
}
复制代码


可以看到我们将一个id为lottie_anim的div添加到页面中,然后在js代码中通过document.getElementById获取这个节点,并设置到lottie中,这样lottie的sdk中就会在这个div上绘制动画。


但是执行的时候发现动画根本没显示,而且没有报错。通过在js中打印日志逐行测试发现document.getElementById("lottie_anim")获取到的是null。但是为什么是null的呢?

我们运行后打开chrome开发者工具,在Elements栏中查找lottie_anim,发现可以找到,但是它的位置如下:

网络异常,图片无法展示
|


可以看到这个div是在一个shadow-root下。

那么这个是做什么用的?


Shadow Dom


shadow dom简单来说就是封装,就是将一个组件封装起来,同时设置了隔离,外界无法访问内部的节点。比如video,我们使用的时候非常简单:


<video src="" id='test'></video>
复制代码


但是当我们打开开发者工具,在设置中将show user agent shadow DOM选中后,在回头看Elements中的节点,就会发现video下面存在一个shadow-root,在下面有很多节点,包括播放按钮、播放时长、进度条等等。


关于Shadow DOM,我参考的是quanzhan.applemei.com/webStack/Tk…

正是因为Shadow DOM隐藏的这种特性,导致了上面的问题。因为在flutter中,我们用HtmlElementView来展示html组件,这些组件都会被放在Shadow DOM中,所以导致了在js中通过document.getElementById获取的都是null,也就导致了很多第三方sdk无法正常使用。


解决问题


我们发现了问题,但是如何去解决呢?

其实可以获取Shadow DOM中的节点,只不过要复杂一点。首先我们看上面的节点信息,在Shadow DOM外一层是一个flt-platform-view的节点,这个我们是可以直接获取到的,通过getElementsByTagName,因为页面上可以会有多个flt-platform-view,所以这是一个array,,如下:


网络异常,图片无法展示
|


我们这里其实只有一个flt-platform-view,所以array里只有一个flt-platform-view节点。然后我们获取它的shadowRoot就可以得到Shadow节点,再通过getElementById来获取我们需要的节点即可,如下:


var sroot = document.getElementsByTagName("flt-platform-view")[0].shadowRoot;
var lootiedom = sroot.getElementById('lottie_anim');
复制代码


这样就可以得到相应的节点,替换上面js第一行代码就可以正常显示了。

其实在网上也有很多人遇到了同样的问题,比如blog.csdn.net/thunder_sz/… ,官方也创建了一个对应的issues:github.com/flutter/flu…

里面有人提到了通过slot来解决这个问题,目前我还没有研究明白怎么处理。另外还提到了在flutter2.0上已经解决了该问题,下面我们来聊聊。


Flutter2.0上的Shadow DOM


其实issues也说了,在flutter2.0上只有Canvas Kit解决了这个问题。那么这又是什么?

之前我们在解决image跨域的问题时提到过,flutter有两种渲染模式:CanvasKit和Html(详细介绍见【flutter web:网络图片(圆形图片)、HTML renderer(解决大量图片卡)】)。


在flutter2.0之后,在浏览器中默认使用的就是CanvasKit这种渲染模式,而这种模式就不存在Shadow DOM的问题。运行后节点如下:

网络异常,图片无法展示
|


可以看到整体结构变化了,没有了Shadow DOM,所以可以直接获取到该节点,这样就不存在问题了。


但是这种模式下存在Image加载网络图片跨域的问题(同样见上面提到的文章),官方给出的解决方案是用html来代替Image,通过图片过多时要使用html render。这样就又回到了之前的问题上了,还需要通过上面的处理来解决。


其实只要解决了Image跨域的问题,还是建议最好使用Canvas Kit来渲染,因为Html Render存在不少问题,比如在debug下不停的打印日志导致非常卡等问题。


同时加载多个HtmlElementView导致失效


因为在项目中,我们可能会同时显示两个HtmlElementView,比如在直播过程(AgoraRtc)中显示动画(lottie),这样如果通过上面的处理就会出现问题。因为上面我们得到的array中都取第一个,这样其实第二个获取的节点是错误的。但是我们又无法确定哪个是第几个,怎么处理?


目前我想到的方法就是遍历,因为我们设置的每个div的id是不同的,所以通过遍历来找到一个即可,代码如下:


var lottieAnim = document.getElementById("lottie_anim");  //渲染模式如果是html,则不能直接这么获取;如果是canvas则可以
var roots = document.getElementsByTagName("flt-platform-view");
  for(var i = 0; i < roots.length; i++){
    var tmp = roots[i].shadowRoot.getElementById("lottie_anim");
    if(tmp){
      lottieAnim = tmp;
    }
  }
复制代码


这里第一步先正常获取,如果获取不到再通过flt-platform-view获取,并遍历找到节点即可。


AgoraRtc和aliplayer的处理


lottie处理起来比较简单,因为获取节点的操作是我们自己的代码,但是其他sdk就不一样了,比如AgoraRtc和aliplayer是在sdk内部获取节点的,这样我们就需要修改他们的sdk。


AgoraRtc的代码大约在4000多行,如下:


void 0 !== t.elementID ? (document.getElementById(t.elementID).appendChild(t.div), t.container = document.getElementById(t.elementID)) : (document.body.appendChild(t.div), t.container = document.body),
            t.parentNode = t.div.parentNode;
            var a = {
                video: {
                    playerId: t.playerId,
                    stateId: 0,
                    playDeferTimeout: null,
                    error: !1,
                    status: "init",
                    reason: null,
                    updatedAt: Date.now()
                },
                audio: {
                    playerId: t.playerId,
                    stateId: 0,
                    playDeferTimeout: null,
                    error: !1,
                    status: "init",
                    reason: null,
                    updatedAt: Date.now()
                }
            };
复制代码


这里document.getElementById(t.elementID)就无法正常获取,导致进入房间且已经订阅成功,但是一直不显示直播流。

通过上面的方法处理一下即可,如下:


var element = document.getElementsByTagName("flt-platform-view")[0].shadowRoot;
            void 0 !== t.elementID ? (element.getElementById(t.elementID).appendChild(t.div),
            t.container = element.getElementById(t.elementID)) : (document.body.appendChild(t.div),
            t.container = document.body),
            t.parentNode = t.div.parentNode;
            console.log(t.elementID);
            var a = {
                video: {
                   ..
                },
                audio: {
                    ...
                }
            };
复制代码


这里没有遍历,因为我们进入直播场景第一个加载的一定是直播控件,所以它一定是第一个。但是其实保险起见,还是遍历一下最好。

aliplayer就更复杂了,里面大量的使用了document,比如:


document.activeElement && document.activeElement !== e && document.activeElement.blur()
复制代码



r = document.createEvent("MouseEvents")
复制代码


这些都是无法正常执行的,因为我们的player被shadow封装,所以document访问不了。这就需要将所有的document替换成document.getElementsByTagName("flt-platform-view")[0].shadowRoot,为了方便可以新建一个全局变量方便替换。 这里也没有使用遍历,同样因为在播放场景播放器一定是第一个加载。


总结


通过最后对两个sdk的修改可以感觉到,每次这样处理并不是很好的方法,如果要更新这些sdk就需要重新维护一次。所以最好的办法就是在flutter2.0上使用CanvasKit渲染,当然前提是先解决Image跨域的问题,这个还需要后续研究。如果你的项目不涉及Image加载网络图片跨域,那么直接使用CanvasKit吧,这些js sdk不需要任何处理可以直接使用。


目录
相关文章
|
3月前
|
Dart 前端开发 Java
|
3月前
|
前端开发 JavaScript Android开发
Flutter 调用本地 web
Flutter 调用本地 web
38 0
|
4月前
|
JavaScript 前端开发
Web Components详解-Shadow DOM样式控制
Web Components详解-Shadow DOM样式控制
133 3
|
4月前
|
JavaScript API 开发者
Web Components详解-Shadow DOM插槽
Web Components详解-Shadow DOM插槽
42 1
|
4月前
|
JavaScript 前端开发 开发者
Web Components详解-Shadow DOM基础
Web Components详解-Shadow DOM基础
133 1
|
5月前
|
监控 Serverless 持续交付
阿里云云效产品使用问题之如何让流水线支持构建 flutter web 应用到 OSS
云效作为一款全面覆盖研发全生命周期管理的云端效能平台,致力于帮助企业实现高效协同、敏捷研发和持续交付。本合集收集整理了用户在使用云效过程中遇到的常见问题,问题涉及项目创建与管理、需求规划与迭代、代码托管与版本控制、自动化测试、持续集成与发布等方面。
|
5月前
|
开发框架 Dart JavaScript
深入探讨Flutter中的Web支持功能,以及如何利用Flutter构建跨平台Web应用的最佳实践
【6月更文挑战第11天】Flutter,Google的开源跨平台框架,已延伸至Web支持,让开发者能用同一代码库构建移动和Web应用。Flutter Web基于Dart转JavaScript,利用WebAssembly和JavaScript在Web上运行。构建Web应用最佳实践包括选择合适项目、优化性能、进行兼容性测试和利用Flutter的声明式UI、热重载等优势。尽管性能挑战存在,Flutter Web为跨平台开发提供了更多机会和潜力。
92 1
|
4月前
|
机器人 开发工具 Android开发
flutter web 优化和flutter_admin_template
flutter web 优化和flutter_admin_template
|
5月前
|
移动开发 小程序 安全
基础入门-APP架构&小程序&H5+Vue语言&Web封装&原生开发&Flutter
基础入门-APP架构&小程序&H5+Vue语言&Web封装&原生开发&Flutter
|
5月前
|
Dart 前端开发 JavaScript
Flutter for Web:跨平台移动与Web开发的新篇章
Flutter for Web是Google的开源UI工具包Flutter的延伸,用于构建高性能、高保真的跨平台应用,包括Web。它基于Dart语言和Flutter的核心框架,利用Skia渲染引擎通过WebAssembly在Web上运行。开发流程包括安装SDK、创建项目、编写Dart代码和部署。性能优化涉及减少渲染开销、代码压缩等。与传统Web框架相比,Flutter for Web在开发效率和性能上有优势,但兼容性和生态系统尚待完善。
83 0