淺談Web容器設(shè)計(jì)的邊界和目標(biāo)
在移動(dòng)端項(xiàng)目的落地過(guò)程中,有很多技術(shù)方案可供選擇,如Native、Flutter、H5……但在業(yè)務(wù)中選擇哪一種技術(shù)方案,當(dāng)然是需要結(jié)合業(yè)務(wù)和技術(shù)的現(xiàn)狀和歷史沉淀來(lái)看。
就歷史沉淀而言,UC是做瀏覽器的,在對(duì)Webview優(yōu)化上的積累自然也是最多。由于UC有對(duì)瀏覽器內(nèi)核有定制優(yōu)化的能力,很多時(shí)候?qū)eb的優(yōu)化和問(wèn)題從前端側(cè)可能是很難找原因,但從內(nèi)核的“上帝視角”卻很容易找到思路和解法。在瀏覽器里面做業(yè)務(wù),只要沒有超過(guò)Web容器的能力范圍,我們一般會(huì)優(yōu)先考慮用Web技術(shù)來(lái)滿足業(yè)務(wù)的訴求。
當(dāng)然,要想準(zhǔn)確把握Webview的能力范圍并不是件容易的事,而不同人或團(tuán)隊(duì)對(duì)于Webview能力邊界的理解也是不太一樣的。當(dāng)我們?cè)谟懻撘豁?xiàng)技術(shù)的邊界時(shí),關(guān)鍵的不是了解該技術(shù)能做好什么,而是知道它做不好什么。
注意,說(shuō)的是做不好,不是做不了。很多時(shí)候,我們所說(shuō)的“做不好”指的是,達(dá)不到最優(yōu)秀的用戶體驗(yàn),通常與使用Native并經(jīng)過(guò)優(yōu)化后的效果進(jìn)行對(duì)比。在用戶體驗(yàn)競(jìng)爭(zhēng)越來(lái)越激烈的情況下,每個(gè)產(chǎn)品都期望能用最好的天花板最高的技術(shù)來(lái)落地。
但在現(xiàn)實(shí)的技術(shù)方案設(shè)計(jì)中,除了考慮技術(shù)所能達(dá)到的最優(yōu)效果外,還需要綜合考慮開發(fā)成本、開發(fā)周期、維護(hù)效率、線上風(fēng)險(xiǎn)等因素,以及根據(jù)業(yè)務(wù)發(fā)展、人力資源配置情況,綜合考慮后最終選擇一個(gè)ROI最高的方案。
好了,方案選擇的路徑不是這里討論的重點(diǎn),還是來(lái)聊聊動(dòng)態(tài)容器吧。下面筆者將列舉一些典型的用Webview技術(shù)的Cover起來(lái)有難度的場(chǎng)景。
Web 容器的邊界場(chǎng)景
一、復(fù)雜的媒體播控
1. 視頻播控
并不是說(shuō)Webview(H5)、小程序處理不了視頻,而是對(duì)于視頻的細(xì)節(jié)處理不夠好。我們都知道web原生的video播放控件功能單一,沒有快進(jìn)/退、倍速、音量調(diào)節(jié)、亮度調(diào)節(jié)、對(duì)緩沖無(wú)感知等問(wèn)題。此外,動(dòng)態(tài)容器處理視頻還有以下常見問(wèn)題:
- 自動(dòng)播放控制:主要是iOS的限制,在webview內(nèi)的video標(biāo)簽需要用戶的許可,因此雙端自動(dòng)播放邏輯是不一致的。在iOS上為了解決自動(dòng)播放的問(wèn)題,通常在body上綁一個(gè)video的play事件,用戶觸碰一下頁(yè)面就代表授權(quán)了,但這解法其實(shí)挺雞賊的,并不能代表用戶的真實(shí)意愿。
- 播放時(shí)長(zhǎng)統(tǒng)計(jì):video播放時(shí)長(zhǎng)統(tǒng)計(jì)是業(yè)務(wù)和算法的需要,如果前端業(yè)務(wù)開發(fā)者來(lái)做video播放時(shí)間統(tǒng)計(jì)會(huì)存在,在點(diǎn)擊播放時(shí)視頻資源才開始下載,什么時(shí)候視頻開始起播基本完全靠估算,播放過(guò)程中網(wǎng)絡(luò)緩沖時(shí)間也可能被統(tǒng)計(jì)到播放時(shí)長(zhǎng)內(nèi),因此時(shí)長(zhǎng)統(tǒng)計(jì)由前端業(yè)務(wù)來(lái)完成非常容易帶來(lái)統(tǒng)計(jì)誤差,業(yè)務(wù)往往是不可接受的。
- 封面無(wú)縫切視頻:由于video真正的播放視頻時(shí)間(起播時(shí)間)不精確,也帶來(lái)視頻的封面圖片替換為視頻播放畫面的過(guò)渡體感會(huì)比較生硬,如果點(diǎn)擊播放后立刻將封面圖片替換掉,而這時(shí)視頻還在加載就會(huì)出現(xiàn)黑屏效果,體感不ok。
- 播控面板UI定制:在webview內(nèi)對(duì)video的UI調(diào)整也是有難度的事情,常見的實(shí)現(xiàn)是先隱藏原生video的控制條,然后在video上覆蓋一層自定義UI,接著在自定UI上通過(guò)事件的方式來(lái)控制底下的video。這樣的封裝有很多開源的方案,但整體體驗(yàn)與Native定制的播放器有很大落差,在自有App內(nèi)幾乎不會(huì)用,站外分享才會(huì)考慮這類方案,實(shí)際也是一個(gè)不得已的選擇。
- 多視頻播控:如果在一個(gè)頁(yè)面中存在多個(gè)視頻,在播放其中視頻時(shí),需要對(duì)其他視頻進(jìn)行暫?;蜾N毀處理;如果這是一個(gè)視頻長(zhǎng)列表,產(chǎn)品期望根據(jù)用戶滾動(dòng)的位置實(shí)現(xiàn)視頻的自動(dòng)播控,那么多視頻問(wèn)題會(huì)更復(fù)雜,因此很少看到有業(yè)務(wù)使用動(dòng)態(tài)技術(shù)構(gòu)建包含視頻自動(dòng)播控的長(zhǎng)列表業(yè)務(wù)。在這個(gè)場(chǎng)景,技術(shù)要么native,要么flutter。
包含多視頻的長(zhǎng)列表滾動(dòng)到可視范圍內(nèi)自動(dòng)播放,技術(shù)以native/flutter為主
當(dāng)然,webview處理video帶來(lái)的細(xì)節(jié)體驗(yàn)問(wèn)題還有不少,有一些問(wèn)題通過(guò)native托管可以有效解決。在各大App自有業(yè)務(wù)內(nèi),對(duì)Webview的video播放器的優(yōu)化基本都走native托管的模式,在技術(shù)實(shí)現(xiàn)上叫混合渲染或同層渲染。
2. 音頻播控
音頻媒體資源的播控問(wèn)題與video是類似的,webview內(nèi)的audio標(biāo)簽也存在功能單一,沒有快進(jìn)/退、倍速、音量調(diào)節(jié)、對(duì)緩沖無(wú)感知等問(wèn)題。
在我們的業(yè)務(wù)中涉及的音頻播控場(chǎng)景比較少,目前為止包含音頻播控的場(chǎng)景主要是圖文的語(yǔ)音播報(bào)功能,由于需要對(duì)音頻播控在App保活期間全局生效,音頻播控的落地頁(yè)面自然不能使用h5的audio標(biāo)簽來(lái)處理TTS音頻,而是對(duì)接Native自定義的語(yǔ)音控制事件體系,頁(yè)面則繪制一個(gè)播控音頻的UI面板。
目前,針對(duì)音頻播控的訴求行業(yè)主要方案是Native base,F(xiàn)lutter實(shí)現(xiàn)應(yīng)該也沒問(wèn)題,Web的實(shí)現(xiàn)相對(duì)較少。目前,我們已完成了此場(chǎng)景Web化,這里需要解決的邊界問(wèn)題是在后臺(tái)模式下的頁(yè)面?;詈蜕芷谕卣?。
3. 動(dòng)圖播控
動(dòng)圖包括gif、apng、webp等,在動(dòng)態(tài)容器只有img標(biāo)簽一種圖片處理方式,在一些動(dòng)圖很多的場(chǎng)景,就很難實(shí)現(xiàn)對(duì)動(dòng)圖的播放有效控制。例如:
- 多宮格圖片的順序播放:由于不知道動(dòng)圖什么時(shí)候播放完成,在多宮格的動(dòng)圖列表中需要等待上一個(gè)動(dòng)圖播完再接著播下一個(gè)的需求是無(wú)法用現(xiàn)有img標(biāo)簽來(lái)實(shí)現(xiàn)的。在業(yè)界碰到類似的問(wèn)題,如果非要用web的方式解決,往往將動(dòng)圖轉(zhuǎn)化為video來(lái)處理,或者就直接標(biāo)記為“動(dòng)圖”讓用戶自己點(diǎn)擊播放。
實(shí)際上,業(yè)務(wù)需要?jiǎng)訄D播放可控的功能,要解決的實(shí)際問(wèn)題是動(dòng)圖可以逐幀暫停/播放、播放開始和結(jié)束都有對(duì)應(yīng)的事件,也就是需要一個(gè)功能完備的動(dòng)圖播放器,而不只是用封面替換動(dòng)圖來(lái)模擬實(shí)現(xiàn)暫停而又不知道最后一幀啥時(shí)候完成的半成品。
目前針對(duì)動(dòng)圖進(jìn)行有序播控的訴求,我們采用的是Flutter方案,本質(zhì)是Native的實(shí)現(xiàn),而在Webview容器內(nèi)可以通過(guò)類似視頻播放器一樣,通過(guò)同層渲染技術(shù)實(shí)現(xiàn)對(duì)動(dòng)圖的逐幀播控。
二、高性能的長(zhǎng)列表
1. 長(zhǎng)列表的性能問(wèn)題
在Web容器下,性能是長(zhǎng)列表面臨的最棘手問(wèn)題。問(wèn)題會(huì)表現(xiàn)為:
- 在不停下拉滾動(dòng)的過(guò)程中,當(dāng)列表插入的頁(yè)面節(jié)點(diǎn)越來(lái)越多,則會(huì)導(dǎo)致滑動(dòng)越來(lái)越卡頓(掉幀);
- 如果列表內(nèi)容過(guò)多而不做 DOM 節(jié)點(diǎn)回收或分頁(yè)處理,頁(yè)面的內(nèi)存可能占用過(guò)大,進(jìn)而導(dǎo)致頁(yè)面或 App 崩潰;
- 在做了頁(yè)面節(jié)點(diǎn)回收后會(huì)導(dǎo)致在快速滑動(dòng)的過(guò)程中,頁(yè)面節(jié)點(diǎn)回收而Dom渲染速度跟不上滾動(dòng)的速度,而導(dǎo)致用戶有明顯的白屏體感。
2. 行業(yè)方案分析
對(duì)于這些問(wèn)題,前端業(yè)界有很多解決方案,解決思路大致兩個(gè)方向:
- 針對(duì)圖片過(guò)多
圖片過(guò)多,這是引起性能問(wèn)題的最常見因素,因此在有大量圖片的頁(yè)面中,我們都會(huì)使用 lazyload 的方式按需加載圖片,長(zhǎng)列表的場(chǎng)景也是一樣的。
- 針對(duì)節(jié)點(diǎn)過(guò)多
業(yè)界普遍的解決方案是虛擬長(zhǎng)列表,根據(jù)列表容器的可視范圍,動(dòng)態(tài)計(jì)算出在可視范圍內(nèi)的列表節(jié)點(diǎn) item,然后只渲染視野邊界內(nèi)容的 item,通過(guò)控制頁(yè)面節(jié)點(diǎn)數(shù)避免內(nèi)存線性增加。
在具體的實(shí)現(xiàn)方案中,目前行業(yè)方案有:
- 給列表容器設(shè)置 height 或 padding 值撐開容器: react-virtualized& vue-virtual-scroll-list
- 通過(guò) transform 對(duì)列表 item 進(jìn)行位置偏移,同時(shí)用一個(gè)影子容器來(lái)實(shí)現(xiàn)滾動(dòng)條的模擬:ngx-virtual-scroller
- 小程序方案:微信小程序recycle-view、taro虛擬列表
當(dāng)然,還有很多,這里就不再一一列舉。
以上的幾種方案有所差異,但設(shè)計(jì)理念基本類似。在實(shí)際的業(yè)務(wù)場(chǎng)景中,當(dāng)容器內(nèi)的每個(gè) item 高度是動(dòng)態(tài)的(等高的計(jì)算邏輯相對(duì)沒那么復(fù)雜,這里不討論),在虛擬列表頁(yè)面節(jié)點(diǎn)數(shù)量增多的過(guò)程,背后存在大量的js邏輯計(jì)算:
- 在計(jì)算可視范圍內(nèi)的 item 內(nèi)容時(shí),必須需要循環(huán)遍歷每個(gè) item,獲得其相對(duì)于可視窗口的偏移量,進(jìn)而計(jì)算出需要展示的 item,以完成虛擬列表布局的需要。
- 容器的外部高度是需要在滾動(dòng)過(guò)程中,不停的進(jìn)行動(dòng)態(tài)計(jì)算并進(jìn)行重新渲染,也就是整個(gè)列表中內(nèi)部每個(gè)元素,在滾動(dòng)過(guò)程中是時(shí)時(shí)刻刻都產(chǎn)生超過(guò)可視范圍的區(qū)塊重繪。
由于獲取Dom元素的真實(shí)高度需渲染完成后才能獲得(相對(duì)Native或Flutter可以在元素layout的過(guò)程,通過(guò)layoutBuilder的回調(diào)就可以獲取其高度,無(wú)需等待元素渲染上屏),導(dǎo)致js計(jì)算列表元素高度需要等待Dom渲染,進(jìn)而到帶來(lái)不可避免的時(shí)間差。
因此,在頁(yè)面快速滾動(dòng)的過(guò)程中,虛擬長(zhǎng)列表在回收節(jié)點(diǎn)計(jì)算的過(guò)程,由于高度計(jì)算的處理邏輯需要等到Dom上屏之后,如果頁(yè)面滾動(dòng)速度越快,計(jì)算量也就越大,等待Dom上屏的時(shí)間間隔就越大,一旦頁(yè)面的滾動(dòng)速度超過(guò)一定閾值,必然出現(xiàn)可視區(qū)域內(nèi)UI的變化速度 > 渲染速度的問(wèn)題,就會(huì)表現(xiàn)為快速滾動(dòng)的頁(yè)面閃白。
3. 閃白無(wú)法量化
實(shí)際上,滾動(dòng)的白屏問(wèn)題是虛擬列表的節(jié)點(diǎn)被回收后引入的新問(wèn)題,是一個(gè)用戶的體感問(wèn)題。在這里筆者用可視區(qū)域內(nèi)的UI變化速度 > 渲染速度只是技術(shù)用語(yǔ)上的表述,尷尬的是從純技術(shù)的角度這是 一個(gè)很難用數(shù)據(jù)進(jìn)行量化的問(wèn)題 。why?
首先,這是一個(gè)新問(wèn)題。
如果我們沒有對(duì)頁(yè)面節(jié)點(diǎn)進(jìn)行回收,那么就不存在滾動(dòng)路徑上頁(yè)面沒內(nèi)容的情況,也就沒有所謂的閃白問(wèn)題。但不做節(jié)點(diǎn)回收就意味著在一個(gè)超長(zhǎng)或無(wú)限下拉的列表中,DOM 節(jié)點(diǎn)會(huì)線性增大,必然導(dǎo)致頁(yè)面占用越來(lái)越多的內(nèi)存,增加更多的排版耗時(shí),進(jìn)而影響頁(yè)面性能和用戶操控體感。
其次,為何無(wú)法數(shù)據(jù)量化?
因?yàn)樵?Webview 內(nèi),前端并不知道當(dāng)前頁(yè)面滾動(dòng)速度是多少(或者在前端不能準(zhǔn)確地用數(shù)據(jù)的方式表達(dá)),滾動(dòng)曲線和滾動(dòng)加速度在不同的手機(jī)和平臺(tái)上也是不盡相同的,因此在不同手機(jī)上發(fā)生白屏的滾動(dòng)速度閾值是不一樣的。
4. 規(guī)避快滑閃白
那么,為了平衡快速滾動(dòng)的閃白問(wèn)題,可以讓容器可以對(duì)頁(yè)面在滾動(dòng)速度的上限進(jìn)行限制,這個(gè)需要客戶端容器側(cè)來(lái)處理。
很多前端開發(fā)者應(yīng)該都知道,在老舊 iOS 系統(tǒng)上,如果 WebView 采用的 UIView 架構(gòu),由于 UIView 和 js 運(yùn)行在同一個(gè)線程,導(dǎo)致在 UIView 滾動(dòng)時(shí)會(huì)阻塞 js 執(zhí)行,因此在 UIView 的容器內(nèi)虛擬長(zhǎng)列表快速滾動(dòng)帶來(lái)的白屏問(wèn)題是不可避免的,好在現(xiàn)在的 iOS 系統(tǒng)基本上已升級(jí)為異步線程模式的 WKWebView 了。
目前,WKWebView 容器滾動(dòng)的慣性速度和加速度的上限默認(rèn)是比安卓的要低一些的(這也只是筆者對(duì)雙平臺(tái)的滾動(dòng)對(duì)比的體感,沒有量化的數(shù)據(jù)),而且iPhone手機(jī)性能通常比較好,因此虛擬長(zhǎng)列表頁(yè)面在快速滾動(dòng)中,iOS閃白體感不那么明顯。
由于安卓的 WebView 容器在快速滾動(dòng)情況下,頁(yè)面會(huì)擁有很高的慣性速度和加速度。在極端滾動(dòng)操控下,比如直接觸控拖拽滾動(dòng)條(參考以上的視頻)或通過(guò)window.scrollTo 快速定位到某個(gè)位置,JS 邏輯還來(lái)不及計(jì)算當(dāng)前滾動(dòng)到的可視區(qū)域所需展示的 item 內(nèi)容時(shí),頁(yè)面就已滾過(guò)了該區(qū)域,閃白問(wèn)題幾乎是不可避免的。
而針對(duì)極端操控的閃白問(wèn)題,可以在安卓的Web容器側(cè)禁用滾動(dòng)條的拖拽功能來(lái)規(guī)避,這并沒有在根本上解決問(wèn)題,但是webview這種機(jī)制不一定全是缺點(diǎn),在大多數(shù)場(chǎng)景下,普通速度滑動(dòng)h5的列表滑動(dòng)也是很順暢的,基本也感覺不到白屏,而技術(shù)角度看webview內(nèi)存占用會(huì)比f(wàn)lutter低,這其實(shí)也是一種優(yōu)勢(shì)。
5. 長(zhǎng)列表的優(yōu)化思路
如果業(yè)務(wù)上,不得不使用長(zhǎng)列表,在前端的優(yōu)化技巧層也是有一些方式方法的。
比如,讓列表渲染的 item 不只是可視范圍內(nèi)的 item,而是會(huì)在上下邊界部分預(yù)留足夠的buffer,這樣可以緩解問(wèn)題。在雙端的具體優(yōu)化策略上,雙端冗余buffer也可以做差異處理。在上下邊界的冗余 item 數(shù)量,iOS 可以冗余少一些(性能好,但內(nèi)存少),而安卓則多一些(設(shè)備內(nèi)存大,就多占內(nèi)存)。
或者,在需要?jiǎng)討B(tài)計(jì)算列表高度的時(shí)候?qū)⑦^(guò)程簡(jiǎn)化,比如原來(lái)需要每一個(gè)參與布局的列表item都需要計(jì)算一次,是否可以采用“分組計(jì)算”的策略,例如10個(gè)item分為1組,回收節(jié)點(diǎn)也是按組進(jìn)行,這樣回收算法的復(fù)雜度就可能只有原來(lái)的1/10。
6. 邊界的選擇
不管對(duì)長(zhǎng)列表采用怎樣的dom節(jié)點(diǎn)回收技術(shù),都必定會(huì)面臨在用戶極端操控下 UI 的變化速度 > 渲染速度問(wèn)題,在現(xiàn)有的瀏覽器JS執(zhí)行必然阻塞Dom渲染的模型下,而且Webview內(nèi)核層面定義沒有透露更多的動(dòng)態(tài)渲染處理API之前,此問(wèn)題暫時(shí)沒有徹底的解法。在對(duì)用戶體驗(yàn)較高的長(zhǎng)列表場(chǎng)景,我們傾向于選擇Flutter,如果是一級(jí)核心場(chǎng)景,主流方案還是Native。
當(dāng)然,問(wèn)題雖如此,這并不意味在h5長(zhǎng)列表中對(duì)非可視區(qū)域的節(jié)點(diǎn)進(jìn)行回收是個(gè)不必要的設(shè)計(jì),畢竟極速滾動(dòng)的閃白還是一個(gè)比較極端場(chǎng)景,很多時(shí)候產(chǎn)品對(duì)于用戶的體驗(yàn)并不沒有那么特別苛刻?;蛟S也不應(yīng)該那么苛刻,特別是人力有限的情況下,畢竟也要綜合考慮ROI,但開發(fā)者最好要知道技術(shù)邊界在哪里,避免掉坑里。
三、復(fù)雜的視差互動(dòng)
視差互動(dòng)(Parallax Effect)或滾動(dòng)(Parallax Scrolling)指操控網(wǎng)頁(yè)滾動(dòng)過(guò)程中,同時(shí)實(shí)現(xiàn)多個(gè)元素以不同的速度移動(dòng),形成立體的運(yùn)動(dòng)效果以提供出色的視覺體驗(yàn)。
1. 視差互動(dòng)的案例分析
先來(lái)看看兩個(gè)真實(shí)業(yè)務(wù)場(chǎng)景的栗子(視頻):
以上視頻的栗子是Flutter實(shí)現(xiàn)的視差效果,含有兩種視差:
- 往上推,logo需要根據(jù)上推手勢(shì)同步縮小;
- 切換tab,當(dāng)前高亮的tab背景和top橫線需要同步跟隨手勢(shì)變換。
如果用H5的方式來(lái)做這個(gè)需求,這兩個(gè)效果是做不到滑動(dòng)操控和UI變換的那種順滑體感。這個(gè)本質(zhì)原因是js是單線程的,當(dāng)js在執(zhí)行時(shí)DOM渲染會(huì)被阻塞,一幀內(nèi)要做多件事情就可能會(huì)出現(xiàn)掉幀,所以做不到絲滑體感。
以上的栗子是基于Flutter實(shí)現(xiàn)的視差,是目前比較常見的視頻播控落地頁(yè)的交互模式。在這個(gè)業(yè)務(wù)場(chǎng)景中是一個(gè)可以橫向切換的多頁(yè)容器,同時(shí)每個(gè)播放頁(yè)面又是一個(gè)可上下滾動(dòng)的嵌套容器:
- 頁(yè)面整體可以上下滾動(dòng),而下方的評(píng)論區(qū)塊則在視頻縮放到最小后可以繼續(xù)局部滾動(dòng);
- 視差效果則是上推視頻容器區(qū)塊同步縮小,播放器也隨著可播區(qū)塊的縮小而跟隨縮??;下拉,則反之。
這個(gè)栗子里面包含的邊界問(wèn)題是復(fù)雜嵌套滾動(dòng)的順滑切換,目前業(yè)界實(shí)現(xiàn)類似效果的技術(shù)方案主要是Nativa或Flutter,沒有見過(guò)H5實(shí)現(xiàn)的效果。
再看一個(gè)相同業(yè)務(wù)的Flutter與H5差異對(duì)比栗子。
以上是相同頁(yè)面的兩種技術(shù)實(shí)現(xiàn)對(duì)比,頭部的視差互動(dòng)在滑動(dòng)操控時(shí),H5有明顯的UI抖動(dòng),F(xiàn)lutter則不會(huì)。可以看到,在我們的業(yè)務(wù)中,F(xiàn)lutter實(shí)現(xiàn)了title隨著容器上推漸顯和下拉而漸隱,H5做不到順滑的漸隱漸顯過(guò)渡,降級(jí)的分享頁(yè)只能取消效果。
2. 為什么Web實(shí)現(xiàn)視差有邊界
當(dāng)我們用Webview來(lái)實(shí)現(xiàn)復(fù)雜的視差交互時(shí),為何會(huì)觸及Web的邊界?
究其原因是復(fù)雜的視差互動(dòng)大多需要通過(guò)js計(jì)算受控目標(biāo)的Dom實(shí)時(shí)位置,不斷循環(huán)“讀取dom位置→計(jì)算dom位置→改變dom位置”,如果受控目標(biāo)過(guò)多(視差效果通常是2個(gè)或以上受控目標(biāo),且每個(gè)目標(biāo)采用不同的運(yùn)動(dòng)曲線),必然會(huì)帶來(lái)js計(jì)算耗時(shí)>16.67ms進(jìn)而導(dǎo)致UI的抖動(dòng),就會(huì)給人一種互動(dòng)動(dòng)畫不順暢的體感。
在前端的業(yè)務(wù)場(chǎng)景中,復(fù)雜一些的動(dòng)畫可以采用css動(dòng)畫來(lái)實(shí)現(xiàn),順暢度會(huì)比js實(shí)現(xiàn)好很多。這是因?yàn)閏ss動(dòng)畫本質(zhì)是由渲染內(nèi)核提供的動(dòng)畫組件能力,它和js是異步的,不會(huì)因?yàn)閖s運(yùn)行而阻塞。但有一個(gè)問(wèn)題,css動(dòng)畫的運(yùn)行狀態(tài)在js側(cè)沒有感知的機(jī)制,如果用js和css混合來(lái)處理視差、動(dòng)畫會(huì)存在兩者銜接不順的問(wèn)題,這就違背了采用視差效果的初衷。
四、復(fù)雜的多tab頁(yè)面
在現(xiàn)在App業(yè)務(wù)場(chǎng)景中,多tab的頁(yè)面是非常常見的UI交互設(shè)計(jì),多tab頁(yè)面設(shè)計(jì)將相同類型的信息聚合到相同的tab內(nèi),不同的分類則按tab橫向拓展,這樣可以在有限的屏幕范圍內(nèi)盡可能多的容納更多信息。
1. 多Tab頁(yè)面的技術(shù)難點(diǎn)
圖片轉(zhuǎn)自https://developer.aliyun.com/article/791254
可以看到很多大型App的首頁(yè)都是類似的設(shè)計(jì),實(shí)現(xiàn)的技術(shù)棧是客戶端,用Web實(shí)現(xiàn)案例會(huì)比較少見。當(dāng)然,在某些App極速版中確實(shí)有基于H5實(shí)現(xiàn)的,但也會(huì)在用戶安裝App后通過(guò)動(dòng)態(tài)加載等方式將Native版本下載回來(lái)。為什么Web實(shí)現(xiàn)類似的多tab長(zhǎng)列表存在困難呢?
其實(shí)在上述的邊界問(wèn)題中也談到了其中的原因,在這個(gè)場(chǎng)景中用web實(shí)現(xiàn)的話,有以下的難點(diǎn):
- 嵌套滾動(dòng):上下可以滑動(dòng),部分局部?jī)?nèi)容滾動(dòng),外層滾動(dòng)容器與tab容器滾動(dòng)是一體化,滾動(dòng)過(guò)渡要自然;
- 長(zhǎng)列表:不同tab容器可以承載著無(wú)限列表內(nèi)容,長(zhǎng)列表的信息比較多,需要對(duì)于非可視內(nèi)容做好節(jié)點(diǎn)回收;
- tab吸頂:列表在滾動(dòng)至頂時(shí),tab欄目菜單需自動(dòng)吸頂,吸頂過(guò)渡自然(通常是某種視差特效);
- 左右滑動(dòng):手勢(shì)的左右橫滑,避免頁(yè)面邊緣退出,相鄰tab的內(nèi)容要保留可切換預(yù)覽,tab切換可預(yù)加載;
- 瀏覽記錄:不同tab容器的內(nèi)容是不相同的,在一次瀏覽周期內(nèi)要保留已瀏覽的位置記錄;
2. 多Tab的容器拓展
從技術(shù)方案上有很多實(shí)現(xiàn)的方法和思路,在UC的業(yè)務(wù)場(chǎng)景中也有很多類似的業(yè)務(wù),在這個(gè)場(chǎng)景是包含了長(zhǎng)列表、視差滾動(dòng)等邊界問(wèn)題的綜合,由于Web容器對(duì)于這邊邊界問(wèn)題缺少原生組件能力支持,業(yè)務(wù)落地的技術(shù)成本往往比較高,而且對(duì)比效果native或flutter的效果差別也比較明顯。
因此,針對(duì)多Tab列表的場(chǎng)景,在業(yè)界的技術(shù)選型中往往以native或flutter技術(shù)為主。在部分業(yè)務(wù)場(chǎng)景則通過(guò)native拓展多Tab容器的方式,每個(gè)tab則是內(nèi)嵌Webview,而native處理tab與tab之間的頁(yè)面切換效果和事件派發(fā),這樣通過(guò)容器封裝也可以實(shí)現(xiàn)Web的場(chǎng)景拓展,這就是Web容器的多page模式(也叫swiper容器)。
五、App的局部彈窗
在同一個(gè)套業(yè)務(wù)代碼里面提供一個(gè)局部彈窗,在技術(shù)上應(yīng)該是比較簡(jiǎn)單的。那么,彈窗在什么情況下會(huì)存在邊界問(wèn)題呢?讓我們先來(lái)看看幾個(gè)業(yè)務(wù)場(chǎng)景的訴求。
1. 局部彈窗的場(chǎng)景
- 場(chǎng)景1:
上面是相機(jī)萬(wàn)物識(shí)別的結(jié)果呈現(xiàn)的頁(yè)面流程。當(dāng)拍照完成后,在拍照結(jié)果的等待頁(yè)面中再?gòu)膹棾鲆粋€(gè)局部彈窗,用于展示云端的搜索結(jié)果內(nèi)容,承載內(nèi)容是一個(gè)第三方頁(yè)面(不是當(dāng)前相機(jī)業(yè)務(wù)負(fù)責(zé),而是另外的業(yè)務(wù)團(tuán)隊(duì))。頁(yè)面的容器頂部bar拖拽放大結(jié)果內(nèi)容頁(yè)面,支持分段式觸控,也就是彈窗容器高度是可變的,內(nèi)部支持局部滾動(dòng)。
- 場(chǎng)景2:
以上兩個(gè)視頻是分別在兩個(gè)不同的內(nèi)容消費(fèi)場(chǎng)景打開用評(píng)論或評(píng)論詳情,其中一個(gè)是圖文H5打開一個(gè)子彈窗,另一個(gè)是沉浸式視頻播放流中打開一個(gè)半屏彈窗。
因?yàn)槎际窃u(píng)論是通用的功能,會(huì)在很多業(yè)務(wù)場(chǎng)景中被調(diào)用,在技術(shù)上我們采用的是H5實(shí)現(xiàn),同行大多采用Native,例如今日頭條、騰訊新聞、網(wǎng)易新聞等均如此,它在代碼上不屬于當(dāng)前的業(yè)務(wù),是一個(gè)獨(dú)立演進(jìn)的業(yè)務(wù)模塊。
- 場(chǎng)景3:
這是UC帶貨直播的業(yè)務(wù)。在我們的直播容器中,上層可見的UI是由Webview實(shí)現(xiàn)的互動(dòng)層來(lái)承載,它包含很多功能實(shí)現(xiàn),例如點(diǎn)贊、禮物互動(dòng)、彈幕列表、商品卡片、福利活動(dòng)、作者關(guān)注等,所有動(dòng)態(tài)運(yùn)營(yíng)能力基本都基于互動(dòng)層來(lái)實(shí)現(xiàn),行業(yè)的主要方案是native為主,而采用Webview來(lái)實(shí)現(xiàn)在技術(shù)上是一種新的嘗試。
在底部的購(gòu)物車按鈕打開的是當(dāng)前直播中的商品列表,在原有的產(chǎn)品設(shè)計(jì)中不屬于當(dāng)前互動(dòng)業(yè)務(wù)團(tuán)隊(duì),而是由直播電商團(tuán)隊(duì)負(fù)責(zé),所以是一個(gè)二方頁(yè)面的局部頁(yè)面。后來(lái)改成了我們通過(guò)接口獲取,但考慮到互動(dòng)層的復(fù)雜度,我們沿用了獨(dú)立頁(yè)面的技術(shù)實(shí)現(xiàn)。
2. 局部彈窗的邊界問(wèn)題
以上,所有的彈窗功能有一個(gè)共同特點(diǎn),就是不屬于當(dāng)前業(yè)務(wù),也就是由A業(yè)務(wù)調(diào)用B業(yè)務(wù),實(shí)現(xiàn)一個(gè)局部的功能界面效果。在業(yè)務(wù)劃分上,可能是不同的業(yè)務(wù)團(tuán)隊(duì)負(fù)責(zé),都是H5技術(shù)棧的話,業(yè)務(wù)實(shí)現(xiàn)所采用的的前端框架大概率是不一樣的。
那么,這里的邊界問(wèn)題是——在兩個(gè)相互隔離的js代碼倉(cāng)庫(kù)中,如何實(shí)現(xiàn)一個(gè)局部的彈窗,而且該彈窗是一個(gè)二方提供的具體實(shí)現(xiàn)?
技術(shù)上,被調(diào)用的局部彈窗可以采用js-sdk的方式提供對(duì)外服務(wù),業(yè)務(wù)調(diào)用方通過(guò)動(dòng)態(tài)注入JS到當(dāng)前頁(yè)面的頁(yè)面中來(lái)實(shí)現(xiàn)。但這就會(huì)帶來(lái)以下的問(wèn)題:
- 業(yè)務(wù)耦合:需要兩個(gè)業(yè)務(wù)是相互解耦的,兩個(gè)的代碼不能產(chǎn)生相互干擾;
- 版本迭代:被注入到業(yè)務(wù)中的二方j(luò)s-sdk需要考慮版本更新問(wèn)題,是加載具體的js-sdk前調(diào)用一個(gè)api來(lái)決定最新版本,還是提供一個(gè)覆蓋式發(fā)布的js-sdk(不管什么版本都是同一個(gè)url,想想就知道有很大的坑)呢?
- 加載性能:將一個(gè)第三方j(luò)s注入到業(yè)務(wù)中,本質(zhì)是一個(gè)異步j(luò)s模塊注入的問(wèn)題,業(yè)務(wù)方需要解決好性能的問(wèn)題。
以上這些問(wèn)題其實(shí)純前端的方式都是不好解決的,因此,這樣的局部彈窗更合理的設(shè)計(jì)是一個(gè)獨(dú)立Web頁(yè)面,獨(dú)立頁(yè)面的好處是避免了業(yè)務(wù)耦合和版本迭代兩個(gè)問(wèn)題,只需要解決加載性能問(wèn)題就可以了。但在具體的技術(shù)實(shí)現(xiàn)上,難道用iframe的方式設(shè)計(jì)這個(gè)彈窗嗎?
很顯然,大多數(shù)前端開發(fā)者都不太愿意采用這樣的設(shè)計(jì),因?yàn)閕frame不好用。因此,這樣的局部彈窗頁(yè)面需要從客戶端角度提供定制化的擴(kuò)展,這就是Web容器的半屏模式,也叫局部容器模式。
六、Web 容器邊界的本質(zhì)
以上舉例了5個(gè)典型的Web邊界問(wèn)題,哪些是可以在容器范圍內(nèi)進(jìn)行突破的呢?當(dāng)然這里所說(shuō)的邊界突破,指的是原來(lái)做不了或做得不夠好的,通過(guò)對(duì)容器的封裝而獲得解決的。
實(shí)際上,容器邊界能力的突破還是需要Native同學(xué)通過(guò)容器進(jìn)行組件/API定制,給前端提供私有組件的方式來(lái)解決標(biāo)準(zhǔn)Web容器的能力局限。從Web瀏覽器的渲染流程上,只要不涉及需要挑戰(zhàn)瀏覽器渲染線程模型的,從原理上可以通過(guò)容器封裝來(lái)解決。
例如媒體播控、局部彈窗,通過(guò)Web容器擴(kuò)展私有組件或API能力是可以比較好的實(shí)現(xiàn)邊界拓展的,但長(zhǎng)列表、視差和復(fù)雜多tab等場(chǎng)景所面臨的本質(zhì)問(wèn)題都是一致的,都需要在足夠小的時(shí)間片段內(nèi)完成“讀取Dom位置(大小)→計(jì)算Dom位置(大小)→改變Dom位置(大小)”,如果這個(gè)時(shí)間片段超過(guò)16.67ms(一幀)都會(huì)面臨體驗(yàn)不夠好的問(wèn)題。
如果想將時(shí)間耗時(shí)壓縮在足夠小的范圍內(nèi),而 JS作為動(dòng)態(tài)解析型語(yǔ)言的執(zhí)行效率在計(jì)算下一幀Dom位置的耗時(shí)就可能會(huì)成為瓶頸,而Web標(biāo)準(zhǔn)沒有在layou階段提供獲取Dom位置大小信息的能力,則進(jìn)一步加劇了需要?jiǎng)討B(tài)計(jì)算目標(biāo)Dom位置的等待時(shí)長(zhǎng) 。
因此,此類問(wèn)題在現(xiàn)有的Web范圍內(nèi)是不太好解決的,而解決它可能需要調(diào)整瀏覽器的渲染流程和架構(gòu),這樣的技術(shù)成本還不如用其他技術(shù)來(lái)得更加實(shí)在,技術(shù)方案又不是只有Web一種選擇,不是嗎?
七、Web 容器設(shè)計(jì)的目標(biāo)
以上筆者列舉了許多Web的邊界問(wèn)題,好像都是說(shuō)動(dòng)態(tài)方案做不好的或做不了的,對(duì)前端而言是不是太過(guò)悲觀了呢?顯然,這并不是筆者的初衷,而是期望通過(guò)了解技術(shù)實(shí)現(xiàn)的邊界,才能更好的推動(dòng)Web容器進(jìn)行完善和向前發(fā)展。
筆者認(rèn)為,對(duì)于Web容器的封裝目標(biāo)是—— 在容器范圍內(nèi)解決原有Web標(biāo)準(zhǔn)容器解決不了的邊界問(wèn)題,并通過(guò)容器的標(biāo)準(zhǔn)化封裝提升對(duì)復(fù)雜問(wèn)題的解決效率,讓W(xué)eb容器能夠覆蓋更多的業(yè)務(wù)場(chǎng)景,進(jìn)而能夠拓展前端的業(yè)務(wù)空間 。這里的關(guān)鍵點(diǎn)有:
1. 解決Web的邊界問(wèn)題
這是一個(gè)很重要的出發(fā)點(diǎn),但凡標(biāo)準(zhǔn)能夠解決的問(wèn)題,其實(shí)就不需要一個(gè)自定義的容器(輪子)來(lái)加持。邊界的問(wèn)題其實(shí)跟業(yè)務(wù)類型和場(chǎng)景有很大關(guān)系,不同的業(yè)務(wù)觸及的邊界是不一樣的,在不同的發(fā)展階段業(yè)務(wù)對(duì)于邊界的體感或認(rèn)知也不盡相同。
不過(guò),是不是所有的邊界問(wèn)題都能通過(guò)容器來(lái)解決呢?顯然不是,有些邊界可能是在現(xiàn)有Web容器范圍內(nèi)就是無(wú)法突破的,該Native的就Native,因此需要開發(fā)者對(duì)于邊界問(wèn)題的有更精準(zhǔn)和更深刻的認(rèn)識(shí)。
2. 解決Web不夠標(biāo)準(zhǔn)的問(wèn)題
很多時(shí)候一個(gè)App應(yīng)用需要在不同的平臺(tái)上提供服務(wù),不同平臺(tái)的實(shí)現(xiàn)或UI展現(xiàn)本來(lái)就存在差異,而這種差異對(duì)產(chǎn)品而言可能是不可接受的,那么可以將這種不一致的問(wèn)題在容器層面進(jìn)行封裝或拓展。
這樣的能力差異挺多的,例如輸入面板、分享面板、日夜間適配、web容器樣式(如前進(jìn)、后退、關(guān)閉、收藏等)、跨頁(yè)通訊、容器push/pop的動(dòng)畫、worker調(diào)度和通訊,等等……這些應(yīng)該是容器封裝要解決的基本內(nèi)容,有一部分是web標(biāo)準(zhǔn)的延伸或拓展(漸進(jìn)增強(qiáng)),也有部分是針對(duì)私域業(yè)務(wù)的定制優(yōu)化。
3. 提升復(fù)雜問(wèn)題的解決效率
復(fù)雜問(wèn)題有很多,舉個(gè)通俗易懂的栗子。例如性能優(yōu)化,這對(duì)于每個(gè)業(yè)務(wù)都是一個(gè)復(fù)雜的問(wèn)題。
做過(guò)移動(dòng)H5性能優(yōu)化的同學(xué)都應(yīng)該知道,純前端視角是很難將性能優(yōu)化到極致的,最多就是通過(guò)服務(wù)端實(shí)現(xiàn)SSR、ER邊緣渲染,但這并不是最極致的性能優(yōu)化手段。在App里面做性能優(yōu)化,極致的優(yōu)化手段還包括離線包、數(shù)據(jù)預(yù)取、圖片預(yù)取、NSR預(yù)渲染、頁(yè)面預(yù)執(zhí)行、端智能調(diào)度等優(yōu)化手段。
當(dāng)然,復(fù)雜問(wèn)題的解決往往也是一個(gè)技術(shù)成本高的事情,如何將復(fù)雜的、成本高的事情簡(jiǎn)單化、標(biāo)準(zhǔn)化、動(dòng)態(tài)化,這也是容器設(shè)計(jì)的重要目的。
4. 拓展前端的業(yè)務(wù)空間
這應(yīng)該是Web容器封裝的最終目標(biāo)。當(dāng)然,一個(gè)新的Web容器方案,除了性能、交互體驗(yàn)?zāi)軌驖M足業(yè)務(wù)需求外,前端的開發(fā)體驗(yàn)也需要得到保障,配套的工程效率工具方案也應(yīng)該是一個(gè)Web容器建設(shè)的重要內(nèi)容,而這往往容易被容器封裝的Owner所忽略的。
因此,Web容器框架應(yīng)該以業(yè)務(wù)為起點(diǎn),前端開發(fā)者需要深度參與其中,這樣才能提出和推動(dòng)更符合前端視角的訴求落地,而不只是被動(dòng)接受客戶端的封裝實(shí)現(xiàn)。