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

貝聊科技發表於2019-03-02

本文作者:Mr.Luo ,貝聊前端經理,作者部落格 mrluo.life

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

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

iOS中的WebView

iOS中的WebView有兩種:UIWebViewWKWebView

WKWebView是從iOS 8開始提供的,除了帶來了更好的效能與更少的記憶體佔用外,它還改良了在UIWebView裡面的一些不好的體驗,比如scroll事件的觸發。在UIWebView內,只會在滾動完全停止後才會觸發scroll事件;而在WKWebView內,則是在滾動過程中不斷觸發。

然而,WKWebView並非向下相容UIWebView,更換成本不小,所以仍然有相當一部分的APP還在使用UIWebView,例如我們貝聊的APP,以及新浪微博。

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

Demo

為了更好地重現這個問題,我們做了一個demo頁,關鍵程式碼如下:

<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開啟此頁,執行效果如下:

執行效果

可以看到,滾動頁面結束後,頁面內的數字不再更新,但是標題還在繼續變化。這說明了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」的特性檢查,所以不會再有這個阻塞的問題。

相關文章