Нативных разработчиков в команде нет, зато половина хорошо знает Vue, поэтому мобильное приложение для iOS и Android я как лид решил делать на Vue и Capacitor. Разбираю, что этот стек дал веб‑команде, какие сложности пришлось решить с нативным тулчейном, вырезом экрана и клавиатурой и когда Capacitor стоит брать. Читать далее
Уровень сложностиСредний
Время на прочтение14 мин
Охват и читатели6.6K
Кейс
Нашей команде поставили задачу разработать мобильное приложение для iOS и Android. Нативных разработчиков на Swift и Kotlin в команде нет, зато половина хорошо знает Vue. Поэтому я как лид выбрал Vue и Capacitor — фреймворк, который упаковывает обычное веб‑приложение в нативную оболочку и даёт доступ к функциям телефона через JavaScript‑плагины.
Через приложение клиенты заказывают охрану и транспорт с водителем. В нём есть:
конфигуратор и оформление заказа;
отслеживание исполнителя на карте;
вход по PIN‑коду или Face ID.
Стек: Vue 3, TypeScript, Vite и Capacitor 8.
По итогам тестирования приложение в целом ведёт себя как нативное. Для этого команде пришлось освоить то, с чем фронтендер обычно не работает:
нативные инструменты сборки на компьютерах с Windows, macOS и Linux;
вёрстку под вырез экрана и клавиатуру;
настройку нативных проектов;
раздачу тестовых сборок через TestFlight.
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 нативные проекты хранятся в репозитории и правятся вручную. Можно пойти другим путём и пересоздавать их из конфигурации при каждой сборке. Мы выбрали этот вариант:
Плюс: проект всегда чистый, конфликтов при слиянии нет.
Минус: ручные правки теряются, поэтому нативные настройки приходится вносить скриптом.
Писать нативный код не нужно, но собирать нативные проекты всё равно придётся, а для этого нужны незнакомые фронтендеру инструменты: 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. Привычные фронтендеру nvm и Volta фиксируют только версию Node, а SDKMAN — только JVM‑инструменты. У mise возможностей больше:
закрепляет версии Node, JDK, Android cmdline‑tools и xcodes;
задаёт переменные окружения;
описывает задачи установки;
работает одинаково на Windows, macOS и Linux.
Всё это хранится в одном файле 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.
Больше всего правок потребовали вещи, которых нет в десктопном браузере: вырез и полоска «домой», экранная клавиатура, сенсорные жесты и особенности 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.
Нативные проекты, которые создаёт 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.
Аналога 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‑приложение можно только в 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 пришлось добавлять в нативный проект самим.
Движок 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.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Фронтенд никогда не умрёт | 0 | 6.54 | 26-07-2026 |
| 2 | «Это уже тысячу раз делали»: как мы добавили медиаленту в Яндекс Еду для iOS. А потом переделали | 0 | 8.46 | 19-06-2026 |
| 3 | Делаем модуль на C++ для приложения React Native | 0 | 6.53 | 30-07-2026 |
| 4 | Разработка на Авроре под Аврору: разворачиваем Coder в облаке и пишем плагин для VS Code | 0 | 7.72 | 27-08-2026 |
| 5 | Микрофронтенды в экосистеме Vue 3: практическое руководство для разработчика в 2026 году | 1 | 9.9 | 29-07-2026 |
| 6 | Продакшн‑разработка в одиночку с AI‑агентами: принуждай к правилам, а не объясняй их | 0 | 8.25 | 24-07-2026 |
| 7 | Сложные сайты на вайбкоде: четыре лендинга, два пути к «дорогим» эффектам и 3D-логотип из спрайтов | 0 | 9.77 | 05-08-2026 |
| 8 | Как я собрал браузерный хоррор на Three.js: GLB-контракт, аномалии и PS1-рендеринг для слабых устройств | 0 | 14.62 | 12-08-2026 |
| 9 | Зачем быстрая реактивность за пределами DOM: ответы на вопросы и знакомство с Raph | 0 | 6.89 | 09-10-2026 |
| 10 | o2 game engine | 1 | 6.4 | 24-08-2026 |