От автора: 30 марта состоялся долгожданный релиз iOS 11.3 с поддержкой базовых функций PWA на iPhone и iPad – сервис воркеры и манифест файлы. Хорошо, наконец-то, получить поддержку этих функций, но UX PWA на iOS все еще не идеален. Это значит, что множество выпущенных PWA iOS до сих пор имеют серьезные проблемы на устройствах Apple, а на Android работают почти, как родные приложения. Во front end сообществе уже высказали свое недовольство, список багов и проблем длинный. Но не опускайте руки! Ниже собрано несколько советов, как решить эти проблемы и максимально приблизить PWA к обычным приложениям.
Что не так с поддержкой PWA на iOS?
В последнем обновлении iOS Apple добавила поддержку сервис воркеров и манифеста приложения. Теперь можно использовать кэширование с сервис воркерами и заставить PWA работать без интернет соединения. Давайте вспомним – это базовое требование в определении PWA. К сожалению, в реализации Apple есть несколько минусов.
По сравнению с Android, поддержка сервис воркеров сильно ограничена. Вы лишь можете сохранять данные приложения и кэшировать его файлы (никаких фоновых задач). У хранилища ограничение в 50Мб и «пару недель».
iOS 11.3 также представила поддержку манифест файла. Но наши тесты показали, что она далека от идеальной. Иконки работают плохо (или вообще не работают), нет поддержка экрана запуска – при загрузке приложения у вас просто пустой белый экран. Приложение перезагружается каждый раз, когда возвращается из фонового режима, нет поддержка push уведомлений и других функций, которые необходимы для мобильного приложения. В общем – общий UX очень плохой.
И что? На iOS невозможно сделать PWA похожими на родные приложения?
К счастью, можно многое сделать, чтобы улучшить внешний вид приложения и обеспечить лучший UX. Пара простых трюков поможет создать приложение, в большинстве случаев не отличимое от родного.
Я считаю, что PWA – это будущее инклюзивного, быстрого и интуитивного веба. Поэтому хочу поделиться с вами некоторыми советами о том, как обойти ограничения iOS для PWA.
Про совет №1: сделаем иконку приложения снова великой (на всех устройствах)
Мы столкнулись с тем, что iOS не использует иконки из манифеста и сильно портит иконку приложения на домашнем экране. Обойти проблему очень легко – просто добавьте мета тег apple-touch-icon с подходящим изображением. Не подкладывайте иконки с прозрачностью, не сработает.
<!-- place this in a head section --> <link rel="apple-touch-icon" href="touch-icon-iphone.png"> <link rel="apple-touch-icon" sizes="152x152" href="touch-icon-ipad.png"> <link rel="apple-touch-icon" sizes="180x180" href="touch-icon-iphone-retina.png"> <link rel="apple-touch-icon" sizes="167x167" href="touch-icon-ipad-retina.png">
Теперь ваше приложение выглядит идеально с самого начала.
Про совет №2: поправьте экран запуска
Экран запуска отображается перед тем, как приложение полностью загрузится и будет готово к использованию. К сожалению, iOS не поддерживает экран запуска из манифеста, как на Android. Вместо этого просто отображается белый экран. Это определенно не то, что мы хотим показать нашим пользователям.
К счастью, мы нашли решение на странице apple developer. Apple поддерживает кастомные мета теги для установки сгенерированных заставок — apple-touch-startup-image. То есть вам просто нужно сгенерировать изображение заставки подходящего размера. Размеры можно посмотреть ниже:
Как только сделаете красивые экраны запуска, останется лишь подключить их в head:
<!-- place this in a head section --> <meta name="apple-mobile-web-app-capable" content="yes" /> <link href="/apple_splash_2048.png" sizes="2048x2732" rel="apple-touch-startup-image" /> <link href="/apple_splash_1668.png" sizes="1668x2224" rel="apple-touch-startup-image" /> <link href="/apple_splash_1536.png" sizes="1536x2048" rel="apple-touch-startup-image" /> <link href="/apple_splash_1125.png" sizes="1125x2436" rel="apple-touch-startup-image" /> <link href="/apple_splash_1242.png" sizes="1242x2208" rel="apple-touch-startup-image" /> <link href="/apple_splash_750.png" sizes="750x1334" rel="apple-touch-startup-image" /> <link href="/apple_splash_640.png" sizes="640x1136" rel="apple-touch-startup-image" />
Страшный белый экран пропал:
Благодаря этому совету, наше PWA выглядит намного лучше, правда?
Про совет №3: создайте «добавить на домашний экран»!
В Android есть встроенное всплывающее меню, предлагающее пользователю добавить приложение на домашний экран и информирующее его о том, что страница является PWA. В iPhone, к сожалению, такого нет. То есть пользователи даже не узнают о возможностях приложения. Более того, на iOS нужно минимум 3 нажатия, чтобы добавить приложение на домашний экран.
Не волнуйтесь, у нас есть фикс! Моно добавить кастомное всплывающее меню, которое будет показывать, что приложение можно добавить на домашний экран.
Дизайн всплывающего меню можете сделать любой. Ниже показан наш пример. Самое сложное – отобразить меню только в Safari и не в автономном режиме (когда приложение уже добавлено на домашний экран). Проверить автономный режим приложения можно с помощью window.navigator.standalone.
Взгляните на код:
// Detects if device is on iOS const isIos = () => { const userAgent = window.navigator.userAgent.toLowerCase(); return /iphone|ipad|ipod/.test( userAgent ); } // Detects if device is in standalone mode const isInStandaloneMode = () => ('standalone' in window.navigator) && (window.navigator.standalone); // Checks if should display install popup notification: if (isIos() && !isInStandaloneMode()) { this.setState({ showInstallMessage: true }); }
После создания подходящего компонента всплывающего меню и вставки кода обнаружения в подходящий метод жизненного цикла это будет выглядеть так:
Не забывайте, что на iPad кнопка совместного доступа расположена вверху экрана, рядом с адресной строкой. Не забудьте поменять положение меню.
Про совет №4: сохраняйте все
iOS перезапускает приложение при каждом запуске/уходе в фоновый режим. Поэтому нужно позаботиться о сохранении состояния. Если вы используете React и Redux, есть несколько замечательных пакетов — redux-persist и react-router-redux. Для Vue можно использовать похожие vuex-persist и vuex-router-sync. Конечно, можно создать и свое решение, наилучшим образом подходящее под ваши нужды.
В React react-router-redux по умолчанию редиректит на подключение к роуту, указанному в URL. Для обычного использования это нормально, но нам лучше брать роут, сохраненный в хранилище. Пример, как это сделать:
import { ConnectedRouter, push } from 'react-router-redux'; class PersistedConnectedRouter extends ConnectedRouter { componentWillMount() { const { store: propsStore, history, isSSR } = this.props; this.store = propsStore || this.context.store; if (!isSSR) { this.unsubscribeFromHistory = history.listen(this.handleLocationChange); } //this is the tweak which will prefer persisted route instead of that in url: const location = this.store.getState().router.location || {}; if (location.pathname !== history.location.pathname) { this.store.dispatch(push(location.pathname)); } this.handleLocationChange(history.location); // -- } } export default PersistedConnectedRouter;
Про совет №5: продумайте навигацию
Чтобы вашим приложением можно было пользоваться в автономном режиме, нужно убедиться, что реализована правильная навигация. На устройствах Apple нет кнопки назад, поэтому нужно предусмотреть, чтобы пользователи могли возвращаться с любого экрана с помощью встроенной в приложение навигации.
Для этого можно отобразить кнопку назад или добавить дополнительное меню на iOS.
Про совет №6: подготовьтесь к офлайну
Чтобы обеспечить настоящий нативный UX, нужно быть готовым к плохому соединению или его отсутствию. В некоторых простых случаях хватит сохранения данных. Но здесь же можно реализовать новый (на iOS) Service Workers API – там можно кэшировать сетевые запросы (например, API запросы).
Для подключения кэширования через сервис воркер можно использовать пример кода от Google. Обязательно прочтите статью, чтобы знать плюсы и минусы этого шаблона.
// source: https://googlechrome.github.io/samples/service-worker/basic/ /* Copyright 2016 Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Names of the two caches used in this version of the service worker. // Change to v2, etc. when you update any of the local resources, which will // in turn trigger the install event again. const PRECACHE = 'precache-v1'; const RUNTIME = 'runtime'; // A list of local resources we always want to be cached. const PRECACHE_URLS = [ 'index.html', './', // Alias for index.html 'styles.css', '../../styles/main.css', 'demo.js' ]; // The install handler takes care of precaching the resources we always need. self.addEventListener('install', event => { event.waitUntil( caches.open(PRECACHE) .then(cache => cache.addAll(PRECACHE_URLS)) .then(self.skipWaiting()) ); }); // The activate handler takes care of cleaning up old caches. self.addEventListener('activate', event => { const currentCaches = [PRECACHE, RUNTIME]; event.waitUntil( caches.keys().then(cacheNames => { return cacheNames.filter(cacheName => !currentCaches.includes(cacheName)); }).then(cachesToDelete => { return Promise.all(cachesToDelete.map(cacheToDelete => { return caches.delete(cacheToDelete); })); }).then(() => self.clients.claim()) ); }); // The fetch handler serves responses for same-origin resources from a cache. // If no response is found, it populates the runtime cache with the response // from the network before returning it to the page. self.addEventListener('fetch', event => { // Skip cross-origin requests, like those for Google Analytics. if (event.request.url.startsWith(self.location.origin)) { event.respondWith( caches.match(event.request).then(cachedResponse => { if (cachedResponse) { return cachedResponse; } return caches.open(RUNTIME).then(cache => { return fetch(event.request).then(response => { // Put a copy of the response in the runtime cache. return cache.put(event.request, response.clone()).then(() => { return response; }); }); }); }) ); } });
Достаточно задать верные пути к файлам, которые необходимо кэшировать, и зарегистрировать сервис воркер. После этого вы готовы к офлайну. В такие моменты лучше показывать, что приложение находится в офлайн режиме – можно добавить обработчики событий online/offline и показывать подходящее уведомление.
componentDidMount() { window.addEventListener('online', () => this.setOnlineStatus(true)); window.addEventListener('offline', () => this.setOnlineStatus(false)); } componentWillUnmount() { window.removeEventListener('online'); window.removeEventListener('offline'); } setOnlineStatus = isOnline => this.setState({ online: isOnline })
Конечный результат нашего примера приложения:
Проблемы, которые еще необходимо решить
Эти советы помогут обеспечить UX для PWA на iOS, близкий к родным приложениям. Но есть проблемы, которые еще нужно решить. У нас все еще нет поддержки:
Фоновой синхронизации
Push уведомлений
Некоторых API типа TouchID, ARKit, In App Payments, Split View на iPad
Блокировки ориентации
Изменения цвета статусбара
Подходящего экрана приложения в таск менеджере (который показывает текущий экран приложения, а не заставку)
Не стоит забывать, что кэш и локальные данные не распространяются на Safari, WebView и Web.App. Поэтому после добавления приложения на домашний экран пользователю опять нужно в нем авторизоваться, что в некоторых случаях ухудшает UX.
Автор: Maciej Caputa
Источник: https://www.netguru.co/
Редакция: Команда webformyself.