Как фронтенд‑команде сделать мобильное приложение без нативных разработчиков

Нашей команде поставили задачу разработать мобильное приложение для iOS и Android. Нативных разработчиков на Swift и Kotlin в команде нет, зато половина хорошо знает Vue. Поэтому я как лид выбрал Vue и Capacitor — фреймворк, который упаковывает обычное веб‑приложение в нативную оболочку и даёт доступ к функциям телефона через JavaScript‑плагины.
Через приложение клиенты заказывают охрану и транспорт с водителем. В нём есть:
конфигуратор и оформление заказа;
отслеживание исполнителя на карте;
вход по PIN‑коду или Face ID.
Стек: Vue 3, TypeScript, Vite и Capacitor 8.
По итогам тестирования приложение в целом ведёт себя как нативное. Для этого команде пришлось освоить то, с чем фронтендер обычно не работает:
нативные инструменты сборки на компьютерах с Windows, macOS и Linux;
вёрстку под вырез экрана и клавиатуру;
настройку нативных проектов;
раздачу тестовых сборок через TestFlight.
Что мы получили от Capacitor
Один стек на всё
Vue 3, Vite, Tailwind, Vitest и Storybook работают так же, как в вебе. Что это даёт на практике:
Вёрстку и роутинг почти целиком проверяем в Chrome DevTools в режиме устройства.
Эмулятор нужен только для того, что зависит от платформы.
Горячая замена модулей (HMR) в Vite работает и на телефоне: достаточно указать адрес dev‑сервера в
server.url.Приложение на устройстве отлаживается как обычная страница: через Safari Web Inspector на iOS и
chrome://inspectна Android.Нативный разработчик на Swift или Kotlin команде не понадобился.
Нативные функции через легко подключаемые плагины
Для работы с возможностями смартфона у Capacitor есть большой набор плагинов: камера, геолокация, уведомления, файловая система, вибрация и другие. Официальные плагины поддерживает команда Capacitor, их список есть в документации. Остальное, например биометрию, закрывают плагины сообщества из отдельного каталога.
Мы используем пять плагинов:
@capacitor/app— события жизненного цикла приложения и системные кнопки;@capacitor/device— сведения об устройстве, например язык системы;@capacitor/keyboard— управление экранной клавиатурой;@aparajita/capacitor-biometric-auth— вход по Face ID и отпечатку пальца с имитацией биометрии в браузере;capacitor-secure-storage-plugin— защищённое хранение данных в Keychain и Keystore.
Разберём подключение на примере @capacitor/keyboard.
Шаг 1. Установить пакет и обновить нативные проекты:
npm install @capacitor/keyboard
npx cap sync
Шаг 2. Настроить плагин в capacitor.config.ts. Параметр resize: None отключает сжатие WebView при открытии клавиатуры:
import type { CapacitorConfig } from "@capacitor/cli";
import { KeyboardResize } from "@capacitor/keyboard";
const config: CapacitorConfig = {
appId: "com.example.app",
appName: "Example",
webDir: "dist",
plugins: {
Keyboard: {
resize: KeyboardResize.None,
},
},
};
export default config;
Шаг 3. Вызвать API из кода приложения. Здесь панель над клавиатурой iOS скрывается, а высота клавиатуры передаётся в CSS‑переменную:
import { Capacitor } from "@capacitor/core";
import { Keyboard } from "@capacitor/keyboard";
if (Capacitor.getPlatform() === "ios") {
await Keyboard.setAccessoryBarVisible({ isVisible: false });
}
await Keyboard.addListener("keyboardWillShow", ({ keyboardHeight }) => {
document.documentElement.style.setProperty("--keyboard-height", `${keyboardHeight}px`);
});
Нативный проект под каждую платформу
Для каждой платформы Capacitor создаёт обычный нативный проект: Xcode для iOS и Android Studio для Android. Внутри него в WebView работает веб‑приложение, а собирается он стандартными инструментами платформы. В WebView доступно всё, что есть в браузере, поэтому мы использовали знакомые библиотеки и код, наработанный в других проектах.
По задумке Capacitor нативные проекты хранятся в репозитории и правятся вручную. Можно пойти другим путём и пересоздавать их из конфигурации при каждой сборке. Мы выбрали этот вариант:
Плюс: проект всегда чистый, конфликтов при слиянии нет.
Минус: ручные правки теряются, поэтому нативные настройки приходится вносить скриптом.
Сложность 1: придётся освоить нативный тулчейн
Писать нативный код не нужно, но собирать нативные проекты всё равно придётся, а для этого нужны незнакомые фронтендеру инструменты: JDK, Android SDK, эмуляторы, Xcode. Все их нам пришлось изучить и настроить, а у каждого свои требования к версиям. Часть инструментов ставит менеджер версий, остальное — вручную:
Ставит менеджер версий mise:
JDK (Temurin 21), на котором работает Gradle;
Android SDK: Platform 36 и Build‑Tools 35.0.0;
образы эмулятора Android API 36 и API 34;
Xcode 26+ и раннер iOS‑симулятора, только на macOS.
Ставим вручную:
Android Studio: из неё нужны Device Manager и Logcat;
виртуальные устройства в Device Manager;
аппаратное ускорение эмулятора: в Windows — компонент Windows Hypervisor Platform, в Linux — KVM (Kernel‑based Virtual Machine).
Ручная настройка
Если ставить всё вручную, легко пропустить один из шагов, и тогда Gradle не найдёт SDK или не запустится эмулятор. К тому же никто не гарантирует, что у двух разработчиков окажутся одинаковые версии. Где легко ошибиться:
JAVA_HOMEдолжен указывать на корень JDK, без/binи без слэша в конце.В SDK Manager нужно включить «Show Package Details», иначе установятся не те версии. Флажок «Command‑line Tools» по умолчанию снят, а без него не будет
sdkmanager.Gradle JDK в настройках Android Studio задаётся отдельно: там тоже нужен Temurin 21.
Путь в
ANDROID_HOMEв Windows, macOS и Linux разный. ВPATHдобавляются три каталога, после чего придётся открыть новый терминал и полностью перезапустить VS Code.
mise: ещё один инструмент, который пришлось освоить
Чтобы не повторять эту настройку на каждой машине, мы освоили менеджер версий mise. Привычные фронтендеру nvm и Volta фиксируют только версию Node, а SDKMAN — только JVM‑инструменты. У mise возможностей больше:
закрепляет версии Node, JDK, Android cmdline‑tools и
xcodes;задаёт переменные окружения;
описывает задачи установки;
работает одинаково на Windows, macOS и Linux.
Всё это хранится в одном файле mise.toml. Упрощённый пример:
mise.toml
# Версии инструментов: mise скачает их и включит при входе в папку проекта
[tools]
java = "temurin-21" # JDK для Gradle, который собирает Android-проект
node = "24"
android-sdk = "23.0" # утилиты командной строки Android SDK
xcodes = { version = "latest", os = ["macos"] } # установщик Xcode, только на macOS
# Переменные окружения, которые mise выставляет в папке проекта
[env]
# SDK лежит в стандартном для ОС каталоге, а не в каталоге mise:
# так он общий с Android Studio и образы не скачиваются дважды
ANDROID_HOME = "{% if os() == 'windows' %}{{ env.LOCALAPPDATA }}\\Android\\Sdk{% elif os() == 'macos' %}{{ env.HOME }}/Library/Android/sdk{% else %}{{ env.HOME }}/Android/Sdk{% endif %}"
# adb и emulator становятся доступны в терминале
_.path = ["{{ env.ANDROID_HOME }}/platform-tools", "{{ env.ANDROID_HOME }}/emulator"]
# Переменные для задач
[vars]
# ABI образа эмулятора: arm64 на Apple silicon, x86_64 на Intel и AMD
abi = "{% if arch() == 'arm64' or arch() == 'aarch64' %}arm64-v8a{% else %}x86_64{% endif %}"
# Задачи запускаются командой mise run <имя задачи>
[tasks."android:sdk"]
description = "Пакеты Android SDK для сборки"
run = "android sdk install platform-tools platforms/android-36 build-tools/35.0.0"
[tasks."android:emulator"]
description = "Эмулятор и образы Android API 36 и API 34"
depends = ["android:sdk"] # сначала выполнится android:sdk
run = "android sdk install emulator system-images/android-36/google_apis/{{ vars.abi }} system-images/android-34/google_apis/{{ vars.abi }}"
[tasks."ios:simulator"]
description = "Раннер iOS-симулятора, только macOS"
run = "xcodebuild -downloadPlatform iOS"
Новому разработчику достаточно запустить mise install и задачу установки. Если чего‑то не хватает, задача проверки окружения покажет, чего именно, и разбираться в ошибках xcodebuild придётся реже. Подробнее о секциях и задачах написано в документации mise.
Грабли, на которые наступили по дороге
В автоматизации Android SDK через mise нашлись три подвоха:
cmdв Windows разбивает строку по;даже внутри кавычек, поэтому пакет"platforms;android-36"превращался в два несуществующих. Пришлось перейти сsdkmanagerна новый Android CLI, где имена пакетов пишутся через слэш.Android CLI возвращает код 9 даже при успешной установке. Этот код приходится игнорировать, а установку проверять по наличию каталогов пакетов.
avdmanagerне видит образы, установленные новым CLI, аandroid emulator createне позволяет задать имя и уровень API. В итоге виртуальные устройства по‑прежнему приходится создавать вручную в Device Manager.
Сложность 2: экран телефона отличается от окна браузера
Больше всего правок потребовали вещи, которых нет в десктопном браузере: вырез и полоска «домой», экранная клавиатура, сенсорные жесты и особенности WebView. Примерно каждый четвёртый баг на тестировании касался отрисовки на телефоне, причём половина таких багов проявлялась только на iPhone.

Два бага связаны с безопасными зонами экрана. На iPhone шапка заезжала под часы и индикаторы: без viewport-fit=cover переменные env(safe-area-inset-*) не работают. Теперь viewport-fit=cover добавляется при запуске на iOS, а шапке задан отступ env(safe-area-inset-top). Низ шторки уходил под полоску «домой» из‑за фиксированного нижнего отступа, его заменил отступ calc(5.125rem + env(safe-area-inset-bottom)).
Три бага связаны с касаниями. Двойной тап на iPhone увеличивал масштаб, как на сайте, потому что масштабирование не было запрещено в viewport: помогли user-scalable=no и touch-manipulation на body. На iPhone и Android после касания оставалась синяя рамка фокуса, так как браузерный outline не был отключён для сенсорного ввода. Класс pointer-coarse:outline-none убрал её для касаний и сохранил для мыши и клавиатуры. На экране шириной 360 px не прокручивался ряд иконок: touch-none на ручке шторки перехватывал все касания. С touch-pan-x ряд прокручивается, а шторка двигается только вертикальным жестом.
Ещё три бага касаются прокрутки и анимаций. Шторку на iPhone не удавалось докрутить до конца, потому что сдвиг параллакса не учитывался в высоте прокрутки, и его компенсирует невидимый отступ на ту же величину. На вытянутых экранах вокруг интро‑анимации появлялись белые полосы: Lottie вписывает кадр 1179×2556 целиком, а параметр preserveAspectRatio: "xMidYMid slice" заполняет им экран. Стекло шапки на iPhone и Android теряло размытие во время перехода между экранами, так как backdrop-filter в отдельном слое View Transitions рисуется прямоугольником. Теперь в отдельный слой выносится только ряд кнопок, а стекло остаётся в снимке страницы.
Остаются баги с клавиатурой. На iPhone над ней появлялась лишняя панель, а саму клавиатуру нельзя было закрыть: так WebView ведёт себя по умолчанию. Это исправили плагин @capacitor/keyboard, атрибут enterkeyhint="done" и закрытие клавиатуры при прокрутке. На Android фон «сжимается» при открытии клавиатуры, потому что градиент растянут по высоте экрана, а видимая высота WebView уменьшается. Этот баг пока не исправлен.
Почему эти ошибки не видны в браузере
Режим устройства в Chrome DevTools не показывает реальную версию WebView, системные отступы (вырез, строка состояния), масштаб системного шрифта и нативные диалоги вроде Face ID. Приложение работает внутри системного WebView, а его версия и поведение зависят от устройства. На iOS движок привязан к версии системы, поэтому на старых iPhone часть веб‑API недоступна.
На Android WebView обновляется отдельно, через Play Store, но поведение самой системы меняется от версии к версии: начиная с Android 15 приложение принудительно отображается под системными панелями (edge‑to‑edge). Поэтому в проекте два образа эмулятора: API 36 (целевая версия) и API 34 (поведение до edge‑to‑edge). На практике сложную вёрстку лучше проверять на реальном iPhone или в симуляторе и отлаживать через Safari → Develop или chrome://inspect.
Сложность 3: нативную часть иногда нужно донастроить
Нативные проекты, которые создаёт Capacitor, и готовые плагины закрывают большую часть задач. Для остального понадобилось несколько нативных настроек и пара обработчиков поведения платформы.
Патчи вместо правок в Xcode
Если нативные проекты пересоздаются, ручные правки в Xcode и Android Studio теряются. Поэтому все нативные настройки мы вносим отдельным скриптом: он запускается при каждой сборке, а повторный запуск ничего не ломает. Что дописывает скрипт и что будет без этого:
NSFaceIDUsageDescriptionвInfo.plist. Без него вход по Face ID падает при первом же запросе.UIApplicationSceneManifestиSceneDelegate. Без них приложение, собранное в Xcode 27, закрывается сразу после запуска.ITSAppUsesNonExemptEncryption = false. Без этого App Store Connect для каждой сборки задаёт вопрос о шифровании, и, пока ответа нет, сборка недоступна тестировщикам.
У скрипта есть слабое место: его нужно запускать везде, где собирается приложение, включая CI. Иначе сборка уйдёт без нужных настроек, например без разрешения на Face ID.
Поведение платформы не совпадает с вебом
Кнопка «Назад» на Android. По умолчанию она закрывала приложение, а не возвращала на предыдущий экран. Понадобился обработчик, связанный с vue‑router. Мы пока не проверяли, закрывает ли он открытую шторку, а не уводит со страницы.
Видео после сворачивания. iOS ставит его на паузу, поэтому слайдер теперь отслеживает видимость страницы через
useDocumentVisibility.
Раздача iOS‑сборок
Аналога APK на iOS нет, поэтому просто отправить файл тестировщику нельзя. С бесплатным Apple ID сборка работает семь дней и только на телефоне разработчика, а для TestFlight нужно участие в Apple Developer Program за $99 в год. Собрать iOS‑приложение можно только на macOS, поэтому раздачу удобно вынести в облачный CI с Mac, например Codemagic: он подписывает сборку и сам загружает её в TestFlight.
Скрипты обвязки тоже надо поддерживать
Помимо самого приложения нужно поддерживать набор скриптов для удобной работы с ним. Эти скрипты:
запускают dev‑сервер и приложение на эмуляторе или симуляторе с live‑reload: находят уже запущенное устройство или поднимают новое и настраивают
adb reverse;проверяют окружение и показывают, чего не хватает на машине;
создают нативные проекты для Android и iOS;
вносят нативные настройки в сгенерированные проекты;
генерируют иконки и заставку приложения из исходных изображений;
собирают iOS‑приложение и загружают его в TestFlight.
Минусы
Для iOS нужны Mac и iPhone
Собрать iOS‑приложение можно только в Xcode, а Xcode работает только на macOS. Поэтому хотя бы у одного разработчика в команде должны быть Mac и iPhone: симулятор не заменяет проверку на реальном устройстве. Подойдёт не любой Mac:
Capacitor 8 требует Xcode 26 или новее (документация Capacitor).
Xcode 26–26.3 запускается на macOS Sequoia 15.6 и новее.
Xcode 26.4 и новее требует macOS Tahoe 26.2, а Xcode 27 — Tahoe 26.6 (системные требования Xcode).
macOS Tahoe ставится на любой Mac с Apple silicon, а из компьютеров на Intel — только на несколько моделей 2019–2020 годов. На остальных Intel‑Mac потолок — Xcode 26.3.
Ограничения задаёт Apple, а не Capacitor: с 28 апреля 2026 года App Store Connect принимает только сборки из Xcode 26 и новее, поэтому Flutter и React Native сталкиваются с теми же ограничениями. На практике для iOS‑разработки стоит брать Mac на Apple silicon.
Клавиатура обошлась дороже всего
По умолчанию открытая клавиатура уменьшает высоту WebView, и вместе с ним перерисовываются фон, градиент и шапка. На iOS мы отключили это поведение (Keyboard.resize: None в capacitor.config.ts), но тогда поле в фокусе перестало само подниматься над клавиатурой. Пришлось написать собственный модуль подъёма полей. Он:
находит контейнер, внутри которого прокручивается поле;
плавно сдвигает содержимое с учётом параллакса;
добавляет временный отступ, если места не хватает;
выставляет CSS‑переменную с высотой клавиатуры, по которой поднимаются шторки и экран входа.
Почти сразу понадобилась поправка: анимация подъёма конфликтовала с касаниями, и теперь она останавливается, как только пользователь дотрагивается до экрана. В нативном приложении эту работу делает система. В Capacitor есть два варианта: либо WebView сжимается и фон перерисовывается вместе с ним, либо подъём полей нужно написать самим.
В Capacitor пока нет поддержки Xcode 27
Приложение, собранное в Xcode 27, закрывается сразу после запуска: в шаблоне Capacitor 8.4 нет сценового жизненного цикла, а стабильного релиза с исправлением пока нет. Ключ UIApplicationSceneManifest в Info.plist и класс SceneDelegate пришлось добавлять в нативный проект самим.
На iPhone возможности WebView зависят от версии iOS
Движок WebView на iPhone обновляется только вместе с системой, поэтому новые веб‑API получают лишь пользователи свежих версий iOS. View Transitions, например, работают начиная с iOS 18, и на более старых телефонах страницы сменяются без анимации.
Итог: когда Capacitor стоит брать
Capacitor себя оправдал: веб‑команда без нативных разработчиков собрала приложение для двух платформ из одной кодовой базы, и почти все проблемы решались веб‑средствами или короткими скриптами. Принцип «написал один раз — работает везде» в целом сработал, но без доработок не обошлось: примерно каждый четвёртый баг на тестировании касался экрана телефона, а сборка держится на наборе скриптов и настроенном mise.
Подходит, если интерфейс состоит из форм, карточек, списков и карт, команда сильна в вебе, а функции устройства нужны стандартные и на них есть готовые плагины.
Стоит заложить больше времени, если продукт держится на сложных жестах, шторках и анимациях, которые должны ощущаться как нативные: их пришлось дорабатывать дольше всего. То же относится к нативным компонентам поверх веба. А для iOS‑версии понадобятся Mac и аккаунт разработчика Apple.
Что сделать в новом проекте с первого дня
Закрепить версии Node, JDK и Android SDK в
mise.tomlи использовать те же версии в CI.Учесть безопасные зоны ещё до начала вёрстки:
viewport-fit=cover,env(safe-area-inset-*)в шапке и нижних панелях, запрет масштабирования.Выбрать стратегию работы с клавиатурой до первой формы: режим resize, панель над клавиатурой,
enterkeyhint.Хранить нативные настройки в скрипте и запускать его и локально, и в CI.
Сразу обработать кнопку «Назад» на Android и возврат приложения из фона.
Проверять вёрстку на реальном iPhone и на двух уровнях Android API, а не только в DevTools.
Оборачивать плагины сообщества в собственный драйвер с тестами, как мы сделали с защищённым хранилищем.
Приложение пока на тестировании, в App Store и Google Play оно ещё не вышло. После релиза расскажем, как оно показало себя у реальных пользователей.
Полезные материалы
Capacitor
Документация Capacitor — с чего начать.
Настройка окружения — что поставить для сборки под iOS и Android.
Capacitor Community и Capawesome — ещё два больших набора плагинов.
Live reload на устройстве — как подключить приложение к dev‑серверу.
Создание своего плагина — если готового нет.
Иконки и заставка — генерация из исходных изображений.
Окружение
mise: конфигурация и задачи.
xcodes — установка и переключение версий Xcode из терминала.
Системные требования Xcode — какая macOS нужна для каждой версии.
Требования App Store к SDK — с каким Xcode принимают сборки.
Вёрстка и отладка на телефоне
env() на MDN — безопасные зоны через
env(safe-area-inset-*).Метатег viewport на MDN —
viewport-fit=coverи запрет масштабирования.View Transitions на Can I use — с каких версий браузеров работает.
Отладка WebView в Chrome —
chrome://inspectдля Android.Web Inspector в Safari — отладка на iPhone и в симуляторе.
Сборка и раздача
TestFlight — раздача iOS‑сборок тестировщикам.
Codemagic для Capacitor — облачная сборка на Mac и загрузка в TestFlight.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.