本文作者: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有兩種:UIWebView和WKWebView。
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」的源代碼。在本地打開該文件,也確實有這片代碼:
從這里的注釋可以發(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)格:
由于iOS下的Promise并沒有完全支持這些特性,所以「corejs」用自己的Promise把原生的Promise覆蓋了。而且,看起來「Vue.js」對阻塞問題的修復(fù)對「corejs」的Promise無效。
解決方案
解決方案有三個:
- 不要安裝「babel-polyfill」,但這樣會造成舊版本瀏覽器下無法運行「Vuex」。
- 把UIWebView更換為WKWebView,但這不是短期內(nèi)可以完成的事情。
- 加載「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