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

本文作者:Mr.Luo ,貝聊前端經(jīng)理,作者博客 http://heeroluo.net 。

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

這個問題只在我們iOS的客戶端內(nèi)出現(xiàn),在微信和Safari內(nèi)卻毫無問題,這讓我們一度懷疑是受到客戶端某些代碼的影響。但仔細(xì)排查過后,發(fā)現(xiàn)問題并沒有這么簡單。

iOS中的WebView

iOS中的WebView有兩種:UIWebViewWKWebView。

WKWebView是從iOS 8開始提供的,除了帶來了更好的性能與更少的內(nèi)存占用外,它還改良了在UIWebView里面的一些不好的體驗,比如scroll事件的觸發(fā)。在UIWebView內(nèi),只會在滾動完全停止后才會觸發(fā)scroll事件;而在WKWebView內(nèi),則是在滾動過程中不斷觸發(fā)。

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

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

Demo

為了更好地重現(xiàn)這個問題,我們做了一個demo頁,關(guān)鍵代碼如下:

<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事件去更新數(shù)據(jù)字段「time」的值,從而把當(dāng)前播放位置不斷地更新到界面上。同時,也把「time」的值更新到頁面標(biāo)題,這樣做的目的是檢查「time」的賦值是否成功。

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

運行效果

可以看到,滾動頁面結(jié)束后,頁面內(nèi)的數(shù)字不再更新,但是標(biāo)題還在繼續(xù)變化。這說明了timeupdate事件是在不斷觸發(fā)的,「time」字段的值也是在不斷更新,但是數(shù)據(jù)變化后更新到界面(刷新DOM)的過程被阻塞了。

被阻塞的其實是...

恰巧,我們在出現(xiàn)bug的產(chǎn)品頁中發(fā)現(xiàn)了另一個現(xiàn)象:出現(xiàn)阻塞問題后,頁面中調(diào)用客戶端的功能也被阻塞了。這又讓我們懷疑是客戶端的鍋,但后來發(fā)現(xiàn)并不是。我們把客戶端的功能調(diào)用都封裝成了Promise,在調(diào)試過程中,我們發(fā)現(xiàn)該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」卻沒有任何響應(yīng)。這證明了被阻塞的確實就是Promise了。

罪魁禍?zhǔn)拙谷皇?..

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

Vue.js對阻塞問題的修復(fù)

從這里的注釋可以發(fā)現(xiàn),「Vue.js」的開發(fā)團隊也知道Promise在UIWebView下的阻塞問題,并進行了修復(fù),但為什么在demo頁中仍然有問題呢?

排查bug很重要的一點就是盡量減少重現(xiàn)問題所需的代碼和依賴。于是,我用「Vue-CLI」初始化一個新項目,并把demo頁放到此項目中。此時再用新浪微博打開頁面進行同樣的操作,并沒有出現(xiàn)阻塞的問題。

然后,把項目中用到的「SASS」、「postcss-px2rem」、「Vuex」和「babel-polyfill」依次安裝,并在每次安裝后都重新打開demo頁進行操作。最后發(fā)現(xiàn),裝完「babel-polyfill」之后問題就重現(xiàn)了。

babel-polyfill

iOS 8以上的Safari和WebView都已經(jīng)支持Promise,但是實測發(fā)現(xiàn),「babel-polyfill」會用自己的Promise覆蓋原生的Promise!查看「babel-polyfill」所依賴的「corejs」的代碼可以發(fā)現(xiàn),它對Promise的特性檢查比較嚴(yán)格:

Promise特性檢查

由于iOS下的Promise并沒有完全支持這些特性,所以「corejs」用自己的Promise把原生的Promise覆蓋了。而且,看起來「Vue.js」對阻塞問題的修復(fù)對「corejs」的Promise無效。

解決方案

解決方案有三個:

  1. 不要安裝「babel-polyfill」,但這樣會造成舊版本瀏覽器下無法運行「Vuex」。
  2. 把UIWebView更換為WKWebView,但這不是短期內(nèi)可以完成的事情。
  3. 加載「babel-polyfill」后,把瀏覽器的Promise重置回原生的Promise。

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

先調(diào)整「babel-polyfill」的引入方式,把它的代碼文件通過其他方式傳到服務(wù)器上。然后修改項目入口文件,也就是根目錄下的「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="http://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」加載執(zhí)行完之后,再把保存下來的Promise覆蓋回去。那iOS<9的怎么辦呢?測試妹子很不容易找到了一臺iOS 8的iPhone來測試,結(jié)論是不會出現(xiàn)阻塞問題,所以iOS<9可以不用管了。

既然「babel-polyfill」已通過script標(biāo)簽引入,那就可以刪除對它的依賴了:

npm uninstall babel-polyfill --save

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

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

這種臨時的解決方案其實并不優(yōu)雅,讓客戶端盡快更換為WKWebView才是正道。

后記

最近蘋果發(fā)布了iOS 11。在iOS 11的WebView中,Promise已經(jīng)是完全體,可以通過「corejs」的特性檢查,所以不會再有這個阻塞的問題。

本文同步發(fā)布在 https://zhuanlan.zhihu.com/p/29515460

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,257評論 4 61
  • 環(huán)縣一中 劉緒洲 師:同學(xué)們,今天我們來學(xué)習(xí)歸有光的《項脊軒志》,請大家先閱讀課文注釋①,了解相關(guān)作家作品知識。 ...
    學(xué)舟語文閱讀 3,786評論 1 1
  • 你是我在他鄉(xiāng)偶遇的失散多年的兄弟 你是一本書的封面我是封底 你是我饑餓時的糧食病中的藥 你是我的曉夢怕一睜眼你便飛去
    醒后夢不還閱讀 206評論 0 2
  • 練習(xí):與神對話 我先用了數(shù)息法和漸進法放松,然后用下樓梯法引導(dǎo)自己穿過任意門。想象自己正在走下一段白色的階梯,每走...
    EmmaJW閱讀 447評論 0 0

友情鏈接更多精彩內(nèi)容