Автор статьи: Mr.Luo, front-end менеджер Beichao, авторский блогmrluo.life.
С начала этого года (2017) наша команда начала внедрять «Vue.js» для разработки мобильных продуктов. Во время тестирования некоего проекта девушка-испытатель сообщила нам о странном баге: на странице, где проигрывается музыка, есть место, отображающее текущую позицию проигрывания музыки синхронно, после того, как музыка начинает проигрываться, содержимое этого места будет по-прежнему воспроизводиться. изменить, ноПосле прокрутки страницы, содержимое не меняется, похоже ссылка заблокирована.
Эта проблема возникает только в нашем клиенте iOS, но в WeChat и Safari такой проблемы нет, что заставило нас заподозрить, что на нее повлиял какой-то код на стороне клиента. Но после тщательного изучения я обнаружил, что проблема не так проста.
Веб-просмотр в iOS
В iOS есть два вида WebView:UIWebViewа такжеWKWebView.
WKWebView предоставляется начиная с iOS 8. Помимо повышения производительности и меньшего использования памяти, он также улучшает некоторые проблемы в UIWebView, такие как запуск событий прокрутки. В UIWebView событие прокрутки запускается только после полной остановки прокрутки; в WKWebView оно запускается постоянно во время процесса прокрутки.
Однако WKWebView не имеет обратной совместимости с UIWebView, а стоимость замены невелика, поэтому все еще существует значительное количество приложений, использующих UIWebView, таких как наше приложение Beichao и Sina Weibo.
Тем не менее, нашим коллегам из команды iOS очень просто временно открыть проблемную страницу с помощью WKWebView для тестирования. Результаты измерений:В WKWebView не будет проблем с блокировкой..
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События для обновления значения поля данных «время», чтобы постоянно обновлять текущую позицию воспроизведения в интерфейсе. В то же время значение «время» также обновляется в заголовке страницы. Цель этого — проверить, успешно ли присвоено «время».
Откройте эту страницу с помощью приложения Sina Weibo, эффект операции будет следующим:
Видно, что после прокрутки страницы цифры на странице уже не обновляются, но заголовок продолжает меняться. Это показывает, что событие timeupdate постоянно срабатывает, и значение поля «время» постоянно обновляется, но процесс обновления интерфейса (обновление DOM) после изменения данных заблокирован.
То, что заблокировано...
По стечению обстоятельств мы обнаружили еще одно явление на странице товара, где возникла ошибка: после возникновения проблемы с блокировкой также блокируется функция вызова клиента на странице. Это заставило нас заподозрить, что это вина клиента, но позже выяснилось, что это не так. Мы инкапсулируем вызовы клиентских функций в Promise.В процессе отладки мы обнаружили, что экземпляр PromiseНе может войти ни в процесс then, ни в процесс catch.
Мы начали подозревать, что заблокирован именно Promise, поэтому добавили в демо две кнопки «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>
Как и ожидалось, после нажатия для воспроизведения музыки и прокрутки страницы при нажатии кнопки «Кнопка 1» появляется «щелчок 1», но нажатие кнопки «Кнопка 2» не дает ответа. Это доказываетТо, что заблокировано, действительно обещание.
Виновником оказался...
Когда я обнаружил проблему, я пошел в поисковик, чтобы найти ответ, но нашел исходный код «Vue.js». Откройте файл локально, и там действительно есть этот фрагмент кода:
Из комментариев здесь можно узнать, что команда разработчиков «Vue.js» также знала о проблеме блокировки Promise в UIWebView и исправила ее, но почему проблема все еще существует на демонстрационной странице?
Очень важно устранять ошибкиМинимизируйте код и зависимости, необходимые для воспроизведения проблемы. Итак, я инициализировал новый проект с помощью «Vue-CLI» и поместил демо-страницу в этот проект. В настоящее время я использую Sina Weibo, чтобы открыть страницу и выполнить ту же операцию, и проблем с блокировкой нет.
Затем последовательно установите «SASS», «postcss-px2rem», «Vuex» и «babel-polyfill», используемые в проекте, и после каждой установки заново открывайте демо-страницу для работы. Наконец, я обнаружил, что проблема снова появилась после установки «babel-polyfill».
babel-polyfill
И Safari, и WebView выше iOS 8 уже поддерживают Promise, но фактическое измерение показало, что«babel-polyfill» перезапишет родной промис своим собственным промисом! Глядя на код «corejs», от которого зависит «babel-polyfill», можно увидеть, что он более строго проверяет свойства Promise:
Поскольку Promise под iOS не полностью поддерживает эти функции, «corejs» перезаписывает собственный Promise своим собственным Promise. Кроме того, похоже, что исправление «Vue.js» для проблем с блокировкой не работает для промисов «corejs».
решение
Есть три решения:
- Не устанавливайте «babel-polyfill», но это предотвратит запуск «Vuex» в старых браузерах.
- Замените UIWebView на WKWebView, но это не то, что можно сделать в краткосрочной перспективе.
- После загрузки «babel-polyfill» сбросьте обещание браузера обратно на собственное обещание.
Учитывая, что эти дополнительные функции в основном бесполезны в реальной разработке, вариант 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, сохраните родной промис, а после загрузки и выполнения «babel-polyfill» перезапишите сохраненный промис обратно. Как насчет iOS
Теперь, когда «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 как можно скорее.
постскриптум
Недавно Apple выпустила iOS 11. В WebView iOS 11 Promise уже является полным телом и может быть проверен функцией «corejs», поэтому этой проблемы с блокировкой больше не будет.