Самастаянныя апдэты OTA для чыстага React Native з hot-updater і Supabase
Установка апдэйтаў JavaScript праз канец-до-канца ў простам дапрыяздзе React Native з апрантам hot-updater і Supabase: ініцыялізацыя, хук для тыхоўскага апдэйта, каналы, скрыпты для развяртання і анулявання змян.
Крыніцы: Ship JavaScript Fixes in Minutes, Not Days: OTA Updates in React Native with Hot Updater
Адаптация на JavaScript у аплікацыі React Native, яка выконваецца ў адной лініі, не павинна чакаць колькісці дзён на перагляд у App Store, пры тым самэе і адбываецца, калі кожныя змены падаюць через новы бінарны файл. Апдэйты па радыё (OTA) дазволяюць надаваць змены, напісаныя на чыстам JavaScript, безпосередня да встановленых аплікацый, тады калі змены, напісаныя на мовах натыўнага програмавання, яшчэ падаюць через магазіны. У гіде показана настройка прыемна для практычнага вжытку, створаная на адной з бесплатных бібліятэкаў hot-updater, з самастаяным серверам Supabase: інсталяцыя, аднойчынае ініцыялізаванне, функцыя тыхоўскага апдэйту з персональным запрошэнням, каналы для кожнай версіі, скрыпты для розгортання і механізмы вярнення да поперадней версіі, якія забезпечваюць безпеку всей системы.
У якості прыклада выступае аплікацыя для логістыкі, якая работае на чыстам React Native па новай архітэктуры (версія 0.85 на момент напісання), дзе воднікам неабходны шырокія змены якомога быстрей, і ніхто не хочаць павяржацца на інструменты Expo.
Што на самай працо заменяе апдэйт OTA
Кожны бінарны файл React Native на самай працо ўскладнены з двух элементаў, запакаваных разам:
- Натыўны код: кэрнаверы Android і iOS, натыўныя модулі, правы на выкарыстоўванне ресурсаў, а таксама всё, што знаходзіцца ў катэгорыях
android/іios/. - Пакет JavaScript: экраны, компаненты, логіка аперацый, а таксама большая частка рутынных правакіў багоў і дадаць новых функцыяй.
Выпуск практычна ў всіх магазінах прыкладных програм закрывае ўсё гэта ў формате APK або IPA. Проблема заключаецца ў тым, што большая частка повсякдзенной роботы стосуецца толькі часткі, якая напісана на JavaScript. Неправы пазначэнны кранц, некоректны параметр API або мінімальная змяна лейауту не выклікаюць патрэбы ў новым натыўным кодзе, але класычны процес выпуску прымушае зноў стварыць всю програму і працаваць над ўсімі аспектамі ў перыод адзначэння багоў.
Апдэйты па методе OTA адсоедзяюць пакет з гэтым цыклам. У працоўку вбудаванага JavaScript больш не ставится прынцып постояннасі — аплікацыя запытае сервер з апдэйтамі, чы існуе новейшы пакет, завантажыць яго і заменіць ў наступны раз, калі аплікацыя зноў запускаецца. Не трэба ні кансалідаваць пакет у магазіне, ні перынсталляваць аплікацыю; паследнія змены доходзяць да корыстувачаў у наступны раз, калі вони запускаюць аплікацыю.
Едзина правіла керуе всім, што следуе:
Змены, якія вырабляюцца толькі ў JavaScript: іх можна адправіць па методе OTA. Змены у натыўных компанентах (нэвядомая натыўная залежнасць, паднятчык версіі, новы натыўны модуль): для такіх змен падае прынцып неабходнасці стварэння рэальнай версіі аплікацыі для магазіна.
Метод OTA не заменяе версій, створаных для натыўных средаў. Ён не дазволяе такім версіям блакаваць выпуск JavaScript. Перад кожным апдэйтом па методе OTA трэба пераканацца, чы не змениліся якія-небудзь натыўны элементы з моменту пярэдньага выпуску; якщо так, новы пакет можа спрачыцца на прыстроях, якія ўсё ще работаюць з старым пакетам.
Выбір апдэйтара типу hot-updater у працоўку з іншымі альтэрнатывамі
Прывычным рашэннем гадоў быў Microsoft CodePush. Калі App Center і CodePush сталі неактуальнымі, командам знадобілася альтэрнатыва. Пры адгукаванні рэалістычныя варыянты выглядзелі так (перад прыняццем рашэння пераканайцеся ў чынным статусе кожнага з іх, адколі рынак быстра развиваецца):
- CodePush / App Center: неактуальны, таму не ўзяты ў расчыт.
- EAS Update: зрэлы і надзеяны, але ён ўжо стрэльна прычаівае проект да экасистемы Expo.
- Revopush і Codemagic CodePush: падходячыя альтэрнатывы, але яны ўжо платныя службы.
- Firebase Storage як бэкенд: доступны толькі ў платнам плане Blaze.
- hot-updater: варыянт на адкрытым кодзе, які можна самостоятельна запусціць без костаў і які падтрымлівае Новую архітектуру.
hot-updater выканаў кращэ з кальколькі конкрэтных прычын:
- Няма косту ліцэнзавання: это адкрыты код, без платаў за кожнага акtyваўанага корыстувальніка.
- Падтрымка новай архітэктуры: неабходная умова для дапрацоўак, якія выкарыстоваюць новую архітэктуру.
- Падключаемы бэкенд: храненне дакументаў і база дадзенаў выбіраюцца за дапамогою плагінаў. Supabase чырпаецца ўсё паслухам, таму што адны проект забезпечвае храненне, Postgres і функцыю edge для выконання задач супэрвізора, усё гэта ў межах ўжылкавага безкоштовнага тарифу.
- Веб-кансоль плюс CLI: вы не прыгвожданы толькі да роботы з тэрміналам.
- Мгновенная ануляцыя змян: несправны пакет можна выключыць проста перакрыўшы наявны параметр, без прабывання зноў выкласты ў працэс чы перагляду на складзе.
- Этапавая розгортка і групы: пакет можа спачатку быць выкорыстаны на невялікай колькасці прыстроёў, напрыклад 1%, а пасля — на 100%, або можа быць нацэлёваны на конкретную групу, напрыклад
qa.
Згадайце аб аднай застерэжнасці: бібліятэка ў безплатны, але зберагчык і пропускна спроможнасць для вашых пакетаў — гэта ваша адпаведная адпаведальнасць. Для малага чы серэднізначнага дапытку зазвычай достатня безплатная версія Supabase; аднак калі ўсё больш вядомых корыстувачаў часта завантажваюць пакеты, ствараецца іншая ситуацыя.
Інтэграцыя hot-updater у чысты дапытк React Native
Інтэграцыя складаецца з трох частак: інсталляцыя пакетаў і наладжванне бэкенду, аднойчыная ініцыялізацыя бібліятэкі і налаштаванне процесу апдэйта ў вашым інтерфейсе.
Інсталюйце пакеты і наладзіце Supabase
Пакет для выканання працы включаецца ў звычныя залежнасці, тады калі CLI, плагін для стварэння чыстага флоу і плагін Supabase ўжо належаць да залежнасцяў для разработкі.
npm install @hot-updater/react-native
npm install -D @hot-updater/bare @hot-updater/supabase hot-updater
Далей нехай CLI створыць для вас базу дадзеных адзюнкты.
npx hot-updater init
Каманда init абсарбуе контэйнер для зберагчыць, стварая неабходныя тэблі, і публікуе функцыю update-server, якую будзе выкаліваць прыстаночная аплікацыя. Яна таксама запішвае файл .env, які містыць даныя абанонента. Негайна дадзіце гэты файл у .gitignore, а таксама задокументавайце, як парадзіснаваць яго з ключоў service-role у вашай Supabase, каліколькі хтось будзе працаваць з чыстым клонам, яму будзе патрэбна ўтварэнне гэтага файла занова.
Парадзіснавайце раз у пункце запуску
HotUpdater.init() належыць да сферы дзеяння модуля ў index.js і должен выконвацца толькі адназначы, перш чым будзе выкарыстоўвана бібліятэка. Яна паведамляе hot-updater, дзе шукать апдэйты. Якщо яе няма, кожная наступная пераглядка апдэйта не мае URL сервера і тыха нічога не робіць, што ўскладнюе дыягностику.
Імпорты запоўнююць бібліятэку, react-native-config для значэнняў, спецыфічных для сяродавысці, а таксама власную службу аналітыкі дапынутку.
// index.js
import { HotUpdater } from '@hot-updater/react-native';
import Config from 'react-native-config';
import { analytics, AnalyticsEvent } from '@services';
Сама настройка чытае URL функцыі edge і стратэгію апдэйта з зменных сяродавысці. Функцыя onError ўмышлена застаецца мінімальной: яна выконваецца пад час запуску, таму не должна залежаць ад частак дапынутку, якія можа ўжо не быць гатовымі. Їё едынава задача — зафіксаваць, што ініцыялізацыя збілася.
HotUpdater.init({
baseURL: Config.HOT_UPDATER_SUPABASE_FUNCTION_URL, // your edge function URL
updateStrategy: Config.HOT_UPDATER_UPDATE_STRATEGY, // e.g. 'appVersion'
onError: (error) => {
// Bootstrap-level handler — keep it dependency-free.
// Just enough to know init itself failed.
if (__DEV__) console.log('🔥 [OTA] init error', error);
analytics.track(AnalyticsEvent.OTA_INIT_ERROR, {
message: String(error?.message ?? error),
});
},
});
Зверніце увагу на раздзеленне функцыяў тут. Функцыя init вяршыць выбір па канекту з джэрамі апдэйтаў. Чы рошту працоўніку пераканацца ў наявнасці апдэйтаў — гэта частка вашай сабе логікі, напрыклад, за дапамогою функцыі-флага з даўнейшага сервера. Раздзеленне эйных двух рашэнняў спакойна дазволяе выключыць функцыю OTA для певнага сераверскага сераўісу, не чапаючыся да коду запуску.
Безшумны хук для апдэйтаў заместо wrap()
Бібліятэка прыносіць компонент вышэйшага порядку HotUpdater.wrap(), які автаматычна караце перакананне, завантажэнне і застосаванне апдэйтаў. Якщо вам патрэбна толькі функцыя «пераканацца пад час запуску, апдэйтаваць, завершыць», выкорыстоўваюце яго; гэта найшвыжы спосаб.
У прынцыповай аплікацыі часта патрабуецца большая кантроль: заванос дадзеных у фоне, адкрыць сае власна модальная панель, регулюванне частоты перагляду і захаванне всіго механізма праз фіч-флаг. Для гэтага краща прыемна хук, якая ручнае керуе API. Наведзена нижэй версія — це спроставаны ядро, напісана на TypeScript. Яна выкарыстоўвае два рэфы як захаванні: адны для перагляду дадзеных у процесе і другі для пакета, які вялікі час тады ўжо заванесены, і выдае рэф модальнай панелі, флаг прымусовага апдэйта і функцію apply.
// useOtaUpdater.ts
import { useCallback, useEffect, useRef, useState } from 'react';
import { AppState, Modal } from 'react-native';
import { HotUpdater } from '@hot-updater/react-native';
export const useOtaUpdater = () => {
const otaModalRef = useRef<Modal | null>(null);
const stagedRef = useRef(false); // a bundle is downloaded & waiting
const busyRef = useRef(false); // a check is in flight
const [otaForced, setOtaForced] = useState(false);
// 1. Check the server + stage the bundle in the background (no reload yet).
const runSilentOtaCheck = useCallback(async () => {
if (busyRef.current || stagedRef.current) return;
busyRef.current = true;
try {
const updateInfo = await HotUpdater.checkForUpdate({
updateStrategy: 'appVersion',
requestTimeout: 20000, // default 5s is too tight for a cold edge function
});
if (!updateInfo) return; // null → up to date (or check failed)
// Download & stage to disk - does NOT reload the app.
await updateInfo.updateBundle();
stagedRef.current = true;
setOtaForced(!!updateInfo.shouldForceUpdate);
otaModalRef.current?.openModal(); // ask the user to apply it
} finally {
busyRef.current = false;
}
}, []);
// 2. User tapped "Update" → swap the JS bundle and restart.
const applyOtaUpdate = useCallback(async () => {
await HotUpdater.reload(); // this is what actually applies the update
}, []);
// 3. Triggers: check on launch and whenever the app returns to foreground.
useEffect(() => {
const timer = setTimeout(runSilentOtaCheck, 3000); // backstop launch check
const sub = AppState.addEventListener('change', (s) => {
if (s === 'active') runSilentOtaCheck();
});
return () => {
clearTimeout(timer);
sub.remove();
};
}, [runSilentOtaCheck]);
return { otaModalRef, otaForced, applyOtaUpdate };
};
Некалькі выбораў у гэтым хуку выніклі з рэальных проблем у прынцыповых аплікацыях:
requestTimeout: 20000: стандартны таймаут складаецца 5 секунд. Функцыя Supabase edge, якая запускаецца праз першы раз, часта працуе дольшэй, чым гэта, тады запрашанне выходзіць за межы таймаута, іcheckForUpdateвяртаеnull. Гэты результат нельга адзначыць ад “вже актуальнага” стану, таму падыткі ніколи не прыходзяць. Падвоўкай таймаута да 20 секунд можна компенсаваць затрымку праз першы запуск.- Спачатку стадія, пазней застосаванне:
updateBundle()завантажае новы пакет на диск без перзапуску нічога.reload()— ўжо едыны функцыйны кал, який заменяе пакет. Раздзелэнне эых крокаў значыць, што процес доставкі ніколи не будзе перарваны раптовым перзапускам; завантажэнне адбываецца на фоне, а корыстнік сам вяршыта рашэнне пра застосаванне пакета.
busyRef чы stagedRef. Две пераконтролькі ніколі не выконваюцца адночасова, а пакет, які вже знаходзится у стадіі прадаравання, ніколі больш не завантажваецца.Є два незначныя моменты, якія вартае зазначыць. Пераконтролька за тры секунды служыць альтэратывай для запуску, на случай, калі першыя змены стану прыкладнення з пазначэннем active выйдзе раней, чым хук будзе падпісаны. А вызов openModal() прыяжджае, што рэф вядомае да спецыяльнага компонента модала, який выканае гэты метод, а не да вбудованага у React Native компонента Modal; таму трэба прыстосаваць тип рэф да вашага сабе компонента.
З’явіце хук і атрыбуты запрошэння
Выклікайце функцыю поблізу корана дапрыгу і адразу прадстаўьце адны модальны вікн, який керуе обомі типамі апдэйтаў. Непяршы апдэйт можна закрыць; пры прымусовым апдэйте кнопка «Анульвацыя» ховаецца і яго нельга закрыць.
// App.tsx
const { otaModalRef, otaForced, applyOtaUpdate } = useOtaUpdater();
return (
<InfoModal
modalRef={otaModalRef}
subText={otaForced ? 'A required update is available.' : 'An update is available.'}
onAcceptPress={applyOtaUpdate}
showCancelButton={!otaForced} // forced updates can't be dismissed
dismissable={!otaForced}
/>
);
Паколькі shouldForceUpdate надходзіць з сервера, вы самі на кожны пакет вяршыце рашэнне, чыі корыстувальнікі могу яго адкласці. Прымусовыя апдэйты застаўляйце толькі для выправленняя, якія сапраўды не можаць чакаць.
Прадзяліце кожную натыўную версію на адны канал
Пасунк з заканчыцы паведамляе кожным бінарам, да якога каналу ўжо ўскладнення належыць — напрыклад, development, stage або production. Паколькі гэтае значэнне компілюецца ў бінар, сервер апдэйта можа выбраць, якую серыю пакетаў трэба прыняць. Для Android задаецца значэнне HOT_UPDATER_CHANNEL для кожнага варіянта продукту ў файле manifest; для iOS — у Info.plist для кожнай меты. Налаштаваецца гэта раз на варіянт або мету, і ўсё застаецца такім.
Два глыбокіяя падводныя камені спрабуюць майже ўсіх. OTA працюе толькі у версіях для выпуску, а не ў версіях для дыбагавання. Таксама кожная новая натыўная версія выклікае неабходнасць адправы ўстановлення, каб яе зарэгістраваць як базовы пакет; без гэтага наступная адправка паведамляе
Skipped (no compatible base bundles).
Адправка апдэйта з лініі каманд
Калі все падключана, для публікацыі змены ў JavaScript достатнья адна команда.
npx hot-updater deploy -p android -t 3.0.0 -c development
Флагі маюць наступны значэнне:
-p: платформа,androidабоios.-t: версія цэльвога дапрынаму, з якой сумка сумесна, тое ўсё твае натыўнаеversionName.-c: канал для публікацыі.-f: необавязкова; пазначае апдэйт як прымусовы, тады ён застосоўваецца негайна.
Зберы свой JavaScript, запускай процэс развяртання, і наступны раз, калі хтось ачыне версію з таго канала, апдэйт вялічка чакае.
Інтеграцыя процэсаў развяртання ў скрыпты для кожнага сераўісу
Запісваць гэтыя команды для двух платформ у трох каналах быстра стае вялікай працай, і лёгкаа ўпусціць публікацыю у неправильны канал па адзінаку. Невялікі скрыпт Node, які керуецца зменнымі сераўнавальнай среды і які доступны через npm scripts, усунець обая проблемы.
Элементы package.json задаюць канал і неабяжнае пазначэнне force, а потым запускаюць той самы скрыпт.
{
"scripts": {
"ota:dev": "OTA_CHANNEL=development node scripts/ota-deploy.mjs",
"ota:stage": "OTA_CHANNEL=stage node scripts/ota-deploy.mjs",
"ota:prod": "OTA_CHANNEL=production node scripts/ota-deploy.mjs",
"ota:dev:force": "OTA_CHANNEL=development OTA_FORCE=true node scripts/ota-deploy.mjs",
"ota:stage:force": "OTA_CHANNEL=stage OTA_FORCE=true node scripts/ota-deploy.mjs",
"ota:prod:force": "OTA_CHANNEL=production OTA_FORCE=true node scripts/ota-deploy.mjs"
}
}
У scripts/ota-deploy.mjs сераўнавальная среда задае як канал, так і опцыю force; скрыпт пасля чаго выкарыстоўвае цэльную версію з android/app/build.gradle, як толькі OTA_VERSION не заменіць яе, і па спадчынству размешчае обея платформы. execSync з параметрам stdio: 'inherit' перадае выходны тэкст КЛІ на ваш тэрмінал і выклекае адзінаковы код завершэння, таму незусекнутая размешчэнне на Android зупіняе выконанне пры перадпрыему спробы для iOS.
import { execSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
// Pull the native versionName straight from build.gradle so -t is never stale.
function getVersion() {
if (process.env.OTA_VERSION) return process.env.OTA_VERSION;
const gradle = readFileSync('./android/app/build.gradle', 'utf8');
const match = gradle.match(/versionName\s+["']([^"']+)["']/);
if (!match) throw new Error('Could not find versionName in build.gradle');
return match[1];
}
const version = getVersion();
const channel = process.env.OTA_CHANNEL ?? 'development';
const force = process.env.OTA_FORCE === 'true' ? '-f' : '';
for (const platform of ['android', 'ios']) {
const cmd = `npx hot-updater deploy -p ${platform} -t ${version} -c ${channel} ${force}`.trim();
execSync(cmd, { stdio: 'inherit' });
}
Паўсядзевы процес работы тады скарочваецца да калькі команд.
npm run ota:dev # ship to the dev channel, both platforms
npm run ota:prod # ship to production
npm run ota:prod:force # ship to production, force immediately
Апранэць версію з Gradle замест таго, каб запісваць яе вручную, усуняе цэлы клас выканацьых памылак пад час выпуску: немагчыма випадкова выдати пакет для застарелай цэльвай версіі. Аднак трэба быць уважным да прыпуска, які гэта робить. Скрыпт выкарыстоўвае versionName Android для обох платформ, таму ён працюе толькі тады, калі версія для маркетынгу iOS залишаецца сінхронай. Якщо гэтыя два элементы можуць адстаць, трэба чытаць версію iOS окрема або явна задаць OTA_VERSION. Синтаксіс VAR=value унутро скрыптах npm таксама прыпускае наявнасць POSIX shell; у Windows патрэбны такі інструмент, як cross-env.
Атрыбуты адвярнення і паэтапны выпуск як ваша захоўніца
Швыракае выправленне — толькі палова задача. Другаяя палова — адмежаванне радыю разлету, калі пакет виявляецца неякісным, і самэлькі тут і з’яўляецца значэнне консолі hot-updater.
Мгновенная ануляцыя
Кожны пакет, перапісаны ў консолі, мае прапуск Enabled. Якщо ўсё недавна завезены пакет не працуе, яго трэба выключыць. Апараты, якія ўжо яго інсталавалі, наступны раз, калі пераглядзяць інформацыю, вернуцца да пасляпэўнага працюючага пакета, без перзбірання, перадачы зноў і чакання ад Apple чысто Google. CLI таксама прымаець можлівасць ануляцыі через hot-updater rollback, якая вяртае пакет, які быў апошнім разам выданы.
Гэта галоўная прычына, чаму OTA можна аднойчы надзірваць: для виправлення памылкі трэба калькі секунд. Згадайце, што ануляцыя дасягае апаратоў толькі тады, калі яны зноў пераглядаюць інформацыю, таму механізм перагляду ў фонавым режыме мае такое ж значэнне для вяселення, як і для доставкі.
Этапавыя розгортанні і групы
У замест на тое, каб адразу выдаць пакет усім прыстроям, можна спачатку выдаты яго невеликам частцы прыстроёў, стежыць за звясткамі пра збоі та статыстыкай, а пасля поступова збільшваць частку, пакуль яна не дасягне 100%. Групы дазволяюць нацеліцца на конкрэтную групу, напрыклад внутранюю qa-групу, перш чым інші побачыць змены. У поўнасою ўзаемадзейнасці з пакянькам «Увядзены» гэта ператварае рызыкаванае розгортанне на контролюемы експерымент. Пераканайцеся ў дасведчэннях да Hot-Updater па точныя опцыі налаштавання, адколькі яны можа змяніцца межы версіяма.
Ключовыя выводы
- Расследжваце OTA як шырокі шлях толькі для JavaScript; все, што стосуецца натыўнага коду, все ўсё раве трэбуе будовы для магазіна, а кожная новая будова павінна быть зарэўнавана як базовы пакет.
HotUpdater.init() роўна адназначы ў index.js, і трэба раздзеляць параметры "дзе пераканаліцца" і "чы гэта неабходна".requestTimeout, калі ваш сервер апдэйта є функцыя без сервера, таму што пераканаленне праз час будзе выглядаць адносова да "жаданых апдэйтаў няма".updateBundle(), а ўжывайце іх за дапамойкой reload() окрема, каб корыстнікі не былі перезапускаемы ў падчас виканання задачі.