expo-apk-standalone

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

APK chạy standalone trên máy thật (không cần Metro) — dùng chung cho mọi project Expo/RN

可在真机独立运行的APK(无需Metro)——适用于所有Expo/RN项目

Khi nào dùng skill này

何时使用此方案

  • User báo: "cài app vào phone rồi không mở được / mở lên bị trắng hình / tự tắt".
  • Vừa build APK (local hoặc CI) và sắp cài lên máy thật test.
  • Đang THIẾT KẾ build pipeline cho app Expo/RN — quyết định artifact chạy thế nào NGAY TỪ ĐẦU (bài học "tránh code xong đi sửa").
  • 用户反馈:“应用安装到手机后无法打开/打开后白屏/自动退出”。
  • 刚完成APK构建(本地或CI),即将安装到真机测试。
  • 正在为Expo/RN应用设计构建流水线——从一开始就确定产物的运行方式(吸取“写完代码再修改”的教训)。

Triệu chứng (nhận diện trong 5 giây)

症状(5秒识别)

  1. Mở app → splash rồi trắng hình/tự thoát.
  2. Logcat có 1 trong các dòng sau:
Unable to load script. Make sure you're running Metro or that your bundle
'index.android.bundle' is packaged correctly for release.

W unknown:ReconnectingWebSocket: Couldn't connect to "ws://localhost:8081/..."
ReactHost: ... Fault reason: Unable to load script.
Diagnosis: APK là DEBUG build mà KHÔNG nhúng JS bundle, nó đang tìm Metro dev server ở
localhost:8081
(máy thật không có) nên chết.
  1. 打开应用→启动页后白屏/自动退出
  2. Logcat中出现以下任意一行:
Unable to load script. Make sure you're running Metro or that your bundle
'index.android.bundle' is packaged correctly for release.

W unknown:ReconnectingWebSocket: Couldn't connect to "ws://localhost:8081/..."
ReactHost: ... Fault reason: Unable to load script.
诊断结果:该APK为DEBUG构建但未嵌入JS bundle,它正在寻找位于
localhost:8081
的Metro开发服务器(真机上没有),因此崩溃。

Nguyên nhân gốc

根本原因

./gradlew assembleDebug
(mặc định của workflow GH Actions no-EAS) KHÔNG bundle JS vào APK — bundle được load từ Metro lúc chạy. Cài lên máy thật không có Metro = app không thể mở. Build "thành công" trên CI ≠ APK chạy được (chỉ bundle JS qua
expo export
cũng không bắt được lỗi này).
./gradlew assembleDebug
(无EAS的GH Actions工作流默认命令)不会将JS bundle打包到APK中——bundle是运行时从Metro加载的。安装到没有Metro的真机上=应用无法打开。CI上“构建成功”≠APK能运行(仅通过
expo export
打包JS也无法发现此错误)。

Cách fix (áp dụng chung — CHỌN THEO RN VERSION)

修复方法(通用型——根据RN版本选择)

Bắt buộc nhúng JS vào debug APK bằng config plugin (sống sót qua
expo prebuild --clean
) chèn vào
react {}
block của
android/app/build.gradle
:
RN versionProperty đúng trong
react {}
RN ≤ 0.7x
bundleInDebug = true
RN 0.76+ / Expo SDK 5x (0.86)
debuggableVariants = []
bundleInDebug
ĐÃ BỊ XÓA khỏi
ReactExtension
, gradle fail
Could not set unknown property 'bundleInDebug'
必须通过配置插件将JS嵌入debug APK(需执行
expo prebuild --clean
生效),将配置添加到
android/app/build.gradle
react {}
块中:
RN版本
react {}
中对应的属性
RN ≤ 0.7x
bundleInDebug = true
RN 0.76+ / Expo SDK 5x (0.86)
debuggableVariants = []
——
bundleInDebug
已从
ReactExtension
中移除,使用会导致gradle报错
Could not set unknown property 'bundleInDebug'

⚠️ BƯỚC BẮT BUỘC: verify property theo đúng RN version TRƯỚC khi chốt (không copy từ blog cũ)

⚠️ 必做步骤:确定RN版本后再验证属性(不要复制旧博客内容)

Lỗi từng mắc thật (CI fail ở configuration phase): chèn
bundleInDebug = true
, grep file sinh ra thấy pass, nhưng gradle reject vì RN 0.86 đã xóa property. Verify trong chính node_modules của project (không cần build full):
bash
undefined
曾遇到过真实错误(CI在配置阶段失败):添加
bundleInDebug = true
,查看生成文件显示通过,但gradle拒绝,因为RN 0.86已移除该属性。直接在项目的node_modules中验证(无需完整构建):
bash
undefined

Property nào thực sự tồn tại trong RN version hiện tại?

当前RN版本实际存在哪些属性?

grep -n "debuggableVariants|bundleInDebug"
node_modules/@react-native/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactExtension.kt
grep -n "debuggableVariants|bundleInDebug"
node_modules/@react-native/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactExtension.kt

Cách plugin dùng property đó (debuggableVariants -> variant KHÔNG được bundle):

插件如何使用该属性(debuggableVariants → 不被打包的变体):

grep -n "debuggableVariants"
node_modules/@react-native/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/TaskConfiguration.kt
undefined
grep -n "debuggableVariants"
node_modules/@react-native/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/TaskConfiguration.kt
undefined

Template plugin (generic — thay
<project>
bằng đường dẫn project của bạn)

通用插件模板(将
<project>
替换为你的项目路径)

Tạo
<project>/plugins/embed-js-in-debug.js
:
js
const { withAppBuildGradle } = require('@expo/config-plugins');

module.exports = function withEmbedJsInDebug(config) {
  return withAppBuildGradle(config, (config) => {
    const contents = config.modResults.contents;
    // Guard bằng MARKER RIÊNG của plugin — không guard bằng tên property:
    // file build.gradle sinh ra ĐÃ có sẵn comment mẫu `// debuggableVariants = [...]`,
    // guard theo tên property = no-op vĩnh viễn (bug thật đã gặp).
    if (contents.includes('// bundle JS into the debug APK')) return config;

    const updated = contents.replace(
      /react\s*\{/,
      'react {\n        // bundle JS into the debug APK so it runs standalone without Metro (added by embed-js-in-debug)\n        debuggableVariants = []'  // đổi theo RN version, xem bảng trên
    );
    if (updated === contents) {
      throw new Error('embed-js-in-debug: could not find "react {" block in app/build.gradle');
    }
    config.modResults.contents = updated;
    return config;
  });
};
Đăng ký trong
app.json
plugins
: thêm
"./plugins/embed-js-in-debug"
.
创建
<project>/plugins/embed-js-in-debug.js
js
const { withAppBuildGradle } = require('@expo/config-plugins');

module.exports = function withEmbedJsInDebug(config) {
  return withAppBuildGradle(config, (config) => {
    const contents = config.modResults.contents;
    // 使用插件专属标记作为防护——不要用属性名防护:
    // 生成的build.gradle文件已包含示例注释`// debuggableVariants = [...]`,
    // 按属性名防护会导致永远不生效(曾遇到的真实bug)。
    if (contents.includes('// bundle JS into the debug APK')) return config;

    const updated = contents.replace(
      /react\s*\{/,
      'react {\n        // bundle JS into the debug APK so it runs standalone without Metro (added by embed-js-in-debug)\n        debuggableVariants = []'  // 根据RN版本修改,参考上方表格
    );
    if (updated === contents) {
      throw new Error('embed-js-in-debug: could not find "react {" block in app/build.gradle');
    }
    config.modResults.contents = updated;
    return config;
  });
};
app.json
plugins
中注册:添加
"./plugins/embed-js-in-debug"

Verify TRƯỚC khi push (bắt lỗi ngay, không đợi CI)

推送前验证(提前发现错误,不要等CI)

bash
cd <project>
npx expo prebuild --platform android --no-install   # sinh android/ cục bộ
grep -n -A4 "react {" android/app/build.gradle      # phải thấy debuggableVariants = [] / bundleInDebug = true
grep -n "applicationId" android/app/build.gradle    # package phải đúng
grep -n "app_name" android/app/src/main/res/values/strings.xml  # tên app phải đúng
.gitignore
phải có
/<project>/android/
+
/<project>/ios/
(prebuild-generated, CI tự sinh lại — đừng commit).
bash
cd <project>
npx expo prebuild --platform android --no-install   # 本地生成android/目录
grep -n -A4 "react {" android/app/build.gradle      # 必须看到debuggableVariants = [] / bundleInDebug = true
grep -n "applicationId" android/app/build.gradle    # 包名必须正确
grep -n "app_name" android/app/src/main/res/values/strings.xml  # 应用名称必须正确
.gitignore
中必须包含
/<project>/android/
+
/<project>/ios/
(prebuild生成的文件,CI会重新生成——不要提交)。

Flow kiểm tra trên máy thật (adb)

真机测试流程(adb)

bash
adb devices -l                              # máy có kết nối?
adb install -r app-debug.apk                # -r = cài đè (bản cũ)
adb shell monkey -p <package> -c android.intent.category.LAUNCHER 1   # mở app
sleep 4
adb shell "dumpsys activity activities | grep -E 'mResumedActivity|topResumedActivity'"  # app có ở foreground?
adb logcat -d | grep -iE "FATAL|Unable to load script|ReactNativeJS|E ReactNative" | tail -30
adb exec-out screencap -p > /tmp/screen.png  # chụp màn hình kiểm tra
  • App ở foreground + logcat không có
    Unable to load script
    /
    FATAL
    → OK.
  • Còn
    Unable to load script
    → bundle chưa vào APK (kiểm tra lại bước verify).
bash
adb devices -l                              # 设备已连接?
adb install -r app-debug.apk                # -r = 覆盖安装(旧版本)
adb shell monkey -p <package> -c android.intent.category.LAUNCHER 1   # 打开应用
sleep 4
adb shell "dumpsys activity activities | grep -E 'mResumedActivity|topResumedActivity'"  # 应用是否在前台?
adb logcat -d | grep -iE "FATAL|Unable to load script|ReactNativeJS|E ReactNative" | tail -30
adb exec-out screencap -p > /tmp/screen.png  # 截图检查
  • 应用在前台 + logcat中无
    Unable to load script
    /
    FATAL
    → 正常。
  • 仍出现
    Unable to load script
    → bundle未嵌入APK(重新检查验证步骤)。

Design-time checklist (tránh "code xong đi sửa mất thời gian")

设计阶段检查清单(避免“写完代码再修改浪费时间”)

Chốt NHỮNG ĐIỀU NÀY Ở GIAI ĐOẠN THIẾT KẾ build pipeline, KHÔNG đợi user cài vào máy mới phát hiện:
  1. Artifact chạy standalone hay dev-mode? Nếu user sẽ tự cài APK test (không có Metro chạy) → phải nhúng bundle (debuggableVariants/bundleInDebug/release) ngay từ build đầu tiên.
  2. App identity chốt trước build lần đầu:
    android.package
    ,
    ios.bundleIdentifier
    ,
    name
    ,
    scheme
    , keystore. Đổi package SAU khi đã cài app = user phải gỡ bản cũ, mất dữ liệu local, bản cũ tồn đọng (package khác = 2 app cạnh nhau). Identity là quyết định thiết kế, không phải chỗ đổi sau.
  3. Config plugin native phải verify API theo đúng RN version (grep
    ReactExtension.kt
    ) — không copy property từ bài blog/version cũ (
    bundleInDebug
    debuggableVariants
    ).
  4. Build success ≠ chạy được — sau build đầu tiên của pipeline mới: cài lên máy + logcat + screencap NGAY, trước khi báo user "xong".
  5. adb logcat -c
    trước khi mở app
    để log sạch, dễ đọc lỗi.
在设计构建流水线阶段就确定以下事项,不要等用户安装到真机才发现问题:
  1. 产物是独立运行还是开发模式? 如果用户将自行安装APK测试(没有运行Metro)→ 从第一次构建开始就必须嵌入bundle(debuggableVariants/bundleInDebug/release)。
  2. 应用标识在第一次构建前确定
    android.package
    ios.bundleIdentifier
    name
    scheme
    、keystore。安装应用后再修改包名=用户必须卸载旧版本,丢失本地数据,旧版本仍会留存(包名不同=两个应用共存)。标识是设计层面的决定,不是事后修改的内容。
  3. 原生配置插件必须根据RN版本验证API(搜索
    ReactExtension.kt
    )——不要复制旧博客/旧版本的属性(
    bundleInDebug
    debuggableVariants
    )。
  4. 构建成功≠能运行——新流水线第一次构建完成后:立即安装到真机+查看logcat+截图,再告知用户“完成”。
  5. 打开应用前执行
    adb logcat -c
    清空日志,便于读取错误信息。

Liên hệ

相关链接

  • Quy trình build APK qua GH Actions (generic): skill
    expo-build-debug-apk-gh
    .
  • Native build failure (Kotlin metadata, pin dependency): skill
    gh-actions-expo-apk-build
    .
  • 通过GH Actions构建APK的流程(通用型):方案
    expo-build-debug-apk-gh
  • 原生构建失败(Kotlin元数据、依赖锁定):方案
    gh-actions-expo-apk-build