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);@overrideState<StatefulWidget> createState() {return _lottieWidget;}void lottiePlay() {_lottieWidget.lottiePlay();}void lottieStop() {_animationListener = null;_lottieWidget.lottieStop();} }class _LottieWidget extends State<LottieWidget> {@overrideWidget 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();}@overridevoid 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问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Flutter入门:如何只关闭自身页面
- 下一篇: JDBC的入门案列以及JDBC的对事务的