Особенности национальной верстки: PWA

от автора

Всем привет! В этой статье мы не будем в очередной раз размышлять о том, почему бизнес и пользователи все чаще начинают отдавать предпочтение PWA вместо мобильных приложений, какие у них есть плюсы, минусы и так далее.

Сегодня мы сосредоточимся на проблемах (а точнее на одной конкретной), с которой вы можете столкнуться, решив сделать свое веб-приложение прогрессивным.

Забегая вперед, сразу скажу, кому статья может быть полезна. Не столько важно, являетесь вы владельцем продукта или разработчиком. Если вы создаете PWA-приложение, в котором планируется реализовать открытие ссылок на сторонние ресурсы в браузере, вы можете столкнуться с неочевидной проблемой в верстке. Об этом я, frontend-разработчик IT-компании SimbirSoft Эльвина, расскажу в статье.

Дисклеймер:

И да, сразу оговорюсь, что это скорее сторителлинг, никакого rocket science тут не будет. Рассказываю о том, как баг нашли, локализовали и исправили. Статью написала, чтобы опытом поделиться. Ну и мемчиками вас позабавить.

Одной из важнейших, на мой взгляд, особенностей PWA является то, что оно выглядит как стандартное мобильное приложение: имеет отдельную иконку запуска на рабочем столе / главном экране, занимает всю его площадь, позволяя нам не захламлять экранное пространство браузерными элементами управления вроде адресной строки или панели вкладок, а сосредоточиться на приложении. В конце концов, мы можем практически полностью перенести функциональности мобильного приложения в PWA. Безусловно, ограничения (а точнее — невозможность реализации определенные фичи) все еще существуют. Но на данный момент технология PWA продолжает совершенствоваться и дополняться новыми фичами, что со временем приведет к тому, что PWA будут неотличимы от мобильного приложения.

А теперь перейдем к сути.

Для пользователя PWA очень удобен – функциональность мобильного приложения, а весит в 100500 раз меньше.

Для владельца продукта есть тоже несомненный плюс – это дешево, да еще и устанавливается на разные ОС без каких-либо проблем, не нужно искать разработчиков отдельно под каждую платформу.

Для разработчика… А вот для разработчика это «веб со своими приколами», над которыми, зачастую, придется поломать голову.

Приведу пример того, над чем поломали голову мы.

Мы разрабатывали приложение bnpl-сервиса Подели, в котором пользователи могут добавлять карты для оплаты. Привязка осуществляется через эквайринг, который открывается по ссылке в браузере. После успешной (или неуспешной) привязки карты, нас должно перебросить обратно в приложение.

Это должно работать так↓

А вот что увидели мы ↓

Причем баг пропадал после того, как пользователь сворачивал и снова разворачивал приложение. Ключевым моментом во всей этой истории было то, что баг воспроизводился ТОЛЬКО в PWA, в вебе все было ок.

Первая мысль была примерно такая: у нас есть элемент «подложка», скорее всего что-то вылезло, нужно фиксануть расположение элементов после возврата, и все будет четко.

Ну как звучит, так и оценили: «Вроде изян».

Поскольку мы знали, в каком случае приложение «восстанавливает свою высоту», решение должно казалось простым – написать обертку для «условной» перезагрузки страницы. Выглядела она примерно так:

export const WithWindowPersistedCacheCheck = ({   children, }: {   children: JSX.Element; }) => {   useEffect(() => {     const handleWindowPersistCheck = (event: PageTransitionEvent) => {       if (event.persisted) {         location.reload();       }     };      window.addEventListener("pageshow", handleWindowPersistCheck);     return () => {       window.removeEventListener("pageshow", handleWindowPersistCheck);     };   }, []);    return children; };

Кажется, все просто: перезагружаем страницу в зависимости от условия, в результате высота должна «сброситься к исходному значению». Однако, это решение не сработало, высота осталась прежней. Тем не менее в будущем нам пригодится этот ХОК, так что запомним его.

Поскольку баг воспроизводился только в PWA, отладка происходила немного сложнее, чем обычно.

В этот момент мы и обнаружили самое интересное: элемент, который нам мешал, – вовсе не элемент. В этом месте вообще нет никаких элементов! Проблему обнаружили в процессе игры «Найди 10 отличий». Обратите внимание на 2 скриншота:

Да-да, высота, которую у нас якобы занимала подложка, оказалась высотой элементов управления браузером!

Собственно, мы подошли к моменту кульминации понимания нашей проблемы: при открытии внешней ссылки в браузере из PWA, при возвращении PWA «думает», что оно обычное веб-приложение и перестает занимать всю высоту экрана. Иными словами, оно резервирует место на экране под браузерные элементы управления.

Первое, с чего мы начали, – попытались вычислить высоту элементов управления браузером.

Высота панели навигации:

window.outerHeight - window.innerHeight;

Высота верхней панели:

window.screen.availHeight - window.outerHeight;

На основе этих данных мы надеялись получить недостающую высоту приложения и увеличить на это значение высоту окна браузера, получив таким образом искомую высоту PWA.

Проблема этого решения заключалась в том, что:

— во-первых, не у всех браузеров есть верхняя панель (зависит от ОС и оболочки),

— во-вторых, применив это решение, в вебе мы получили бы выпадающие элементы, которые скрывались за навигационной панелью.

Нужно было конкретно определять, находимся ли мы в режиме PWA (полноэкранный режим).

Для этого существует 2 способа:

• первый – из CSS

display-mode: standalone

• второй – из JS

navigator.standalone

Для первого варианта мы просто попытались установить высоту нашего приложения 100vh. Однако этого оказалось недостаточно, т.к. нужно было реагировать на событие перехода из приложения в эквайринг и обратно.

Для второго варианта мы попытались создать resize-хук:

export const usePWAResize = () => {   const [minAppHeight, setMinAppHeight] = useState(0);    const calculateTargetHeight = () => {     let windowHeight = window.innerHeight;     if (window.visualViewport) {       windowHeight += window.visualViewport.offsetTop;     }     if (navigator.standalone) {       windowHeight += window.outerHeight - window.innerHeight;     } else {       windowHeight +=         window.screen.height -         window.innerHeight -         window.visualViewport?.offsetTop;     }     setMinAppHeight(windowHeight);   };    useEffect(() => {     calculateTargetHeight();     window.addEventListener("resize", calculateTargetHeight);     return () => {       window.removeEventListener("resize", calculateTargetHeight);     };   }, []);    return minAppHeight; };

Из него мы получили искомую высоту приложения и подставили её инлайном к нашему элементу-обёртке.

Это даже как-то заработало, но ключевое слово тут «как-то». Как я сказала ранее, высота панелей навигации у всех браузеров разные +, а у каких-то её нет совсем. Из-за этой особенности на некоторых устройствах верстка вставала как надо, а на других что-то да выпадало.

Но самой главной проблемой было то, что это не работало на устройствах Android – в браузере этой ОС не существует свойства standalone у интерфейса navigator. Он есть только в safari на iOS (яблочники ликуют 🙂 )

Нужно было решение более простое и универсальное.

Для начала мы решили вывести все известные нам высоты в режимах PWA и веб:

screen.availHeight

screen.height

window.outerHeight

document.getElementById(“wrapper”)?.offsetHeight

Получается, что document.getElementById(“wrapper”)?.offsetHeight – высота «обертки» нашего приложения. Именно это значение и отличалось в зависимости от режима приложения, но всегда оставалось корректным.

И тут нам в голову пришла идея: если при входе в приложение мы видим корректную высоту, которая изменяется только при возвращении из браузера, почему бы нам не запомнить это значение, сохранив его где-нибудь в сессии. И потом, в случае, если высота будет отличаться от исходной, «прибивать её назад гвоздями».

Решение не элегантное, зато выглядит как то, что должно работать) Пробуем:

useEffect(() => {     const pageWrapper = document.getElementById("wrapper");     const currentHeight = pageWrapper?.offsetHeight;     let initialHeight = sessionStorage.getItem("initialHeight") || "0";     let parsedInitialHeight = initialHeight || 0;      if (initialHeight === "undefined" || initialHeight === "0") {       sessionStorage.setItem("initialWrapperHeight", JSON.stringify(currentHeight));       initialHeight = JSON.parse(         sessionStorage.getItem("initialWrapperHeight") as string       );     }     if (initialHeight !== "undefined" && initialHeight !== "0") {       parsedInitialHeight = initialHeight;     }     if (       Number(parsedInitialHeight) &&       currentHeight &&       Number(parsedInitialHeight) > currentHeight     ) {       pageWrapper?.setAttribute("style", `min-height: ${initialHeight}px`);     }   }, []);

Добавив этот эффект и обернув наш компонент в ХОК WithWindowPersistedCacheCheck, мы добились нужного нам поведения. Причем как в вебе (его эти изменения не затронули, т.к. он работал изначально корректно), так и в PWA.

Некоторые скажут, что можно было бы и забить, ведь баг не критичный. Но мы в команде Подели и SimbirSoft привыкли делать удобный и качественный продукт, чтобы пользователь видел качественную картинку  без дополнительных перезагрузок приложения.

Возможно, это решение не самое элегантное. Так что если среди наших читателей найдутся те, кто сталкивался с такой проблемой и действительно решил её более легким способом, велком в комментарии)

Спасибо за внимание!


ссылка на оригинал статьи https://habr.com/ru/articles/889642/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *