Всем привет, меня зовут Илья, я работаю Frontend разработчиком в компании Бастион.
В данной статье я хочу затронуть такую интересную тему, как обновление бандла Capacitor-приложений (CodePush, live update и т.д).
Сталкивались ли Вы когда-нибудь с ситуацией, когда необходимо незначительно обновить мобильное приложение, написанное на Capacitor?
Предположим такую ситуацию: Вы выпустили релиз приложения, где все изменения не связаны с обновлением нативного кода, то есть Вы не добавляли новых библиотек в приложение, которые содержат нативный код (доступ к камере, NFC и т. д.). Или, допустим, Вы обновили пару строчек в спецификации. Даже ради таких, казалось бы, небольших изменений вам придется делать как минимум один релиз в магазин приложений (а их бывает много).
А если нужно сделать релизы во всех популярных магазинах? Google Play, RuStore, AppGallery и, конечно же, самые нерасторопные из существующих — App Store. Выпуск во всех интересующих магазинах может занять значительное время. Вы, конечно, можете автоматизировать этот процесс при помощи различных инструментов, но, так или иначе, это занимает время на одобрение модераторами.
Ковальски, варианты?
Из вышесказанного вытекает вполне логичный вопрос: можно ли как-то обойти процесс аппрува в магазинах приложений? Ответ — да, можно. Как известно, ни один билд Capacitor-приложения не обходится без такой директории, как dist (или build и т. д). Именно её и можно обновить (по сути, заменить) на другую аналогичную директорию, так как она не затрагивает никакого нативного кода.
А поможет нам в этом замечательный плагин capacitor-updater . С его помощью, а также с open-source проектом Capgo от тех же разработчиков, можно настроить автоматическое обновление приложения. Также Capgo доступен для self-hosted решения; для этого в их документации есть пример.
Но существует ещё вариант решения задачи. В документации это называется Manual Setup
— что по существу является интеграцией плагина capacitor-updater
в наше Capacitor-приложение.
Пример реализации именно этого варианта будет приведён ниже.
А разве так можно?
Возможно по ходу прочтения статьи у Вас возник вопрос. А разве магазины приложений разрешают такого рода обновления? Ответ - да, разрешают. Ниже приведены несколько объяснений на этот счет:
![Обновление Capacitor приложений в обход сторов - 1 Заметка](https://www.pvsm.ru/images/2025/02/06/obnovlenie-Capacitor-prilojenii-v-obhod-storov.png)
Перевод:
Обновления по воздуху ограничены изменениями в пакете JavaScript. Если вам нужно обновить нативный код, например, добавить или удалить плагин или изменить конфигурацию нативного проекта, вам потребуется отправить новый нативный двоичный файл в магазины приложений.
Например, в документации Google Play в разделе Злоупотребление ресурсами устройства и сети говорится о том, что приложения, использующие интерпретируемые языки (JS), не должны нарушать правила Google Play, что, де-факто, является разрешением на использование обновлений по воздуху.
Apple же считает что интерпретируемый код может быть загружен в Приложение, но только при условии, что такой код:
-
Не изменяет основное назначение Приложения, предоставляя функции или возможности, несогласованные с намеренным и рекламированным назначением Приложения, представленного в App Store
-
Не создает магазин или площадку для других кодов или приложений
-
Не обходит подпись, песочницу или другие средства безопасности ОС.
Все условия магазинов приложения автоматически выполнены ввиду природы webView приложений. Можно приступать к реализации.
Практическое применение
В качестве примера мы воспользуемся ручным режимом обновления доступном нам бесплатно. Вы также можете выбрать self-hosted решение у Capgo
, которое также является бесплатным, но данная статья не о нем. Итак, приступим.
Шаг 1
Первым делом нам нужно установить необходимые зависимости в виде:
-
-
compare-versions
выполнив:
npm i @capgo/capacitor-updater compare-versions
Разумеется устанавливать нужно версию совместимую с версией capacitor
вашего проекта. В моем случае это последняя 6 версия, поэтому при установке я не указываю конкретную.
Шаг 2
Также необходимо в capacitor.config.ts
или capacitor.config.json
добавить следующие строки:
plugins: {
CapacitorUpdater: {
autoUpdate: false,
},
},
Они явно указывают плагину на то, что скачивать и выполнять обновления мы будем в ручном режиме. Далее я приведу код написанный с использование библиотеки React
, но вы вольны использовать абсолютно любое решение.
Для этого создадим провайдер и назовем его AppUpdateProvider
:
const AppUpdateProvider: FC<PropsWithChildren> = ({ children }) => {
return children;
};
export default AppUpdateProvider;
Шаг 3
Далее добавялем в наш провайдер вызов следующей функции:
CapacitorUpdater.notifyAppReady();
Она уведомляет capacitor-updater
о том, что текущий бандл работает корректно. По умолчанию эта функция должна быть вызвана в первые 10 секунд после запуска приложения, иначе получим ошибку.
Я не расписываю отдельно вспомогательный код, по типу селекторов стора, так как это не относится к теме статьи. К тому же это специфика каждого отдельного проекта который может быть написан на любом фрейворке/библиотеке.
const dispatch = useAppDispatch();
const appRelease = useAppSelector(selectAppRelease); // берем из стора информацию о последнем релизе
Вы, например, можете получить информацию о последнем доступном релизе используя публичное api Github выполнив обычный fetch запрос.
Пример реализации
Далее приведен достаточно большой кусок кода с комментариями, после которого я объясню некоторые моменты:
useEffect(() => {
if (appRelease === null) { // Если нет релиза, то получить его
dispatch(
getAppRelease({ // Redux thunk с axios запросом к внутреннему сервису компании для получения информации о релизе
type: ReleaseType.APP,
last: true,
}),
);
} else {
const downloadBundle = async () => {
dispatch(setAppUpdating(true)); // Переменная стора для отображения модального окна загрузки
return CapacitorUpdater.download({
url: APP_BUNDLE_DOWNLOAD_LINK, // Url zip архива бандла, можно скопировать из релиза на github
version: appRelease.version, // Версия скачиваемого релиза
});
};
CapacitorUpdater.addListener('downloadComplete', async ({ bundle: downloadedBundle }) => { // Слушатель ивента завершения скачивания бандла
try {
await CapacitorUpdater.set(downloadedBundle); // При успешном скачивании заменяет текущий бандл на скачанный
dispatch(setAppUpdating(false)); // Закрываем мобальное окно
} catch (e) {
console.error(e);
dispatch(setAppUpdating(false));
}
});
if (compare(String(version), appRelease.version, '<')) downloadBundle(); // Скачивание начинается если текущая версия из package.json ниже последнего релиза
return () => {
CapacitorUpdater.removeAllListeners();
App.removeAllListeners();
};
}
}, [appRelease]);
Выполняя функцию CapacitorUpdater.download
указывается обязательное поле url
. Здесь должна укаызваться прямая ссылка на zip архив с бандлом приложения. Находиться этот архив может где угодно, главное чтобы протокол был https
.
Далее очень важное уточнее касательно создания данного zip архива:
Создание архива бандла ОБЯЗАТЕЛЬНО должно производиться при помощи Capgo/cli
Исполнив следующую строчку в корне проекта, на выходе вы получите нужный нам zip архив.
npx @capgo/cli bundle zip [appId] --name [myapp]
Где appId
- собственно app ID приложения, посмотреть который можно в capacitor.config.ts
или capacitor.config.json
, а name - имя выходного файла. Не забудьте изменить поле version
в package.json
проекта. После этого нужно загрузить его на сервер для получения по прямой ссылке в коде выше это - APP_BUNDLE_DOWNLOAD_LINK
.
После успешного скачивания файла по ивенту 'downloadComplete' выполняется функция CapacitorUpdater.set
- которая заменяет текущий бандл приложения на скачанный и перезапускает приложение.
В конечном итоге наш провайдер должен выглядеть следующим образом:
const AppUpdateProvider: FC<PropsWithChildren> = ({ children }) => {
CapacitorUpdater.notifyAppReady();
const dispatch = useAppDispatch();
const appRelease = useAppSelector(selectAppRelease); // берем из стора информацию о последнем релизе
useEffect(() => {
if (appRelease === null) { // Если нет релиза, то получить его
dispatch(
getAppRelease({ // Redux thunk с axios запросом к внутреннему сервису компании для получения информации о релизе
type: ReleaseType.APP,
last: true,
}),
);
} else {
const downloadBundle = async () => {
dispatch(setAppUpdating(true)); // Переменная стора для отображения модального окна загрузки
return CapacitorUpdater.download({
url: APP_BUNDLE_DOWNLOAD_LINK, // Url zip архива бандла, можно скопировать из релиза на github
version: appRelease.version, // Версия скачиваемого релиза
});
};
CapacitorUpdater.addListener('downloadComplete', async ({ bundle: downloadedBundle }) => { // Слушатель ивента завершения скачивания бандла
try {
await CapacitorUpdater.set(downloadedBundle); // При успешном скачивании заменяет текущий бандл на скачанный
dispatch(setAppUpdating(false)); // Закрываем мобальное окно
} catch (e) {
console.error(e);
dispatch(setAppUpdating(false));
}
});
if (compare(String(version), appRelease.version, '<')) downloadBundle(); // Скачивание начинается если текущая версия из package.json ниже последнего релиза
return () => {
CapacitorUpdater.removeAllListeners();
App.removeAllListeners();
};
}
}, [appRelease]);
return children;
};
export default AppUpdateProvider;
Добавляем провайдер в main.tsx
файл нашего проекта:
root.render(
<Provider store={store({})}>
<AppUpdateProvider>
<App />
</AppUpdateProvider>
</Provider>,
);
Результат выполнения нашего провайдера выглядит так:
![Обновление Capacitor приложений в обход сторов - 2 Заметка](https://www.pvsm.ru/images/2025/02/06/obnovlenie-Capacitor-prilojenii-v-obhod-storov-2.gif)
После запуска приложения происходит выполнение кода провайдера, в результате чего скачивается новый бандл приложения (с красными кнопками вместо синих) и устанавливается взамен старого.
На этом у меня все! Пишите любые интересующие вас вопросы в комментарии.
VK: https://vk.com/sudondie
TG: @sudondie
Автор: sudondie