CapacitorJS: кто не рискует, тот не пьет шампанское

Привет! Меня зовут Эдуард Аксамитов, я фронтенд-инженер в студии FANS. В этой статье расскажу, как на основе веб-кода мы подняли приложение для iOS при помощи CapacitorJS. Спойлер: не пытайтесь повторить это дома, трюки выполнены профессиональными каскадерами.

Предварительный рисерч, как мы выбрали подход и фреймворк

Мы в студии перезапустили платформу видеоконтента для «Синхронизации», теперь она классно работает и на десктопе, и на телефоне. Но было еще две задачки:

  • Как можно быстрее запустить приложение в App Store. Мы договорились с «Синхронизацией», что подумаем, какие есть варианты помимо нативного приложения за кучу денег.
  • Приложение на Android тоже нужно, но не срочно, поскольку у ~95% пользователей «Синхронизации» устройства на iOS.

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

Дисклеймер: если у вас есть много денег и времени на разработку — нативное приложение будет классным вариантом. Если один из пунктов не выполняется — есть альтернативы, о них и поговорим.Так что мы стали думать, как быстро запустить приложение для iOS, не разрабатывая его с нуля.

Основа «Синхронизации» — видеоконтент. Независимо от технологий, просмотр видео должен быть удобным. Как минимум, нам нужны:

  • Поддержка Picture in Picture. Это такое маленькое окошко, в котором видео продолжает играть, когда сворачиваешь приложение.
  • Поддержка оффлайн-просмотра — чтобы курс можно было скачать, например, для просмотра в самолете.

Какие есть варианты в таком случае:

  • Поддержка Picture in Picture. Это такое маленькое окошко, в котором видео продолжает играть, когда сворачиваешь приложение. Progressive Web App (PWA) — сайт на стероидах, его можно поставить на home screen. Плюсы — это по-прежнему сайт, то есть та же самая платформа, что и в вебе. Его поддерживает та же команда, и каждая фича программируется только один раз. Минусы — PWA нельзя выложить в App Store, а в случае «Синхронизации» это важный канал дистрибуции.
  • PWA в тонкой обертке, например, PWA builder. Уже можно выкладывать в App Store, но функции все равно жестко ограничены веб-платформой, а поддержка нативных фичей минимальная.
  • Hybrid App — смесь нативного приложения и UI на веб-технологиях. В теории можно подключать любые нативные функции вроде пушей, offline-storage и так далее. Примеры технологий — Ionic, CapacitorJS, Cordova (олды помнят).
  • Cross-Platform SDK — React Native или Flutter. С одной стороны, можно сделать все необходимое, с другой — нужна отдельная реализация, вариант переиспользования UI с вебом отпадает.
  • True Native Apps — сделать два нативных приложения: на Swift для iOS и на Kotlin — для Android. Органичений нет, можно на полную использовать возможности платформ. Вариант идеальный, если выкинуть из уравнения стоимость, время на изначальную разработку и ресурсы на поддержку двух приложений.

PWA и PWA в тонкой обертке не решали поставленной задачи: в PWA, установленном на iOS, не работает Picture in Picture (хотя в обычном вебе все ок), и в обоих вариантах сложно сделать надежный офлайн-просмотр. В случае с True Native Apps очень дорогая разработка. Поэтому в итоге мы выбирали между Hybrid App или Cross-Platform SDK.

Flutter и React Native — понятные варианты. Много готовых модулей, trade off известны. Выбираем технологию, нанимаем разработчиков, делаем кроссплатформенное приложение. Но нам не давала покоя мысль: «А что, если все-таки можно сделать из нашей новой веб-платформы первую версию приложения?». Профиты от этого решения колоссальные: приложение могут поддерживать фронтендеры, любая новая фича сразу появляется и вебе, и на iOS, а в перспективе — и на Android.Так что мы стали думать, как быстро запустить приложение для iOS, не разрабатывая его с нуля.

Hybrid App — экзотическая технология, риски высокие. Стоит ли играть в эту игру? Поговорили с заказчиком, посомневались, взвесили все за и против, собрали техническое демо с ключевыми фичами, и решили — кто не рискует, тот не пьет шампанское, пробуем CapacitorJS! Спойлер — у нас получилось, а с каким сложностями столкнулись по пути, расскажем дальше.

Интерфейс видео-уроков
Вот так выглядело техническое демо — прототип, на которым мы за несколько дней проверили самые сложные фичи, чтобы как можно раньше собрать все возможные грабли

Круги ада СapacitorJS (и Apple)

1. Красивая обертка — врата в ад

CapacitorJS выглядит убедительно: современный дизайн, слоган «cross-platform native runtime for web apps», внушительный список официальных плагинов. GitHub репозиторий подозрительно чист — мало открытых issues, регулярные релизы, активные мейнтейнеры. Все это создает иллюзию зрелого продукта.

Реальность раскрывается постепенно. Официальные плагины покрывают только базовый функционал: камера, storage, push-уведомления. Для всего остального приходится писать нативный код.

Issues объясняется просто: мало кто использует CapacitorJS в продакшене без всей экосистемы Ionic, а те, кто использует, решают проблемы костылями и не документируют их. Красивый лендинг продает мечту о кроссплатформенной разработке, но умалчивает, что CapacitorJS — всего лишь тонкая обертка над WebView.

2. Не платформа, а костыль для WebView

CapacitorJS позиционируется, как замена Cordova и мост между веб и нативом. На деле это просто WebView с минимальной обвязкой. Для bounce-эффекта на iOS приходится лезть в нативный код: self.bridge?.webView?.scrollView .bounces = true. Поддержка safe-areas требует CSS-костылей и молитв о совместимости со всеми устройствами.

WebView из коробки не готов к продакшену. Каждая платформа требует своих настроек, причем часть из них недокументирована. Разработка веб-приложения превращается в постоянное переключение между Xcode и Android Studio для починки платформенных особенностей.

3. Запуск проекта — квест для избранных

Команда npx cap add ios создает иллюзию простоты. На самом деле, из коробки создается только один Target в Xcode, а для разработки нужен отдельный, и начинается ручная синхронизация настроек между ними.

CapacitorJS умеет самостоятельно управлять permissions приложения, но в его плагинах Info.plist придется править руками. Ionic-расширение для VSCode генерирует иконки, создавая лишние файлы больших размеров, когда для иконок нужно всего лишь пять штук. Плюс абсурдно использовать Ionic-утилиты, когда в проекте только ядро. В итоге все равно приходится загружать ресурсы прямо в Xcode, как и в обычной нативной разработке.

4. UI/UX-пытки

Простой таб-бар внизу экрана превратился в 1500 строк Swift-кода. CapacitorJS живет в UIKit, а современная iOS-разработка — это SwiftUI. Интеграция требует UIHostingController, ручного управления constraints и надежды, что следующая версия iOS не сломает эту конструкцию.

Полноэкранный Splash Screen с edge-to-edge дизайном превращается в головоломку. Черные полосы под Status Bar, прыгающий контент при появлении клавиатуры, несовместимость с системными жестами. Финальное решение — черный div под iOS status bar и молитва, чтобы Apple не изменила высоту статус-бара в следующем iPhone.

5. Кладбище плагинов

Экосистема плагинов CapacitorJS — археологические раскопки. Большинство плагинов сообщества либо заброшены, либо обещают «iOS support is coming soon» (спойлер: не coming). В описаниях не указано, какие платформы поддерживаются — это выясняется после установки, когда приходит «Plugin isn’t implemented on iOS».

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

6. Native bridge — дырявый event bus

На бумаге API для плагинов выглядит элегантно: отправил сообщение, получил ответ. На практике это простой и неповоротливый event bus, где любая асинхронность превращается в callback hell. Документация показывает примеры в стиле «Hello, World!», но реальная логика не вписывается в эту модель.

Race conditions становятся нормой жизни. Запрос отправлен из JS, нативный код выполняется, пользователь переходит на другой экран — callback потерян. Дебаг состоит из расстановки console.log на стороне JS и нативных логов на стороне Swift, после чего начинается сопоставление временных меток в попытке понять место сбоя.

7. Платформенный ад

Каждая платформа живет по своим законам. Например, Android уменьшает WebView при появлении клавиатуры, ломая верстку с 100svh. Исправление в четыре этапа: отключить ресайз WebView, добавить слушатель клавиатуры через Capacitor API, управлять padding вручную, фиксить сломанный скролл к инпуту.

В iOS для работы с видео-плеером плагинов нет и приходится писать код на AVFoundation для HLS-стриминга, создавать нативный плеер на AVKit для оффлайн-просмотра, настраивать флаги в Xcode для picture-in-picture и фонового воспроизведения. Это решение невозможно перенести на Android — там совершенно другая архитектура. CapacitorJS обещает кроссплатформенность, но получается два разных приложения с общим HTML-интерфейсом.

8. Дебаг через страдания

Safari Web Inspector для iOS работает только на Mac, Chrome DevTools для Android теряет breakpoints после reload. Нативная часть дебажится отдельно в Xcode или Android Studio, и связать два контекста практически невозможно.

Проблемы, которые не возникают в симуляторе, но происходят на реальных устройствах, превращаются в детектив. Симулятор работает идеально, iPhone крашится. Логи доступны только при подключении кабелем к Mac. Главный инструмент дебага — alert() и надежда на лучшее.

9. App Store рулетка — финальный босс

После всех кругов ада, тонн нативного кода и исправленных багов остается последнее испытание — App Store. Эксперты пугали, что приложения на веб-технологиях могут вообще не пропустить в AppStore, мол ими слишком часто злоупотребляют нелегальные онлайн-казино. Мы с такими сложностями не столкнулись.

Единственная наша трудность с Apple — очень строгая политика в отношении приема платежей не через AppStore (без IAP). Формулировки гайдлайнов Apple написаны так, что их можно трактовать как угодно.

Guideline 3.1.3(a) обещает возможность сделать приложение без In-App Purchases, только для бесплатного контента. На практике это квест с секретными правилами, которые раскрываются методом проб и ошибок. Более 20 ревью, десятки отклонений за «недостаточно reader app», хотя из приложения вырезали больше половины. Мало убрать IAP — нужна правильная авторизация, отключенная регистрация, магические фразы в описании.

В результате долгой переписки с командой ревью Apple, мы назначили созвон и очень приятный инженер из Apple на простом и понятном русском объяснил, что делать можно, а что — нельзя. Мы реализовали его «рекомендации» и приложение сразу пропустили.

Вывод

Есть деньги и время — лучше всего натива.

Хочется сэкономить — используем react native. Ну, или flutter, if that's your thing.

Но если у вас есть готовый нормальный сайт и крутой программист, то за пару дней можно собрать приложение на CapacitorJS.

Ради бога, не пытайтесь притащить нативную навигацию и прочие нативные компоненты в WebView-приложение — это боль. Конкретно у нас разработчику было интересно, и он затащил это на чистом энтузиазме. Нам повезло, но повторять этот эксперимент не стоит!