Архитектура и данные

PWA или нативная оболочка: как совместить веб-приложение с Capacitor для Android и iOS

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

Нужно ли делать отдельное мобильное приложение?

На первый взгляд есть два противоположных пути.

Первый:

Web
↓
PWA

Оставить продукт веб-приложением, добавить manifest, Service Worker, установку на домашний экран и offline-возможности.

Второй:

Web
↓
Native shell
↓
Android / iOS

Упаковать веб-приложение в мобильную оболочку и распространять через мобильные платформы.

Часто эти варианты обсуждают как взаимоисключающие:

Или PWA, или мобильное приложение.

В одном из наших проектов мы пришли к другой архитектуре.

В приложении одновременно существуют:

обычная веб-версия
+
PWA
+
Service Worker
+
Capacitor 8
+
Android
+
iOS

При этом основная бизнес-логика интерфейса не переписывается три раза.

Получается одна web-first система, у которой есть несколько способов доставки пользователю:

                    Web application
                         │
            ┌────────────┼────────────┐
            │            │            │
            ▼            ▼            ▼
         Browser        PWA       Capacitor
                                      │
                                ┌─────┴─────┐
                                ▼           ▼
                             Android       iOS

Такой подход оказался интереснее простой дискуссии «что лучше».

Потому что PWA и Capacitor решают разные задачи.

Разберём, зачем поддерживать оба варианта, где действительно можно использовать общий код, а где различия платформ всё равно приходится учитывать.


Сначала определимся: что даёт PWA

Progressive Web App остаётся веб-приложением.

Пользователь может открыть его обычной ссылкой:

https://example.ru

и сразу начать работу.

Никакого магазина приложений.

Никакой предварительной установки.

Но при наличии Web App Manifest поддерживающий браузер может предложить установить приложение на устройство. После установки оно получает иконку и может запускаться в более самостоятельном режиме, похожем на обычное приложение. Service Worker часто используется для offline-сценариев, кеширования и фоновых возможностей.

Получаем очень сильное свойство:

ссылка
↓
открыли
↓
работаем

А уже потом:

понравилось приложение
↓
установили

Для продукта это означает минимальный порог входа

Представим сервис планирования или личный кабинет.

Пользователю отправляют ссылку:

example.ru

Он не должен сначала:

найти приложение;
открыть магазин;
скачать;
дождаться установки;
открыть;

Можно сразу показать продукт.

Это особенно полезно для:

SaaS;
CRM;
B2B-кабинетов;
сервисов планирования;
внутренних приложений;
контентных платформ.

PWA сохраняет главное преимущество Web — мгновенное распространение версии

Мы обновляем сервер.

Пользователь при следующем корректном обновлении приложения получает новую web-версию.

Не требуется отдельно публиковать каждое изменение через мобильный store.

Хотя Service Worker добавляет собственную задачу — управление кешем и жизненным циклом обновления.

Если сделать его неправильно, можно получить неприятную ситуацию:

Backend:
version 3.5

Server frontend:
version 3.5

User Service Worker cache:
version 3.4

То есть сервер уже новый, а пользователь продолжает работать со старым JavaScript.


Поэтому PWA — это не просто manifest и кнопка «Установить»

Production PWA требует подумать о:

cache version;
Service Worker lifecycle;
offline fallback;
update strategy;
старых cache;
network failures.

Сам Service Worker становится отдельным инфраструктурным компонентом frontend.


Что тогда даёт Capacitor

Capacitor решает другую задачу.

Это native runtime для web-first приложений: существующий HTML/CSS/JavaScript интерфейс можно запускать внутри нативного Android/iOS приложения и при необходимости обращаться к native API через плагины. В актуальной документации Capacitor 8 официально поддерживаются Android, iOS и Web как целевые платформы.

Условно:

Web code
   ↓
Capacitor
   ↓
Native WebView
   ↓
Android / iOS

Но важное слово здесь — не WebView.

А native bridge.


JavaScript получает доступ к возможностям устройства

Например приложению могут понадобиться:

камера;
файловая система;
геолокация;
нативные уведомления;
haptics;
status bar;
share sheet;
нативный lifecycle.

Capacitor предоставляет плагинный слой между web-кодом и платформой, а при необходимости можно написать собственный native plugin. Официальная документация прямо позиционирует Capacitor как web-first runtime с доступом к нативным SDK.


Значит ли это, что PWA больше не нужна?

Нет.

Это первое важное архитектурное решение.

Capacitor добавляет ещё один канал доставки.

Он не обязан заменять web.

Можно сохранить:

Web / PWA

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

И одновременно:

Android / iOS

для пользователей, которым нужен привычный установленный mobile app.


Получается один продукт с разными входами

Например:

Desktop user
    ↓
Browser
    ↓
Web

Android browser user
    ↓
PWA

Android app user
    ↓
Capacitor Android

iPhone browser user
    ↓
Web / installed web app

iPhone app user
    ↓
Capacitor iOS

На backend все они могут обращаться к одному API.


Архитектура выглядит примерно так

                      Backend API
                          │
                   PostgreSQL / etc.
                          │
              ┌───────────┼───────────┐
              │                       │
              ▼                       ▼
         Browser/PWA             Native apps
              │                       │
              │                  Capacitor 8
              │                   /        \
              ▼                  ▼          ▼
        Service Worker       Android       iOS

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


Но «одна кодовая база» нужно понимать правильно

Очень легко представить:

Написали сайт — нажали кнопку — получили Android и iOS.

В реальности всё немного сложнее.

Общий слой действительно может быть очень большим:

UI;
domain logic;
API client;
формы;
валидация;
состояние приложения;
большая часть offline logic.

Но Capacitor также создаёт полноценные нативные проекты:

android/
ios/

которые имеют:

permissions;
native configuration;
signing;
store metadata;
platform resources.

То есть правильнее говорить:

общий web-core + platform-specific adapters и native projects.


Мы стараемся не писать так

if (android) {
  // 400 строк
}

if (ios) {
  // ещё 350
}

if (web) {
  // третий вариант
}

Если платформенные проверки начинают проникать во всю бизнес-логику, общий код быстро превращается в смесь трёх приложений.


Лучше создать platform layer

Например:

src/
├── domain/
├── ui/
├── api/
│
└── platform/
    ├── notifications.js
    ├── files.js
    ├── share.js
    ├── haptics.js
    └── lifecycle.js

Бизнес-код спрашивает:

platform.notifications.requestPermission()

а не:

Если iOS — делай одно, если Android — второе, если PWA — третье.

Например интерфейс уведомлений

export async function
requestNotifications() {
  // platform-specific implementation
}

Web adapter может использовать web-возможности.

Native adapter — Capacitor plugin.

UI не обязан знать техническую реализацию.


Capacitor умеет определять текущую платформу

В API есть разделение между:

web
ios
android

а также проверка, запущено ли приложение как native Capacitor app или как Web/PWA. Это позволяет локализовать platform branching в небольшом количестве мест вместо распространения if по всему приложению.

Концептуально:

if (Capacitor.isNativePlatform()) {
  // native capability
} else {
  // browser / PWA
}

Но даже такую проверку лучше прятать внутри platform adapter, если она начинает повторяться.


Почему PWA и Capacitor хорошо сосуществуют

Потому что возможности можно разделить на несколько уровней.

Уровень 1 — универсальный Web

Работает почти везде:

личный кабинет;
дневник;
задачи;
формы;
аналитика;
статьи;
профиль;
настройки.

Это основа продукта.


Уровень 2 — PWA enhancements

Например:

installable experience;
manifest;
offline shell;
Service Worker caching;
web push там, где поддерживается;
background web capabilities.

Приложение остаётся сайтом, но становится удобнее как установленный web app.


Уровень 3 — Native enhancements

Например:

native notifications;
native filesystem;
device APIs;
нативный share;
native lifecycle;
платформенные permissions.

Они доступны через Capacitor.


Это и есть progressive enhancement на уровне продукта

Базовый сценарий:

работает в браузере.

Браузер поддерживает PWA:

получаем установку и дополнительные web-возможности.

Пользователь использует native app:

добавляем device integration.

Но основная бизнес-ценность остаётся одной.


Очень важно не делать native feature обязательным для базовой функции без причины

Представим приложение позволяет добавить фотографию.

На Android/iOS можно использовать:

native Camera plugin.

Но Web-версия всё равно может позволить:

<input type="file">

То есть:

Native:
камера напрямую

Web:
выбор/загрузка файла

Пользователь решает одну задачу разными средствами.


Это лучше, чем отключать функцию полностью

Плохо:

Если не Capacitor
→ функция недоступна.

Хорошо:

есть native capability?
→ используем лучший native UX

нет?
→ web fallback.

Capability важнее названия платформы

Не всегда нужно спрашивать:

Это Android?

Гораздо полезнее:

Доступна ли нужная возможность?

Например:

if (
  Capacitor.isPluginAvailable('Camera')
) {
  // native path
} else {
  // web fallback
}

Capacitor предоставляет такую проверку plugin availability именно для подобных сценариев.


Теперь самый интересный вопрос — Service Worker

В PWA Service Worker имеет понятную роль:

Browser
   ↓
Service Worker
   ↓
Cache / Network

Например:

app shell;
offline fallback;
статические assets;
controlled caching.

В native build модель уже другая

Capacitor использует собранные web-assets внутри native приложения; конфигурация Capacitor задаёт webDir — каталог с финальными web-файлами, включая index.html, которые используются нативным проектом.

Поэтому нативному приложению не нужно рассчитывать на Service Worker для того, чтобы получить базовый HTML/JS shell:

Native package
   ↓
bundled web assets
   ↓
WebView

Frontend уже находится внутри установленного приложения.


Это значит, что offline нужно разделить на два понятия

Очень часто говорят:

Capacitor работает offline, потому что файлы находятся в приложении.

Это только половина истины.

Да, можно открыть локальный application shell.

Но нужны ли пользователю данные?

Например:

список задач;
записи дневника;
проект;
сообщения.

Если всё это приходит только из API:

Shell offline ✓

Useful data offline ✗

Настоящий offline-first требует отдельной модели данных

Например:

Local state
    ↓
local persistence
    ↓
user edits offline
    ↓
network returns
    ↓
sync
    ↓
server

Capacitor сам по себе эту задачу не решает.

PWA Service Worker тоже не решает автоматически.

Он может кешировать network responses и assets, но бизнесовая синхронизация конфликтующих пользовательских данных — отдельная архитектурная задача.


Поэтому мы разделяем

Offline shell

и:

Offline business data.

Это разные уровни.


Service Worker не должен становиться обязательной частью native logic

Если Service Worker создан для PWA, мы бы не строили критическую Android/iOS-функцию на предположении:

В WebView всё будет работать точно так же, как в Chrome PWA.

Native runtime имеет собственный lifecycle и собственные API.

Критичные native-сценарии лучше реализовывать через явно поддерживаемые механизмы Capacitor.


Это особенно касается push-уведомлений

С точки зрения пользователя функция называется одинаково:

«Уведомить меня»

Но техническая доставка в Web/PWA и в native app может быть разной.


Поэтому хороший abstraction выглядит так

NotificationService
        │
   ┌────┴────┐
   ▼         ▼
 Web       Native
 push      push

Backend при этом может работать с общей концепцией:

notification subscriptions

но знать тип endpoint:

web
android
ios

Не нужно притворяться, что delivery transport одинаковый

Это важное правило гибридной архитектуры.

Можно унифицировать business intent:

Напомнить пользователю о задаче.

Но техническая доставка может быть платформенной.


То же относится к permissions

На Web пользователь видит один UX запроса разрешения.

На Android — другой.

На iOS — третий.

Нативные платформы требуют собственных permission/configuration layers; в iOS, например, определённым native API нужны Usage Descriptions в Info.plist.

Значит permission flow нужно проектировать как пользовательский сценарий.

Не просто:

requestPermission()

при первом открытии приложения.


Лучше объяснить пользователю, зачем разрешение нужно

Например:

Хотите получать напоминания
о запланированных задачах?

После явного действия:

[Включить уведомления]

уже вызывается platform permission.

Это работает и UX-wise, и архитектурно чище.


Следующая важная разница — обновления

У PWA и native shell совершенно разный release lifecycle.


Web/PWA

Условно:

Developer
↓
build
↓
server deploy
↓
Service Worker update
↓
users gradually receive new version

Команда контролирует deployment напрямую.


Capacitor

Для изменения packaged native приложения обычно требуется:

web build
↓
Capacitor sync
↓
Android/iOS build
↓
sign
↓
mobile release
↓
user installs/update

И это уже отдельный release channel.


В итоге одновременно могут существовать разные frontend versions

Например:

Web:
4.8

Android:
4.7

iOS:
4.6

Backend при этом:

API:
5.1

Это один из главных архитектурных вызовов.


Backend должен учитывать version skew

Очень опасно проектировать API так:

Deploy backend 5.1
↓
старый mobile client
перестаёт работать

Пользователь может не обновлять приложение неделями.


Поэтому API желательно изменять совместимо

Например было:

{
  "name": "Project A"
}

Новому клиенту нужно дополнительное поле:

{
  "name": "Project A",
  "archived": false
}

Добавить поле обычно безопаснее, чем внезапно заменить:

name

на:

projectName

и удалить старое.


Это делает backward compatibility особенно важной

Web можно обновить почти сразу.

Native users — нет.

Значит:

API contract

становится точкой совместимости нескольких поколений клиентов.


Иногда полезно передавать client version

Например:

X-App-Platform: android
X-App-Version: 4.7.2

или эквивалент через API payload/telemetry.

Это помогает диагностировать:

Ошибка только у iOS 4.6?

или:

Проблема у всех клиентов?

Но сервер не должен превращаться в тысячу условий

Плохо:

if version < 3.2 ...
if version < 3.7 ...
if android 4.1 ...
if ios 4.2 ...

Бесконечная поддержка старых контрактов тоже создаёт технический долг.

Нужна version support policy.


Второй release-вызов — кеш PWA

Web release может быть уже новым.

Но Service Worker пользователя ещё удерживает старые assets.

Теперь одновременно:

Web user A:
4.8

PWA user B:
4.7 cached

Android:
4.7

iOS:
4.6

И все идут к одному API.

Вот почему управление версиями — не декоративная задача.


Намного безопаснее считать frontend клиентом API

Не:

Frontend и backend всегда обновляются одновременно.

А:

Backend должен пережить разумное окно существования старых клиентов.

Это особенно важно, если продукт одновременно PWA и native.


На Web всё просто:

https://example.ru/day/2026-10-01

Пользователь открывает URL.

В native app хочется получить:

ссылка
↓
открывается приложение
↓
тот же экран.

Хорошо, когда route model общая

Например domain route:

/day/2026-10-01

понимает и browser router, и native deep-link adapter.

Не нужно создавать:

web URL

и совершенно отдельную систему native navigation IDs, если в этом нет необходимости.


Это одно из преимуществ web-first architecture

URL уже является хорошим идентификатором состояния интерфейса.

Его можно использовать как основу:

Browser routing
+
Deep link mapping

Но native lifecycle отличается от браузерного

Например приложение:

было открыто;
ушло в background;
вернулось;

или:

было полностью закрыто;
пользователь нажал deep link.

Это разные сценарии.

Native adapter должен уметь передать событие основному router.


То же самое с кнопкой Back

Browser history:

Back

и Android system back gesture/button — не всегда одно и то же.

Если просто оставить web navigation как есть, можно получить странный UX:

Android Back
→ закрывает приложение

хотя пользователь ожидал:

вернуться на предыдущий экран.

Так появляются небольшие platform adapters, которые нельзя увидеть при обычной desktop-разработке.


Safe Area — ещё один пример

Web на desktop:

top: 0

означает верх экрана.

На современном телефоне есть:

status bar;
notch;
dynamic island;
system bars.

UI должен учитывать safe areas.

В Capacitor 8 были обновлены подходы к edge-to-edge на Android, и документация рекомендует использовать соответствующие CSS env()/safe-area переменные для корректной компоновки интерфейса.

Это хороший пример того, почему:

Mobile responsive website

и:

Production native shell

всё-таки не полностью одинаковая задача.


Ещё один важный слой — клавиатура

На desktop форма выглядит идеально.

На телефоне открывается keyboard.

И внезапно:

нижняя кнопка исчезла;
modal перестала прокручиваться;
active input оказался под keyboard.

Эти проблемы часто не проявляются в desktop responsive mode.


Поэтому native app нужно тестировать на реальных устройствах

Минимум:

Android
iOS

И желательно несколько классов экранов.


Мы не считаем тест Chrome DevTools полноценной проверкой Android

Responsive mode отлично помогает проверить layout.

Но он не моделирует полностью:

native WebView;
permissions;
system keyboard;
safe areas;
application lifecycle;
native plugins;
store build.

Получается настоящая test matrix

Например:

СценарийBrowserPWAAndroidiOS
Login✓✓✓✓
Offline shell—✓✓✓
Core data✓✓✓✓
File upload✓✓✓✓
Notificationsplatformplatformnativenative
Deep linkURLURL✓✓
Background/restorebrowserbrowser✓✓
Upgrade old versionwebSWstorestore

Слово:

«кроссплатформенное»

не означает:

«достаточно протестировать один Chrome».

Но тестировать всё четыре раза тоже не обязательно

Хорошая архитектура позволяет разделить тесты.


Общие domain tests

Один раз:

задачи;
дневник;
валидация;
расчёты;
state machine.

Web integration tests

Проверяют:

browser;
PWA;
Service Worker;
manifest;
cache update.

Native smoke tests

Проверяют именно границу:

Capacitor starts;
native plugins;
permissions;
safe area;
deep links;
background/resume.

Так мы не дублируем весь test suite без необходимости.


CI/CD тоже лучше разделить на ветви

Например:

                     Web source
                         │
                         ▼
                       Build
                         │
              ┌──────────┼──────────┐
              │          │          │
              ▼          ▼          ▼
             Web      Android      iOS
              │          │          │
          deploy       build       build
              │          │          │
        PWA tests    native QA    native QA

Одна ошибка web-core ломает все платформы.

Но platform-specific проблема остаётся локальной.


Это позволяет сохранить главное преимущество общего кода

Исправили:

расчёт;
форму;
историю задач;
API client.

Изменение автоматически становится частью следующей версии:

Web
Android
iOS.

Не нужно реализовывать одну business-функцию три раза.


При этом release timing может различаться

Web:

сегодня.

Android:

после mobile build/release.

iOS:

после своего release lifecycle.

Поэтому:

один source

не означает:

одновременный release.

Это нужно учитывать архитектурно.


Что хранить локально

Ещё один важный вопрос гибридного приложения.

На Web можно использовать:

IndexedDB;
Cache Storage;
localStorage

в зависимости от задачи.

В native приложении могут появиться дополнительные device storage возможности.


Не стоит позволять каждой платформе создать свою бизнес-модель

Плохо:

PWA data format A

Android data format B

iOS data format C

Через год поддерживаются уже три системы синхронизации.


Лучше иметь одну domain serialization

Например:

{
  "id": "...",
  "updatedAt": "...",
  "data": {}
}

А platform storage adapter решает:

где физически сохранить.

Особенно это важно для offline-first

Например пользователь изменил запись без сети.

Нам нужны:

local value;
server value;
base value;
revision;
conflict strategy.

Эта логика должна быть общей.

А не:

Android sync algorithm
+
PWA sync algorithm.

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


Capacitor — не замена backend

Иногда native shell создаёт ощущение:

Теперь приложение находится на телефоне, сервер нужен меньше.

Нет.

Если продукт использует:

аккаунты;
синхронизацию;
общие данные;
AI;
push;
cloud backup;

backend по-прежнему остаётся центральной частью системы.

Capacitor меняет клиент.

Не всю архитектуру продукта.


И наоборот: PWA не делает backend необязательным

Service Worker способен помочь offline.

Но он не превращает сложную SaaS-систему в полностью автономную программу.


Следующая тема — безопасность

Web и native app всё равно считаются клиентами.

Нельзя рассуждать:

Запрос пришёл из нашего Android приложения, значит ему можно доверять.

Пользователь контролирует своё устройство.

API всё равно должен проверять:

authentication;
authorization;
ownership;
validation.

Native shell не заменяет server-side permissions

Например UI скрывает:

[Удалить проект]

на Android.

Злоумышленник всё равно может сформировать HTTP-запрос самостоятельно.

Поэтому все критичные проверки остаются на backend.


Secrets тоже нельзя просто положить в JavaScript bundle

Web assets native приложения можно анализировать.

Поэтому:

database password;
private backend key;
master encryption secret

не должны становиться частью frontend только потому, что frontend упакован в APK/IPA.


Capacitor даёт дополнительные native-возможности, но не делает JavaScript сервером

Это важная граница.


А что с SEO?

Здесь двойная стратегия даёт интересное преимущество.

Native Android/iOS приложение само по себе не заменяет публичный Web.

Web/PWA остаётся доступным по обычным URL:

https://example.ru

То есть публичные страницы могут:

индексироваться;
получать organic traffic;
использоваться в рекламе;
получать внешние ссылки.

Пользователь приходит из поиска

Например:

Google / Яндекс
↓
public web page
↓
web application

После знакомства с продуктом он может:

продолжить в браузере;
установить PWA;
установить native app.

Получается хорошая acquisition funnel.


Если сделать только native app

Органический search entry уже приходится решать отдельным маркетинговым сайтом.


Поэтому для web-first продукта сохранение Web часто очень полезно

Особенно если SEO — один из каналов привлечения.


Когда достаточно только PWA

Не каждому проекту нужен Capacitor.

Например приложение:

в основном формы;
таблицы;
контент;
личный кабинет;
задачи;

и практически не требует native APIs.

Если пользователи спокойно устанавливают PWA и store presence не является бизнес-требованием, дополнительная Android/iOS инфраструктура может не окупиться.


PWA особенно сильна, если важны

моментальный вход по ссылке;
быстрые обновления;
SEO;
один deployment;
минимальная стоимость mobile support.

Когда Capacitor становится интересным

Например нужны:

App Store / mobile distribution;
нативные уведомления;
device APIs;
filesystem;
camera;
platform integrations;
более привычный mobile installation flow.

И при этом приложение уже имеет качественный web-interface.

Тогда Capacitor позволяет использовать накопленный web-код вместо полного переписывания продукта на две отдельные native codebase.


Когда поддержка обоих вариантов особенно оправдана

Есть несколько сильных условий.

1. У продукта уже есть web-аудитория

Нельзя сказать пользователям:

Теперь приложение существует только в store.

Web остаётся важным каналом.


2. SEO важно для привлечения

Публичный сайт и web pages должны продолжать жить.


3. Часть пользователей хочет обычное mobile app

Для них:

иконка;
store;
native notifications;
device integration

имеют ценность.


4. Большая часть UI и business logic универсальна

То есть Capacitor действительно позволяет переиспользовать значительную часть приложения.


5. Native features являются enhancement, а не полностью другим продуктом

Например:

основной дневник

одинаков.

А native добавляет:

notification;
device integration.

Это очень хороший сценарий.


Когда гибрид становится плохой идеей

Представим приложение почти полностью состоит из:

AR;
сложной 3D-графики;
Bluetooth;
background sensors;
heavy native media processing.

А Web-часть — только небольшой экран.

В таком случае web-first architecture может уже не давать главного преимущества.


Нельзя выбирать Capacitor только чтобы поставить сайт в store

Если мобильный UX плохой:

мелкие кнопки;
desktop layout;
неудобные формы;
неработающий back;
сломанная keyboard;

обёртка не исправит это автоматически.

Получится:

плохой мобильный сайт
в native package.

Сначала приложение должно быть действительно mobile-friendly

И только потом native shell усиливает его.


Ещё один анти-паттерн — две независимые frontend codebase

Например:

web/
mobile/

которые в первый день были одинаковыми.

Через год:

Web:
feature A/B/C/D

Mobile:
A/C/E

iOS:
ещё отдельные fixes.

Главное преимущество Capacitor исчезло.


Поэтому мы стараемся держать различия на периферии

                   Common domain/UI
                         │
              ┌──────────┼──────────┐
              │          │          │
          Web adapter Android adapter iOS adapter

Не:

Web app
Android app
iOS app

как три отдельных продукта.


Platform adapters нужно делать маленькими

Например:

notifications
files
share
device info
lifecycle

Но:

task creation;
journal rules;
user profile;
analytics calculations

не должны становиться platform-specific.


Хороший тест архитектуры

Спросите:

Если завтра полностью убрать Capacitor, продолжит ли основная бизнес-логика работать в браузере?

Если:

да,

web-core действительно независим.

Если половина domain импортирует:

@capacitor/...

напрямую, граница уже начала разрушаться.


Импорты Capacitor полезно локализовать

Например:

platform/capacitor/

а не разбросать:

@capacitor/core

по сотне UI-компонентов.


Второй хороший тест

Если понадобится заменить Capacitor plugin одним собственным native plugin, сколько файлов изменится?

Если:

один adapter,

архитектура хорошая.

Если:

сорок экранов,

platform boundary слишком слабая.


Как обращаться с Service Worker

Мы бы рассматривали его как отдельный Web/PWA adapter.

Например:

platform/
├── native/
│   ├── notifications
│   └── lifecycle
│
└── web/
    ├── service-worker
    ├── pwa-install
    └── web-push

Service Worker не должен напрямую определять domain rules.


Например плохой вариант

если Service Worker cache имеет запись X,
значит задача выполнена.

Cache — delivery/storage mechanism.

Статус задачи — business state.

Их нельзя смешивать.


Service Worker особенно важно тестировать при обновлениях

Например:

Install old PWA
↓
cache v12
↓
deploy v13
↓
reload/update
↓
старый cache удалён
↓
v13 работает

Не только:

новый browser profile
↓
v13 работает.

Проблемы PWA чаще обнаруживаются именно у существующих пользователей.


Для native shell существует аналогичный upgrade test

Android v12 installed
↓
update to v13
↓
local data remains
↓
session works
↓
new schema compatible

То же:

iOS.

То есть нужно тестировать не только installation

Но и:

upgrade.

В production это часто более важный сценарий.


Особенно если меняется local storage schema

Например старый клиент хранит:

{
  "tasks": [...]
}

Новый:

{
  "tasksById": {}
}

Просто установить новую версию на чистое устройство недостаточно.

Нужно проверить миграцию старых локальных данных.


Один web-core делает такие миграции проще

Если storage format общий, одна migration может работать:

PWA
Android
iOS.

Platform adapter занимается только физическим persistence.


Не нужно забывать о store assets

Поддержка Capacitor означает, что теперь существуют:

app icon;
splash;
store screenshots;
privacy descriptions;
version numbers;
signing.

Это отдельный release surface.


Web release может занимать минуты

Native release — уже самостоятельный operational процесс.

Поэтому перед решением:

Сделаем мобильную оболочку.

нужно считать не только стоимость первой реализации.

Но и стоимость каждого будущего релиза.


Это главный скрытый TCO

Теперь нужно поддерживать:

Android tooling;
iOS tooling;
Capacitor upgrades;
native plugins;
store requirements;
real devices;
signing.

Например текущая ветка Capacitor 8 имеет свои требования к Xcode, Android Studio, SDK и platform target, то есть native toolchain тоже движется независимо от вашего web-продукта.


Поэтому Capacitor — не «бесплатный APK из сайта»

Он экономит огромное количество дублирования web-логики.

Но native application lifecycle всё равно существует.


Это нормальный trade-off

Мы получаем:

один web-core
+
Android
+
iOS

вместо:

Web team
+
Kotlin application
+
Swift application

Но цена не становится нулевой.


Практичная структура проекта

Например:

project/
│
├── src/
│   ├── domain/
│   ├── api/
│   ├── ui/
│   ├── offline/
│   └── platform/
│       ├── web/
│       └── native/
│
├── public/
│   ├── manifest.webmanifest
│   └── icons/
│
├── service-worker/
│
├── android/
├── ios/
│
└── capacitor.config.*

Здесь видно:

domain

не принадлежит ни одной платформе.

А:

android/
ios/

остаются настоящими native projects.


Как выглядит build

Концептуально:

Shared source
     ↓
Web build
     │
     ├────→ Deploy Web/PWA
     │
     ├────→ Capacitor Android
     │
     └────→ Capacitor iOS

Capacitor использует директорию собранных web-assets как источник для native приложения.

Это и позволяет реально переиспользовать один frontend build.


Но конфигурации сред могут отличаться

Например:

Web production API:
https://api.example.ru

Native тоже должен обращаться туда же.

Нельзя случайно собрать Android:

API_BASE_URL=http://localhost:4173

и обнаружить это после публикации.


Поэтому native build нужен собственный production preflight

Проверяем:

API URL;
build mode;
app ID;
version;
plugins;
permissions;
icons;
signing configuration.

Так же как web deployment имеет собственный preflight.


Какой вариант дешевле поддерживать

Если нужна только таблица, форма и профиль:

PWA

почти наверняка дешевле.

Если уже нужны:

store distribution;
native notifications;
device APIs;
сильная mobile integration,

Capacitor начинает оправдывать дополнительную стоимость.


А поддержка обоих вариантов оправдана, когда у них разные задачи

Это ключевой вывод.

PWA отвечает:

Как максимально быстро доставить продукт пользователю через Web?

Capacitor:

Как этот же продукт глубже встроить в Android/iOS?

Это не один и тот же вопрос.


Сравним

ВозможностьWeb/PWACapacitor Android/iOS
Открытие по ссылкеОтличноНужна установка
SEO/public WebДаСамо приложение — нет
Быстрый web-релизДаNative package живёт отдельным циклом
Установка без storeДаОбычно native distribution
Offline shellService Worker/cachePackaged web assets
Device APIsЧерез доступные Web APIЧерез native plugins
Native permissionsОграничено web-модельюДа
Store presenceНе основной путьДа
Единый web-coreДаДа
Native maintenanceНетДа
Deep OS integrationОграничена платформой/browserЗначительно шире

Когда мы бы выбрали только PWA

Если:

основной канал — Web;
SEO важно;
native API почти не нужны;
команда небольшая;
минимальный TCO важен.

Когда выбрали бы PWA + Capacitor

Если:

Web остаётся важным;
нужны Android/iOS;
большая часть UI общая;
нужны native APIs;
store presence полезен;
продукт должен работать и по ссылке, и как mobile app.

Когда web-first подход уже может не подойти

Если мобильное приложение по своей природе в основном:

native sensors;
Bluetooth;
AR;
сложная background activity;
high-performance graphics;
глубокая platform integration.

Тогда доля общего Web-кода может стать слишком маленькой.


Практический checklist PWA + Capacitor

Перед тем как поддерживать оба канала, мы бы проверили:

  • основная business logic не зависит от Capacitor;
  • Web остаётся полноценным клиентом продукта;
  • manifest и PWA installation проверяются отдельно;
  • Service Worker имеет версионирование cache и upgrade-test;
  • старые PWA-клиенты корректно переходят на новый release;
  • Capacitor imports локализованы в platform layer;
  • native plugin имеет web fallback, если функция должна существовать в браузере;
  • native permissions запрашиваются осознанно;
  • Android back/navigation протестированы;
  • iOS/Android safe areas проверены;
  • keyboard не ломает формы и modal;
  • deep links ведут в ту же domain navigation model;
  • backend API совместим с несколькими версиями mobile clients;
  • application version можно диагностировать на сервере;
  • upgrade Android/iOS проверяется поверх предыдущей версии;
  • local user data не теряется после native update;
  • offline shell не путается с offline business data;
  • sync logic является общей, а не отдельной для каждой платформы;
  • критичные права доступа всегда проверяются backend;
  • secrets не зашиваются во frontend bundle;
  • web/PWA и native имеют отдельные smoke-tests;
  • Android/iOS тестируются хотя бы на реальных устройствах;
  • native toolchain обновляется контролируемо;
  • store release рассматривается как отдельный production-процесс.

Самый важный архитектурный тест

Удаляем мысленно:

android/
ios/

Работает ли Web?

Если нет, мы создали слишком сильную зависимость от оболочки.


Второй тест

Удаляем PWA features:

Service Worker;
install prompt.

Открывается ли сайт как обычное web-приложение?

Если нет, progressive enhancement тоже нарушен.


Третий тест

Backend видит клиента:

Web 5.2
Android 5.0
iOS 4.9

Продолжают ли работать критичные сценарии?

Если нет — release model слишком жёстко связывает server и mobile clients.


Четвёртый тест

Пользователь потерял интернет.

Что именно продолжает работать?

Нужно уметь перечислить это явно:

Application shell      ✓
Existing local data    ✓
Create local record    ✓
Server synchronization ✗

или:

Application shell      ✓
Business data          ✗

Оба варианта допустимы.

Опасно только не знать, какой из них реализован.


Что мы получили в итоге

Не:

сайт
+
ещё одно Android приложение
+
ещё одно iOS приложение.

А:

                 Shared web product
                        │
           ┌────────────┼────────────┐
           │            │            │
           ▼            ▼            ▼
         Browser       PWA       Native shell
                                     │
                               ┌─────┴─────┐
                               ▼           ▼
                            Android       iOS

У продукта остаётся одна domain model.

Один API.

Основной UI.

Общая offline/sync logic.

Но capabilities расширяются в зависимости от платформы.


Вместо вывода

Выбор между PWA и нативным приложением часто формулируют слишком жёстко:

Что выбрать?

Для web-first продукта иногда правильнее задать другой вопрос:

«Нужно ли нам вообще выбирать только одно?»

PWA даёт очень сильные свойства Web:

открытие по ссылке;
SEO;
моментальную доступность;
быстрый deployment;
установку без отдельного native codebase.

Capacitor добавляет:

Android;
iOS;
native plugins;
device APIs;
store distribution;
platform integration.

И при правильной архитектуре один вариант не обязан уничтожать другой.

Ключевой принцип — сохранять общими:

domain;
UI;
API client;
business rules;
sync logic.

А различия концентрировать на границе:

notifications;
permissions;
files;
deep links;
lifecycle;
device APIs.

Тогда Capacitor действительно работает как нативная оболочка вокруг web-first продукта, а не как начало второго независимого приложения.

Service Worker при этом продолжает решать web/PWA-задачи.

Native package имеет собственный lifecycle.

Backend учитывает несколько поколений клиентов.

А пользователь может выбрать наиболее удобный путь:

открыть сайт

или:

установить PWA

или:

использовать Android/iOS приложение.

Это немного дороже в поддержке, чем один только Web.

Но для продукта, который одновременно хочет сохранять открытость Web и получить полноценное присутствие на мобильных платформах, такой подход даёт очень хороший компромисс:

одна основа продукта — несколько способов доставки пользователю.

Есть похожая задача?

Опишите продукт, интеграции и ограничения. До разработки зафиксируем объём, риски и критерии приёмки.

Обсудить проект →