expo-apk-standalone
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAPK 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秒识别)
- Mở app → splash rồi trắng hình/tự thoát.
- 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 ở (máy thật không có) nên chết.
localhost:8081- 打开应用→启动页后白屏/自动退出。
- 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,它正在寻找位于的Metro开发服务器(真机上没有),因此崩溃。
localhost:8081Nguyên nhân gốc
根本原因
./gradlew assembleDebugexpo export./gradlew assembleDebugexpo exportCá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 ) chèn vào block của :
expo prebuild --cleanreact {}android/app/build.gradle| RN version | Property đúng trong |
|---|---|
| RN ≤ 0.7x | |
| RN 0.76+ / Expo SDK 5x (0.86) | |
必须通过配置插件将JS嵌入debug APK(需执行生效),将配置添加到的块中:
expo prebuild --cleanandroid/app/build.gradlereact {}| RN版本 | |
|---|---|
| RN ≤ 0.7x | |
| RN 0.76+ / Expo SDK 5x (0.86) | |
⚠️ 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 , 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):
bundleInDebug = truebash
undefined曾遇到过真实错误(CI在配置阶段失败):添加,查看生成文件显示通过,但gradle拒绝,因为RN 0.86已移除该属性。直接在项目的node_modules中验证(无需完整构建):
bundleInDebug = truebash
undefinedProperty 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
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
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
node_modules/@react-native/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/TaskConfiguration.kt
undefinedgrep -n "debuggableVariants"
node_modules/@react-native/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/TaskConfiguration.kt
node_modules/@react-native/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/TaskConfiguration.kt
undefinedTemplate plugin (generic — thay <project>
bằng đường dẫn project của bạn)
<project>通用插件模板(将<project>
替换为你的项目路径)
<project>Tạo :
<project>/plugins/embed-js-in-debug.jsjs
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 → : thêm .
app.jsonplugins"./plugins/embed-js-in-debug"创建:
<project>/plugins/embed-js-in-debug.jsjs
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.jsonplugins"./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/<project>/android//<project>/ios/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/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→ OK.FATAL - Còn → bundle chưa vào APK (kiểm tra lại bước verify).
Unable to load script
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 - 仍出现→ bundle未嵌入APK(重新检查验证步骤)。
Unable to load script
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:
- 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.
- App identity chốt trước build lần đầu: ,
android.package,ios.bundleIdentifier,name, 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.scheme - Config plugin native phải verify API theo đúng RN version (grep ) — không copy property từ bài blog/version cũ (
ReactExtension.kt→bundleInDebug).debuggableVariants - 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".
- trước khi mở app để log sạch, dễ đọc lỗi.
adb logcat -c
在设计构建流水线阶段就确定以下事项,不要等用户安装到真机才发现问题:
- 产物是独立运行还是开发模式? 如果用户将自行安装APK测试(没有运行Metro)→ 从第一次构建开始就必须嵌入bundle(debuggableVariants/bundleInDebug/release)。
- 应用标识在第一次构建前确定:、
android.package、ios.bundleIdentifier、name、keystore。安装应用后再修改包名=用户必须卸载旧版本,丢失本地数据,旧版本仍会留存(包名不同=两个应用共存)。标识是设计层面的决定,不是事后修改的内容。scheme - 原生配置插件必须根据RN版本验证API(搜索)——不要复制旧博客/旧版本的属性(
ReactExtension.kt→bundleInDebug)。debuggableVariants - 构建成功≠能运行——新流水线第一次构建完成后:立即安装到真机+查看logcat+截图,再告知用户“完成”。
- 打开应用前执行清空日志,便于读取错误信息。
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