EAS Update برای OTA در React Native: راهنمای کامل مهاجرت از CodePush در ۲۰۲۶
بعد از بازنشستگی CodePush در مارس ۲۰۲۵، EAS Update به استاندارد عملی OTA در React Native تبدیل شده است. در این راهنما مهاجرت گامبهگام، تنظیم runtime version، تفاوت channel و branch، rollout تدریجی و الگوهای rollback ایمن را با کد آماده پوشش میدهم.
EAS Update سرویس رسمی Expo برای ارسال بهروزرسانیهای OTA (Over-the-Air) در React Native است که به شما اجازه میدهد کد جاوااسکریپت و assetهای اپلیکیشن را بدون عبور از فرآیند بررسی اپاستور بهروز کنید. بعد از بازنشستگی Microsoft CodePush در مارس ۲۰۲۵، EAS Update به استاندارد عملی این حوزه تبدیل شده است. در این راهنما مهاجرت گامبهگام از CodePush، تنظیم runtime version، تفاوت channel و branch، و الگوهای rollout ایمن را با کد قابل اجرا پوشش میدهم. صادقانه بگویم، دو اپ تولیدی را در شش ماه گذشته خودم مهاجرت دادهام و چند جزئیات وجود دارد که در مستندات رسمی خیلی برجسته نشدهاند.
EAS Update جایگزین رسمی CodePush است که در مارس ۲۰۲۵ بازنشسته شد و توسط Expo برای ارسال JS bundle و asset به اپ نصبشده ارائه میشود.
سه مفهوم اصلی EAS Update عبارتاند از runtimeVersion (سازگاری با کد بومی)، branch (جریان توسعه) و channel (مقصد در build).
EAS Update فقط میتواند کد JS و assetها را بهروز کند؛ هر تغییری در native code نیازمند انتشار build جدید در اپاستور است.
برای پروژههای Bare React Native (بدون Expo) هم EAS Update کار میکند، کافی است expo-updates را به صورت دستی نصب و پیکربندی کنید.
الگوی ایمن انتشار شامل rollout تدریجی با درصد، نظارت بر crash از طریق Sentry، و قابلیت rollback فوری با republish روی همان branch است.
هزینه EAS Update در پلن رایگان شامل ۱۰۰۰ MAU است و پلن Production با ۹۹ دلار در ماه تا یک میلیون MAU را پوشش میدهد.
EAS Update چیست و چگونه کار میکند؟
EAS Update یک سرویس میزبانیشده از سوی Expo است که نسخههای جدید کد جاوااسکریپت و assetهای اپلیکیشن (تصاویر، فونتها، JSON) را به اپلیکیشنهای نصبشده روی دستگاه کاربر تحویل میدهد. ساختار آن ساده اما دقیق است: کتابخانه expo-updates داخل اپ هنگام راهاندازی به CDN جهانی EAS وصل میشود، نسخه فعلی JS bundle خود را با آخرین update منتشرشده روی branch مرتبط با channel اپ مقایسه میکند، و در صورت وجود نسخه جدید آن را دانلود و در راهاندازی بعدی فعال میسازد.
این چرخه به شما اجازه میدهد bugهای UI، تغییرات متن، آزمایشهای A/B و حتی فیچرهای کامل JS را در عرض دقایق و بدون انتظار برای بررسی Apple یا Google به دست کاربران برسانید. اما مهم است که محدودیت اصلی را همان ابتدا روشن کنم: EAS Update هرگز نمیتواند کد بومی (Swift, Kotlin, Objective-C++) یا کتابخانههای جدیدی که به native module نیاز دارند را تغییر دهد. هر تغییری در native side به یک build جدید و در نتیجه انتشار جدید در اپاستور نیاز دارد. این محدودیت ربطی به EAS ندارد و قانون اپاستورهاست؛ همین قانون باعث بازنشستگی CodePush هم شد. مستندات رسمی این تفکیک را با عبارت "JS-only updates" مشخص میکند (مستندات رسمی EAS Update).
چرا باید از CodePush مهاجرت کنیم؟
در ۳۱ مارس ۲۰۲۵ مایکروسافت رسماً سرویس App Center و به همراه آن CodePush را بازنشسته کرد. این تصمیم میلیونها اپ React Native را که سالها به CodePush برای OTA متکی بودند مجبور به مهاجرت کرد. سه گزینه عمده در بازار باقی ماند: EAS Update از Expo، نسخه self-hosted CodePush (با fork جامعه)، و سرویسهای ثالثی مثل ReactNative.dev OTA. در عمل اکثریت توسعهدهندگان به EAS Update مهاجرت کردند، به سه دلیل اصلی.
اول، EAS Update نسبت به CodePush از روز اول با معماری مدرن طراحی شده: پشتیبانی native از Hermes engine، delta updates برای کاهش حجم دانلود، و یکپارچگی با EAS Build برای حفظ تطابق runtime version. دوم، Expo SDK رسمی و فعالانه نگهداری میشود؛ CodePush در ماههای پایانی عملاً unmaintained بود. سوم، EAS Update با ابزارهای ناوبری و state موجود در اکوسیستم Expo مثل Expo Router برای مسیریابی فایلمحور به طور یکپارچه کار میکند و نیازی به تنظیم جداگانه bundleResourceName ندارد.
راهاندازی EAS Update از صفر
برای پروژههای Expo (Managed یا Bare) راهاندازی EAS Update چهار مرحله ساده دارد: نصب کتابخانه، پیکربندی app.json، انجام build اولیه، و انتشار اولین update. فرض میکنم EAS CLI نصب است (npm install -g eas-cli) و با اکانت Expo خود وارد شدهاید.
# 1. نصب expo-updates و افزودن پیکربندی پیشفرض
npx expo install expo-updates
eas update:configure
# 2. این دستور به طور خودکار app.json را بهروز میکند:
# - updates.url را به https://u.expo.dev/<project-id> تنظیم میکند
# - runtimeVersion.policy را به "appVersion" تنظیم میکند
# - یک extra.eas.projectId اضافه میکند
# 3. ساخت پروفایل development و production در eas.json
cat > eas.json <<'EOF'
{
"cli": { "version": ">= 13.0.0" },
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"channel": "development"
},
"preview": {
"distribution": "internal",
"channel": "preview"
},
"production": {
"channel": "production",
"autoIncrement": true
}
},
"submit": { "production": {} }
}
EOF
# 4. ساخت اولین binary که با EAS Update sync است
eas build --profile production --platform all
# 5. انتشار اولین update روی channel production
eas update --branch production --message "تغییر متن دکمه ورود"
نکته مهم در مرحله ۳: channel در هر build profile یک قرارداد بین binary و سرور EAS است. هر binary که با channel: "production" ساخته شود، فقط updateهایی را میخواند که روی branchی منتشر شده باشند که به channel "production" نگاشت شده. این نگاشت پیشفرض one-to-one است اما در پنل EAS قابل تغییر است (مثلاً branch staging-v2 را به channel production متصل کنید برای A/B test).
Runtime Version و سازگاری با کد بومی
مهمترین مفهومی که اشتباه فهمیدنش به crash منجر میشود runtimeVersion است. این رشته نسخه کد بومی اپ شماست. هنگام انتشار یک update، EAS فقط آن را به اپهایی تحویل میدهد که runtimeVersion یکسانی دارند. اگر شما یک کتابخانه جدید با native code نصب کنید (مثلاً react-native-mmkv یا کتابخانهای از معماری جدید React Native)، باید runtimeVersion را افزایش دهید — وگرنه update روی نسخه قدیمی اپ نصب میشود که آن native module را ندارد و crash میکند.
سه policy رایج برای runtimeVersion وجود دارد و انتخاب درست به استراتژی انتشار شما بستگی دارد:
Policy
مقدار تولیدشده
کاربرد
هشدار
appVersion
"1.2.0" (از app.json)
تیمهای کوچک با cycle انتشار ماهانه
اگر فراموش کنید version را bump کنید، update به اپ ناسازگار میرود
fingerprint
"a3f8...c9d2" (hash از native files)
توصیهشده برای ۲۰۲۶؛ به طور خودکار با تغییر native code تغییر میکند
نیازمند EAS CLI ≥ 13 و فایل fingerprint.json
nativeVersion
"1.2.0(45)"
تیمهایی که build number را جدا از version مدیریت میکنند
هر build جدید بدون تغییر native هم runtime جدید میسازد
توصیه من برای ۲۰۲۶ بدون استثنا policy fingerprint است. ابزار @expo/fingerprint در زمان build یک hash پایدار از تمام فایلهای native (Podfile.lock، build.gradle، فهرست native moduleها) تولید میکند. این یعنی شما نیازی به مدیریت دستی version ندارید؛ هر تغییر واقعی در native side به طور خودکار باعث جدا شدن updateها میشود و هر تغییر فقط در JS باعث میشود update به همه نسخههای runtime یکسان تحویل داده شود.
تفاوت channel و branch در EAS Update چیست؟
این دو اصطلاح مرتب با هم اشتباه گرفته میشوند، اما تمایز سادهای دارند: branch جریان مداومی از updateهاست (مثل branch در Git)، و channel یک تگ روی binary است که میگوید این binary به کدام branch گوش میدهد. یک channel در هر زمان فقط به یک branch متصل است، اما این اتصال در پنل EAS قابل تغییر است بدون اینکه binary را دوباره بسازید، و این همان قدرت جداسازی است.
مثال عملی: من یک binary با channel: "production" در اپاستور دارم. روزانه updateها روی branch: "production" منتشر میشوند. میخواهم یک fix احتیاطی را روی ۱۰٪ از کاربران آزمایش کنم. کاری که میکنم: یک branch جدید با نام production-canary میسازم و fix را روی آن منتشر میکنم، سپس در پنل EAS یک rollout ۱۰ درصدی از channel production به branch production-canary تعریف میکنم. ۹۰ درصد کاربران هنوز update قبلی را میگیرند، ۱۰ درصد نسخه canary را. اگر مشکلی پیش بیاید، rollout را به ۰٪ کاهش میدهم و همه به branch production برمیگردند.
# انتشار روی یک branch مشخص
eas update --branch production --message "افزودن تأیید دو مرحلهای"
# مشاهده اتصال channel به branch
eas channel:view production
# تغییر اتصال channel به branch دیگر (rollout 25%)
eas channel:edit production --branch production-canary --percent 25
# بازگرداندن سریع: rollout را به 0 برسانید
eas channel:edit production --branch production --percent 100
مهاجرت گامبهگام از CodePush به EAS Update
اگر یک اپ تولیدی با CodePush دارید، فرآیند مهاجرت معمولاً نیم روز کاری میبرد. ترتیبی که در آخرین دو اپی که مهاجرت دادهام طی کردم به این صورت بود.
حذف کامل CodePush از کد بومی: در iOS وابستگی CodePush را از Podfile و AppDelegate.mm حذف کنید. در Android خط apply from: "../../node_modules/react-native-code-push/android/codepush.gradle" و فراخوانی CodePush.getJSBundleFile() در MainApplication.java را حذف کنید.
حذف npm package:npm uninstall react-native-code-push و حذف importها و HOC های codePush() از کد JS.
نصب expo-updates: برای پروژه Bare RN از npx install-expo-modules استفاده کنید سپس npx expo install expo-updates. این کار ExpoUpdatesModule را به native side تزریق میکند.
پیکربندی EAS:eas update:configure را اجرا کنید. فایل app.json یا app.config.js بهروز میشود.
تنظیم channelهای معادل: CodePush معمولاً سه deployment داشت: Production، Staging، Development. در eas.json سه profile با channelهای همنام بسازید.
اولین build و submit: با eas build --profile production یک binary جدید بسازید و در اپاستور submit کنید. این نسخه اولین نسخهای است که میتواند update از EAS بگیرد.
دوره موازی: به مدت یک نسخه minor، هم CodePush (در نسخههای قدیمی) و هم EAS (در نسخه جدید) فعال نگه دارید تا کاربرانی که هنوز update نکردهاند پشتیبانی شوند. پس از رسیدن نرخ adoption به ۹۵٪، CodePush را خاموش کنید.
انتشار، rollout تدریجی و rollback
الگوی انتشاری که به تیمها توصیه میکنم شامل سه مرحله است: انتشار به internal، rollout تدریجی، و نظارت با telemetry. هرگز یک update را به ۱۰۰٪ کاربران تولیدی push نکنید بدون اینکه حداقل ۲۴ ساعت روی preview channel آزمایش شده باشد. کنار EAS Update، یکپارچگی با Sentry یا یک سیستم مشابه ضروری است؛ بدون آن متوجه نمیشوید update جدید crash rate را بالا برده.
# مرحله ۱: انتشار روی preview برای تیم داخلی
eas update --branch preview --message "v1.4.0-rc1: refactor checkout"
# مرحله ۲: پس از تأیید QA، انتشار روی branch production-canary
eas update --branch production-canary \
--message "v1.4.0: refactor checkout" \
--auto
# مرحله ۳: rollout تدریجی از channel production
eas channel:edit production --branch production-canary --percent 5
# منتظر ۲ ساعت، چک crash dashboard
eas channel:edit production --branch production-canary --percent 25
# منتظر ۴ ساعت
eas channel:edit production --branch production-canary --percent 100
# rollback فوری در صورت مشکل
eas channel:edit production --branch production --percent 100
# یا republish نسخه قبلی روی production
eas update:republish --branch production --group <previous-update-group-id>
نکتهای که در تجربه شخصی برایم مهم بوده: --auto به طور خودکار branch فعلی Git را به عنوان نام branch EAS انتخاب میکند و commit message را به عنوان پیام update استفاده میکند. این برای CI/CD ایدهآل است. در GitHub Actions میتوانید یک workflow بسازید که روی هر push به main به طور خودکار eas update --auto اجرا کند و روی preview channel منتشر کند.
کنترل بهروزرسانی در JS با useUpdates
برای ارائه تجربه کاربری بهتر، میتوانید با hook های expo-updates به طور برنامهای updateها را چک، دانلود و فعال کنید. این قابلیت به ویژه برای اپهای مالی یا پزشکی که نیاز به تأیید کاربر برای بهروزرسانی دارند مفید است. کنترل دستی به شما اجازه میدهد update را در پسزمینه دانلود کنید و فقط زمانی reload کنید که کاربر در یک حالت ایمن قرار دارد (مثل صفحه اصلی، نه وسط تراکنش).
اگر در مدیریت state اپ خود از Zustand یا TanStack Query استفاده میکنید (جزئیات در راهنمای مدیریت State در React Native)، میتوانید قبل از reloadAsync به طور خودکار stateهای persisted را flush کنید تا دادههای حساس کاربر در فرآیند restart از دست نرود.
EAS Update در پروژههای Bare React Native
یک باور رایج اشتباه این است که EAS Update فقط برای پروژههای Managed Expo کار میکند. در واقع هر پروژه React Native (حتی اپلیکیشنهایی که سالها قبل بدون Expo شروع شدهاند) میتواند از EAS Update استفاده کند. کافی است expo-modules-core و expo-updates را به صورت دستی نصب و پیکربندی کنید. این برای تیمهایی که نمیخواهند مهاجرت کامل به Expo SDK داشته باشند گزینه خوبی است.
# 1. نصب expo modules در پروژه bare
npx install-expo-modules@latest
# 2. نصب expo-updates
npm install expo-updates
# 3. در iOS داخل Podfile:
# pod 'EXUpdates', :path => '../node_modules/expo-updates'
cd ios && pod install && cd ..
# 4. در Android، MainApplication.kt را تغییر دهید:
# override fun getJSBundleFile(): String? = UpdatesController.getInstance()
# .launchAssetFile
# 5. در AppDelegate.mm (iOS):
# - (NSURL *)bundleURL {
# return [[EXUpdatesAppController sharedInstance] launchAssetUrl];
# }
# 6. eas.json را بسازید و channelها را تعریف کنید
eas update:configure
تفاوت اصلی نسبت به Managed: شما باید خودتان native side را پیکربندی کنید و بعد از هر pod install یا تغییر در build.gradle ممکن است نیاز به update پیکربندی داشته باشید. اما در عوض کنترل کامل روی native code خود را حفظ میکنید. تیمهای بزرگی مثل Discord و Shopify از این مدل ترکیبی Bare + EAS Update استفاده میکنند.
قیمتگذاری، MAU و محدودیتهای پلن
EAS Update بر اساس Monthly Active Users (MAU) قیمتگذاری میشود، نه تعداد updateها یا حجم bundle. MAU بر اساس تعداد دستگاههای منحصربهفردی که در یک ماه حداقل یکبار اپ را باز کردهاند محاسبه میشود. این مدل برای اکثر اپها منصفانهتر از مدل CodePush قدیمی (که بر اساس حجم bandwidth بود) است.
بر اساس قیمتگذاری منتشرشده توسط Expo در ۲۰۲۶، پلن Free شامل ۱۰۰۰ MAU است، پلن Production با ۹۹ دلار در ماه تا یک میلیون MAU را پوشش میدهد و بعد از آن ۲ دلار به ازای هر ۱۰۰۰ MAU اضافه میشود. پلن Enterprise قیمتگذاری سفارشی دارد و شامل SLA، single tenant و امکان self-hosting CDN است. مقایسه دقیق پلنها در صفحه قیمتگذاری رسمی Expo در دسترس است.
سؤالات متداول
آیا EAS Update برای React Native CLI (بدون Expo) کار میکند؟
بله. شما میتوانید expo-modules-core و expo-updates را در یک پروژه React Native CLI نصب کنید و EAS Update به طور کامل کار میکند. تنها لازم است تنظیمات Podfile و MainApplication را به صورت دستی انجام دهید. این مسیر توسط Expo رسماً پشتیبانی میشود و توسط تیمهای بزرگی مثل Shopify در تولید استفاده میشود.
حداکثر حجم یک update در EAS Update چقدر است؟
EAS Update محدودیت سختی روی حجم ندارد اما برای حفظ تجربه کاربری توصیه میشود total bundle زیر ۲۰ مگابایت بماند. EAS از delta updates پشتیبانی میکند، یعنی فقط فرقهای نسبت به نسخه قبلی دانلود میشوند و این معمولاً حجم واقعی دانلود را به چند صد کیلوبایت کاهش میدهد.
آیا میتوانم با EAS Update یک کتابخانه جدید با native code اضافه کنم؟
خیر. EAS Update فقط کد JS و assetهای static را بهروز میکند. هر کتابخانهای که شامل native module جدید است (مثل react-native-mmkv یا یک wrapper روی Bluetooth) نیاز به build جدید و submission مجدد به اپاستور دارد. این قانون اپاستورهاست، نه محدودیت EAS.
چگونه یک update را در صورت crash به سرعت rollback کنم؟
دو روش وجود دارد: یا eas channel:edit را اجرا کنید و channel را به branch قبلی برگردانید (سریعترین راه)، یا از eas update:republish با group ID نسخه پایدار قبلی استفاده کنید. هر دو روش در عرض چند دقیقه به همه کاربران رسانده میشوند. توصیه میکنم همیشه آخرین update پایدار را در یک branch جدا نگه دارید.
EAS Update به App Tracking Transparency نیاز دارد؟
خیر. EAS Update هیچ identifier تبلیغاتی (IDFA) یا داده شخصی کاربر را جمعآوری نمیکند. تنها داده ارسالی شامل runtime version، channel و یک device ID ناشناس برای محاسبه MAU است. این کاملاً با سیاست App Tracking Transparency اپل و GDPR سازگار است و نیازی به نمایش prompt به کاربر ندارد.