本文作者:Mr.Luo 举畸,貝聊前端經(jīng)理,作者博客 http://heeroluo.net 凳枝。
從今年(2017年)年初起抄沮,我們團(tuán)隊(duì)開始引入「Vue.js」開發(fā)移動(dòng)端的產(chǎn)品。在某個(gè)項(xiàng)目的測(cè)試過程中岖瑰,測(cè)試妹子跟我們反饋了一個(gè)奇怪的bug:在一個(gè)播放音樂的頁(yè)面中叛买,有一個(gè)地方同步顯示音樂的當(dāng)前播放位置;音樂開始播放后蹋订,這個(gè)地方的內(nèi)容會(huì)不斷改變率挣,但是滾動(dòng)頁(yè)面后,內(nèi)容卻不再變化露戒,看起來像是某個(gè)環(huán)節(jié)被阻塞了椒功。
這個(gè)問題只在我們iOS的客戶端內(nèi)出現(xiàn),在微信和Safari內(nèi)卻毫無(wú)問題智什,這讓我們一度懷疑是受到客戶端某些代碼的影響动漾。但仔細(xì)排查過后,發(fā)現(xiàn)問題并沒有這么簡(jiǎn)單荠锭。
iOS中的WebView
iOS中的WebView有兩種:UIWebView和WKWebView旱眯。
WKWebView是從iOS 8開始提供的,除了帶來了更好的性能與更少的內(nèi)存占用外节沦,它還改良了在UIWebView里面的一些不好的體驗(yàn)键思,比如scroll事件的觸發(fā)。在UIWebView內(nèi)甫贯,只會(huì)在滾動(dòng)完全停止后才會(huì)觸發(fā)scroll事件吼鳞;而在WKWebView內(nèi),則是在滾動(dòng)過程中不斷觸發(fā)叫搁。
然而赔桌,WKWebView并非向下兼容UIWebView,更換成本不小渴逻,所以仍然有相當(dāng)一部分的APP還在使用UIWebView疾党,例如我們貝聊的APP,以及新浪微博惨奕。
即便如此雪位,我們讓iOS組的同事臨時(shí)用一個(gè)WKWebView打開問題頁(yè)來測(cè)試,卻是很簡(jiǎn)單的事情梨撞。實(shí)測(cè)結(jié)果是:在WKWebView內(nèi)不會(huì)有阻塞問題發(fā)生雹洗。
Demo
為了更好地重現(xiàn)這個(gè)問題香罐,我們做了一個(gè)demo頁(yè),關(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>
頁(yè)面功能非常簡(jiǎn)單时肿,播放音樂的時(shí)候庇茫,通過timeupdate事件去更新數(shù)據(jù)字段「time」的值,從而把當(dāng)前播放位置不斷地更新到界面上螃成。同時(shí)旦签,也把「time」的值更新到頁(yè)面標(biāo)題,這樣做的目的是檢查「time」的賦值是否成功寸宏。
用新浪微博APP打開此頁(yè)宁炫,運(yùn)行效果如下:
可以看到,滾動(dòng)頁(yè)面結(jié)束后氮凝,頁(yè)面內(nèi)的數(shù)字不再更新淋淀,但是標(biāo)題還在繼續(xù)變化。這說明了timeupdate事件是在不斷觸發(fā)的覆醇,「time」字段的值也是在不斷更新朵纷,但是數(shù)據(jù)變化后更新到界面(刷新DOM)的過程被阻塞了。
被阻塞的其實(shí)是...
恰巧永脓,我們?cè)诔霈F(xiàn)bug的產(chǎn)品頁(yè)中發(fā)現(xiàn)了另一個(gè)現(xiàn)象:出現(xiàn)阻塞問題后袍辞,頁(yè)面中調(diào)用客戶端的功能也被阻塞了。這又讓我們懷疑是客戶端的鍋常摧,但后來發(fā)現(xiàn)并不是搅吁。我們把客戶端的功能調(diào)用都封裝成了Promise,在調(diào)試過程中落午,我們發(fā)現(xiàn)該P(yáng)romise實(shí)例既無(wú)法進(jìn)入then的流程谎懦,也沒有進(jìn)入catch的流程。
我們開始懷疑被阻塞的是Promise溃斋,于是就在demo中增加兩個(gè)按鈕「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>
就如料想的那樣界拦,點(diǎn)擊播放音樂并滾動(dòng)頁(yè)面后,點(diǎn)擊「Button1」彈出了「click 1」梗劫,但是點(diǎn)擊「Button2」卻沒有任何響應(yīng)享甸。這證明了被阻塞的確實(shí)就是Promise了。
罪魁禍?zhǔn)拙谷皇?..
找到了問題梳侨,就去搜索引擎找答案蛉威,但竟然搜到了「Vue.js」的源代碼。在本地打開該文件走哺,也確實(shí)有這片代碼:
從這里的注釋可以發(fā)現(xiàn)蚯嫌,「Vue.js」的開發(fā)團(tuán)隊(duì)也知道Promise在UIWebView下的阻塞問題,并進(jìn)行了修復(fù),但為什么在demo頁(yè)中仍然有問題呢择示?
排查bug很重要的一點(diǎn)就是盡量減少重現(xiàn)問題所需的代碼和依賴妒牙。于是,我用「Vue-CLI」初始化一個(gè)新項(xiàng)目对妄,并把demo頁(yè)放到此項(xiàng)目中。此時(shí)再用新浪微博打開頁(yè)面進(jìn)行同樣的操作敢朱,并沒有出現(xiàn)阻塞的問題剪菱。
然后,把項(xiàng)目中用到的「SASS」拴签、「postcss-px2rem」孝常、「Vuex」和「babel-polyfill」依次安裝,并在每次安裝后都重新打開demo頁(yè)進(jìn)行操作蚓哩。最后發(fā)現(xiàn)构灸,裝完「babel-polyfill」之后問題就重現(xiàn)了。
babel-polyfill
iOS 8以上的Safari和WebView都已經(jīng)支持Promise岸梨,但是實(shí)測(cè)發(fā)現(xiàn)喜颁,「babel-polyfill」會(huì)用自己的Promise覆蓋原生的Promise!查看「babel-polyfill」所依賴的「corejs」的代碼可以發(fā)現(xiàn)曹阔,它對(duì)Promise的特性檢查比較嚴(yán)格:
由于iOS下的Promise并沒有完全支持這些特性半开,所以「corejs」用自己的Promise把原生的Promise覆蓋了。而且赃份,看起來「Vue.js」對(duì)阻塞問題的修復(fù)對(duì)「corejs」的Promise無(wú)效寂拆。
解決方案
解決方案有三個(gè):
- 不要安裝「babel-polyfill」,但這樣會(huì)造成舊版本瀏覽器下無(wú)法運(yùn)行「Vuex」抓韩。
- 把UIWebView更換為WKWebView纠永,但這不是短期內(nèi)可以完成的事情。
- 加載「babel-polyfill」后谒拴,把瀏覽器的Promise重置回原生的Promise尝江。
考慮到那些額外的特性在實(shí)際開發(fā)中基本用不上,方案3反而是一種比較好的臨時(shí)解決方案英上。
先調(diào)整「babel-polyfill」的引入方式茂装,把它的代碼文件通過其他方式傳到服務(wù)器上。然后修改項(xiàng)目入口文件善延,也就是根目錄下的「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時(shí)少态,就把原生Promise保存下來,待「babel-polyfill」加載執(zhí)行完之后易遣,再把保存下來的Promise覆蓋回去彼妻。那iOS<9的怎么辦呢?測(cè)試妹子很不容易找到了一臺(tái)iOS 8的iPhone來測(cè)試,結(jié)論是不會(huì)出現(xiàn)阻塞問題侨歉,所以iOS<9可以不用管了屋摇。
既然「babel-polyfill」已通過script標(biāo)簽引入,那就可以刪除對(duì)它的依賴了:
npm uninstall babel-polyfill --save
然后修改「/build/webpack.base.conf.js」幽邓,移除「babel-polyfill」的打包入口:
entry: {
// app: ['babel-polyfill', './src/main.js']
app: ['./src/main.js']
}
這種臨時(shí)的解決方案其實(shí)并不優(yōu)雅炮温,讓客戶端盡快更換為WKWebView才是正道。
后記
最近蘋果發(fā)布了iOS 11牵舵。在iOS 11的WebView中柒啤,Promise已經(jīng)是完全體,可以通過「corejs」的特性檢查畸颅,所以不會(huì)再有這個(gè)阻塞的問題担巩。
本文同步發(fā)布在 https://zhuanlan.zhihu.com/p/29515460