注:以下所有例子均 只 在 iOS 的微信中測試過,但對於餓了么APP的內置瀏覽器同樣適用(兩者使用相同內核)
引題
工作中常常有需要顯示大量信息的情況,列表超出一屏就涉及到滾動的問題。例如
- var n = 1
ul
while n <= 100
li= n++
在 iOS 中用微信打開,滾動非常順滑,so far so good!但某天產品需求有變,要求加一個固定在頭部的標題,於是改成這樣:
- var n = 1
h1= "Momentum Scrolling on iOS"
ul
while n <= 100
li= n++
body, ul { margin: 0; } html, body { height: 100%; } body { display: flex; flex-direction: column; } h1 { flex-shrink: 0; } ul { flex: 1; overflow: scroll; }
直接用 flex 盒模型實現,動態適應標題的高度,很簡單不是么。但是這時在 iOS 上打開后測試,發現有問題,下半部分區域滾動起來感覺很不順滑,用老板的話說就像“卡齒輪”
這時就有大牛推薦了傳說中的神器:-webkit-overflow-scrolling: touch
ul { flex: 1; overflow: scroll; -webkit-overflow-scrolling: touch; }
很簡單的一個屬性,順滑滾動效果就回來了!雖然不太明白是怎么回事,解決問題就好。 但是產品經理又說了,需要在滾動時獲取滾動條的位置做些其他操作。太簡單了,加個 scroll
事件搞定。
document.querySelector('ul').addEventListener('scroll', function() { this.previousElementSibling.textContent = 'ScrollTop: ' + this.scrollTop; })
隨手寫好在瀏覽器中測試通過,然而在手機上測試就不太對勁:那個值是會變,然而滾動的時候不變,只有在滾動結束后變一次。
整個滾動過程中 scroll 事件只在滾動結束后會被觸發一次,問題是出在這個所謂的神器 -webkit-overflow-scrolling
上面
-webkit-overflow-scrolling 究竟是什么鬼?
一個只有 iOS 設備支持的非標准屬性。蘋果自己的解釋:指定是否在 overflow: scroll
的元素中使用“原生”的滾動方式
他包含兩個可選值:auto
和 touch
- auto:就是普通的無慣性滾動效果
- touch:原生的滾動效果。使用此效果會構造一個 stacking context
什么是 stacking context
?這可以說是CSS里一個陰暗面,極其晦澀。有興趣的朋友可以去看高人的解釋,這里不做討論(其實筆者自己也不是非常明白:cry:),總之所有的坑都是由此而起。
-webkit-overflow-scrolling 引發了那些坑?
下面列出我遇到過的坑:
滾動中 scrollTop 屬性不會變化。
嚴格來說,上面的 scroll 事件不觸發只是本坑的一個副作用,所以說不必考慮通過 touchmove
事件轉發 scroll
事件等點子,scroll
事件觸發了一樣檢測不到 scrollTop
屬性的變化(當然檢測手指的移動距離另說)。同樣,檢測滾動區域內部元素的 getBoundingClientRect
同樣無效。
例中起了一個無限的rAF循環不停地獲取 scrollTop
的值,然並卵。
手勢可穿過其他元素觸發元素滾動
這個更奇葩。例中用一個半透明的 div
蓋在了滾動區域 ul
上面(實踐中可能是一個彈框的背部蒙版),甚至給 ul
自己加上了 pointer-events: none
,手指在 div
上滑動仍然會觸發 ul
的滾動。你可以在顯示半透明蒙版時將 ul
的 -webkit-overflow-scrolling: touch
或 overflow: scroll
去掉,但是會造成屏幕明顯的閃爍。如果給 body
的 touchmove
事件 preventDefault()
可以防止觸發滾動,但是是所有滾動區域都會失效。
運行時通過 JS 動態添加元素溢出高度導致滾動失效
Google 上一搜一片,但是筆者沒有遇到過,或許在新版本系統中已經修正,這里不展開討論。
滾動時暫停其他 transition
還有沒有其他未踩的坑呢?
……
有沒有什么好的解決方案
使用 WKWebView
替換 UIWebView
內核
可能有些讀者已經發現,scroll
事件不能觸發的坑在 iOS Safari 和 iOS Chrome 瀏覽器中不存在,為什么呢?這里要從 iOS 上瀏覽器的發展史說起。
由於蘋果公司對安全性等原因的考慮,蘋果公司靜止第三方瀏覽器在 iOS 設備上使用自己的瀏覽器的內核,換句話說,使用自己內核的瀏覽器都被禁止上架 AppStore。各大廠商無奈,於是長久以來,包括 Chrome
在內的所有第三方瀏覽器,都只是使用 iOS 系統內置的瀏覽器控件包一層外殼,這個控件就是 UIWebView
。這個 UIWebView
不僅速度差,HTML5 支持率低,占用內存高,還有各種各樣奇怪的問題。然而蘋果公司卻給自己的 Safari 瀏覽器開了后門。首先 Safari 使用的支持 JIT 編譯的 JS 引擎內核 Nitro
比 UIWebView
里老舊的解釋性 JavaScriptCore
內核速度搞數倍,然后 HTML5 支持度也比 UIWebView
高,還少了某些奇葩bug。久而久之就形成了 iOS 設備上 Safari 瀏覽器全面碾壓其他第三方瀏覽器的現象。
在喬幫主撒手人寰不久之后,蘋果公司口氣終於松動,雖然沒有放開第三方瀏覽器內核的限制,但把 Safari
的瀏覽器內核提取了出來開放第三方瀏覽器使用,那就是如今的 WKWebView
(WK 即 Webkit 的縮寫)。但由於 WKWebView
只支持 iOS8 以上系統,各大瀏覽器廠商並未立刻跟進。直到最近的 iOS9 時代,Chrome 成為第一個吃螃蟹的 APP,使用了 WKWebView
內核。測試數據表明,使用 WKWebView
內核的 Chrome 瀏覽器在速度和 HTML5 支持率上已經與 Safari 瀏覽器不相上下。緊接着 Mozilla 公司宣布 Firefox 登錄 iOS 平台,使用的也是 WKWebView
內核(於是有了第一款基於 Webkit
內核的火狐瀏覽器 :)
不知蘋果做了什么手腳,也許蘋果的開發人員認為 WKWebView
的效能已經足以支撐在 scroll
事件中執行額外代碼而不造成 UI 卡頓,總之在 WKWebView
內核中滾動可以正常觸發 scroll
事件,當然也能正常獲得 scrollTop
的值。然而經過測試第二個問題仍然存在。
在這里筆者強烈建議各個 APP 遷移內嵌瀏覽器至新的 WKWebView
內核。但是就我看到的,包括微信和餓了么在內,幾乎所有的國產 APP 都還在使用 UIWebView
內核,這不得不說是一大前端開發之殤。
自己實現一套滾動邏輯
比如前段時間很火的 iScroll,筆者曾近也使用過一段時間。最后得出的結論是:iScroll 挖出的坑不比它填上的坑少,比如在 iScroll 里加個 click
事件都要小心翼翼、特別對待(因為絕大多數情況綁定 touch
事件的回調函數里第一件做的事情就是 preventDefault
)。
最值得一提的是 iScroll 的速度問題,比原生實在相差太多,在中低端安卓機型上卡頓明顯,如果還要綁定 scroll
事件做些別的事情就更卡了。
總之筆者並不建議使用。
放棄 H5,擁抱 Native
也許這才是真正的終極解決之道