[貝聊科技]一個頁面阻塞問題的排查過程

本文做者:Mr.Luo ,貝聊前端經理,做者博客 mrluo.lifejavascript

從今年(2017年)年初起,咱們團隊開始引入「Vue.js」開發移動端的產品。在某個項目的測試過程當中,測試妹子跟咱們反饋了一個奇怪的bug:在一個播放音樂的頁面中,有一個地方同步顯示音樂的當前播放位置;音樂開始播放後,這個地方的內容會不斷改變,可是滾動頁面後,內容卻再也不變化,看起來像是某個環節被阻塞了。css

這個問題只在咱們iOS的客戶端內出現,在微信和Safari內卻毫無問題,這讓咱們一度懷疑是受到客戶端某些代碼的影響。但仔細排查事後,發現問題並無這麼簡單。html

iOS中的WebView

iOS中的WebView有兩種:UIWebViewWKWebView前端

WKWebView是從iOS 8開始提供的,除了帶來了更好的性能與更少的內存佔用外,它還改良了在UIWebView裏面的一些很差的體驗,好比scroll事件的觸發。在UIWebView內,只會在滾動徹底中止後纔會觸發scroll事件;而在WKWebView內,則是在滾動過程當中不斷觸發。java

然而,WKWebView並不是向下兼容UIWebView,更換成本不小,因此仍然有至關一部分的APP還在使用UIWebView,例如咱們貝聊的APP,以及新浪微博。webpack

即使如此,咱們讓iOS組的同事臨時用一個WKWebView打開問題頁來測試,倒是很簡單的事情。實測結果是:在WKWebView內不會有阻塞問題發生web

Demo

爲了更好地重現這個問題,咱們作了一個demo頁,關鍵代碼以下:npm

<template>
    <div>
        <audio ref="player" :src="audioURL" @timeupdate="updateTime" controls></audio>
        <div class="current-time">{{ time }}</div>
    </div>
</template>

<script> export default { data() { return { audioURL: require('./music.mp3'), time: '' }; }, methods: { updateTime() { this.time = this.$refs.player.currentTime; document.title = this.time; } } }; </script>
複製代碼

頁面功能很是簡單,播放音樂的時候,經過timeupdate事件去更新數據字段「time」的值,從而把當前播放位置不斷地更新到界面上。同時,也把「time」的值更新到頁面標題,這樣作的目的是檢查「time」的賦值是否成功。瀏覽器

用新浪微博APP打開此頁,運行效果以下:bash

運行效果

能夠看到,滾動頁面結束後,頁面內的數字再也不更新,可是標題還在繼續變化。這說明了timeupdate事件是在不斷觸發的,「time」字段的值也是在不斷更新,可是數據變化後更新到界面(刷新DOM)的過程被阻塞了。

被阻塞的實際上是...

恰巧,咱們在出現bug的產品頁中發現了另外一個現象:出現阻塞問題後,頁面中調用客戶端的功能也被阻塞了。這又讓咱們懷疑是客戶端的鍋,但後來發現並非。咱們把客戶端的功能調用都封裝成了Promise,在調試過程當中,咱們發現該Promise實例既沒法進入then的流程,也沒有進入catch的流程

咱們開始懷疑被阻塞的是Promise,因而就在demo中增長兩個按鈕「Button1」和「Button2」:

<template>
    <div>
        <audio ref="player" :src="audioURL" @timeupdate="updateTime" controls></audio>
        <div class="current-time">{{ time }}</div>
        <input type="button" value="Button1" @click="click1" />
        <input type="button" value="Button2" @click="click2" />
    </div>
</template>

<script> export default { data() { return { audioURL: require('./music.mp3'), time: '' }; }, methods: { click1() { alert('click1'); }, click2() { Promise.resolve().then(() => { alert('click2'); }); }, updateTime() { this.time = this.$refs.player.currentTime; document.title = this.time; } } }; </script>
複製代碼

就如料想的那樣,點擊播放音樂並滾動頁面後,點擊「Button1」彈出了「click 1」,可是點擊「Button2」卻沒有任何響應。這證實了被阻塞的確實就是Promise了。

罪魁禍首居然是...

找到了問題,就去搜索引擎找答案,但居然搜到了「Vue.js」的源代碼。在本地打開該文件,也確實有這片代碼:

Vue.js對阻塞問題的修復

從這裏的註釋能夠發現,「Vue.js」的開發團隊也知道Promise在UIWebView下的阻塞問題,並進行了修復,但爲何在demo頁中仍然有問題呢?

排查bug很重要的一點就是儘可能減小重現問題所需的代碼和依賴。因而,我用「Vue-CLI」初始化一個新項目,並把demo頁放到此項目中。此時再用新浪微博打開頁面進行一樣的操做,並無出現阻塞的問題。

而後,把項目中用到的「SASS」、「postcss-px2rem」、「Vuex」和「babel-polyfill」依次安裝,並在每次安裝後都從新打開demo頁進行操做。最後發現,裝完「babel-polyfill」以後問題就重現了。

babel-polyfill

iOS 8以上的Safari和WebView都已經支持Promise,可是實測發現,「babel-polyfill」會用本身的Promise覆蓋原生的Promise!查看「babel-polyfill」所依賴的「corejs」的代碼能夠發現,它對Promise的特性檢查比較嚴格:

Promise特性檢查

因爲iOS下的Promise並無徹底支持這些特性,因此「corejs」用本身的Promise把原生的Promise覆蓋了。並且,看起來「Vue.js」對阻塞問題的修復對「corejs」的Promise無效。

解決方案

解決方案有三個:

  1. 不要安裝「babel-polyfill」,但這樣會形成舊版本瀏覽器下沒法運行「Vuex」。
  2. 把UIWebView更換爲WKWebView,但這不是短時間內能夠完成的事情。
  3. 加載「babel-polyfill」後,把瀏覽器的Promise重置回原生的Promise。

考慮到那些額外的特性在實際開發中基本用不上,方案3反而是一種比較好的臨時解決方案。

先調整「babel-polyfill」的引入方式,把它的代碼文件經過其餘方式傳到服務器上。而後修改項目入口文件,也就是根目錄下的「index.html」:

<script> var _Promise; // 檢查是否iOS9+(iOS9+才支持Symbol) var useNativePromise = typeof Promise === 'function' && /^(iPhone|iPad|iPod)/.test(navigator.platform) && typeof Symbol === 'function'; if (useNativePromise) { _Promise = Promise; } </script>
<script src="//s2.imgbeiliao.com/assets/js/lib/babel-polyfill/6.23.0/polyfill.min.js"></script>
<script> if (_Promise) { Promise = _Promise; } </script>
複製代碼

上面代碼的流程就是:檢查到是iOS>=9時,就把原生Promise保存下來,待「babel-polyfill」加載執行完以後,再把保存下來的Promise覆蓋回去。那iOS<9的怎麼辦呢?測試妹子很不容易找到了一臺iOS 8的iPhone來測試,結論是不會出現阻塞問題,因此iOS<9能夠不用管了。

既然「babel-polyfill」已經過script標籤引入,那就能夠刪除對它的依賴了:

npm uninstall babel-polyfill --save
複製代碼

而後修改「/build/webpack.base.conf.js」,移除「babel-polyfill」的打包入口:

entry: {
    // app: ['babel-polyfill', './src/main.js']
    app: ['./src/main.js']
}
複製代碼

這種臨時的解決方案其實並不優雅,讓客戶端儘快更換爲WKWebView纔是正道。

後記

最近蘋果發佈了iOS 11。在iOS 11的WebView中,Promise已是徹底體,能夠經過「corejs」的特性檢查,因此不會再有這個阻塞的問題。

相關文章
相關標籤/搜索