Flutter Web:Shadow Root问题
Posted BennuCTech
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter Web:Shadow Root问题相关的知识,希望对你有一定的参考价值。
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,我参考的是http://quanzhan.applemei.com/webStack/TkRBeE1RPT0=
正是因为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第一行代码就可以正常显示了。
其实在网上也有很多人遇到了同样的问题,比如https://blog.csdn.net/thunder_sz/article/details/111413043 ,官方也创建了一个对应的issues:https://github.com/flutter/flutter/issues/50452 。
里面有人提到了通过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不需要任何处理可以直接使用。
关注公众号:BennuCTech,获取更多干货!
以上是关于Flutter Web:Shadow Root问题的主要内容,如果未能解决你的问题,请参考以下文章
通过 Webpack 将 CSS 样式注入到 shadow root
有人知道如何使用 selenium webdriver 识别 shadow dom web 元素吗?