🔧 Expo 迁移故障排除与学习总结#
📋 文档概述#
本文档记录了从 React Native CLI 项目迁移到 Expo Bare Workflow 过程中遇到的所有问题、解决方案和学习要点,供未来参考和复盘使用。
项目背景:
- 原项目:React Native 0.71.10 + 复杂原生模块(RFID功能)
- 目标:Expo SDK 50 Bare Workflow
- 迁移时间:2025年7月23日
🚀 完整迁移步骤与方案#
阶段1:项目分析与准备#
1.1 原项目结构分析#
1
2
3
4
5
6
7
8
9
10
11
12
13
|
原项目结构 (App/):
├── app/ # 主要应用代码
│ ├── App.tsx # 应用入口
│ ├── components/ # 组件库
│ ├── navigation/ # 导航配置
│ ├── redux/ # 状态管理
│ └── ...
├── android/ # Android 原生代码
│ └── app/src/main/java/com/inventory/
│ ├── RFIDWithUHFUARTModule.java # RFID UART 模块
│ └── RFIDWithUHFBLEModule.java # RFID BLE 模块
├── ios/ # iOS 原生代码
└── package.json # 依赖配置
|
1.2 关键依赖识别#
原始关键依赖:
1
2
3
4
5
6
7
8
|
{
"react-native": "0.71.10",
"react-native-quick-sqlite": "^8.0.0-beta.2",
"react-native-fs": "^2.20.0",
"react-native-vision-camera": "^2.15.4",
"@react-navigation/native": "^6.1.0",
"react-redux": "^8.0.5"
}
|
需要迁移的模块:
react-native-quick-sqlite → expo-sqlite
react-native-fs → expo-file-system
react-native-vision-camera → expo-camera
阶段2:新项目创建与基础配置#
2.1 创建 Expo Bare Workflow 项目#
1
2
3
4
|
# 在 Inventory 根目录下创建新项目
npx create-expo-app InventoryExpo --template bare-minimum
cd InventoryExpo
|
2.2 核心配置文件设置#
app.json 配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
{
"expo": {
"name": "Inventory",
"slug": "inventory-app",
"version": "0.0.1",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "vg.zeta.app.inventory",
"buildNumber": "1"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#FFFFFF"
},
"package": "vg.zeta.app.inventory",
"versionCode": 1
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}
|
eas.json 构建配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
{
"cli": {
"version": ">= 7.8.0"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal"
},
"production": {}
},
"submit": {
"production": {}
}
}
|
metro.config.js 配置:
1
2
3
4
5
|
const { getDefaultConfig } = require('@expo/metro-config');
const config = getDefaultConfig(__dirname);
module.exports = config;
|
babel.config.js 配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
module.exports = {
presets: ['babel-preset-expo'],
plugins: [
'macros',
[
'react-native-reanimated/plugin',
{
// globals: ['__scanCodes'],
},
],
[
'module-resolver',
{
root: ['./'],
alias: {
'@app': './app',
'@deps': './deps',
},
},
],
],
};
|
2.3 依赖安装#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
# 安装 Expo SDK 50 核心依赖
npm install expo@~50.0.0
# 安装替代模块
npm install expo-sqlite@~13.0.0
npm install expo-file-system@~16.0.0
npm install expo-camera@~14.0.0
# 安装其他必要依赖
npm install expo-status-bar@~1.11.0
npm install expo-splash-screen@~0.26.0
# React Navigation
npm install @react-navigation/native@^6.1.0
npm install @react-navigation/native-stack@^6.9.0
npm install @react-navigation/stack@^6.3.0
# React Native 兼容包
npm install react-native-safe-area-context@4.6.3
npm install react-native-screens@~3.22.0
npm install react-native-gesture-handler@~2.12.0
# Web 支持
npm install react-native-web@~0.19.6
npm install react-dom@18.2.0
npm install @expo/metro-runtime@~3.1.3
# 开发依赖
npm install --save-dev @babel/core@^7.20.0
npm install --save-dev babel-preset-expo@~9.5.0
npm install --save-dev @tsconfig/react-native@^3.0.6
npm install --save-dev typescript@^5.1.3
npm install --save-dev babel-plugin-macros@^3.1.0
|
阶段3:代码迁移策略#
3.1 自动化迁移脚本创建#
创建批处理脚本 (migrate-code.bat):
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@echo off
chcp 65001 > nul
echo 正在迁移应用代码...
robocopy "..\App\app" "app" /E /XD node_modules .git
robocopy "..\App\deps" "deps" /E /XD node_modules .git
echo 正在复制原生模块...
robocopy "..\App\android\app\src\main\java" "android\app\src\main\java" /E
robocopy "..\App\ios" "ios" *.h *.m *.mm /S
echo 代码迁移完成!
pause
|
3.2 资源文件处理#
1
2
3
4
5
|
# 复制并调整图标文件
cp ../App/android/app/src/main/res/mipmap-hdpi/ic_launcher.png assets/icon.png
cp ../App/android/app/src/main/res/mipmap-hdpi/ic_launcher.png assets/adaptive-icon.png
cp ../App/android/app/src/main/res/mipmap-hdpi/ic_launcher.png assets/splash.png
cp ../App/android/app/src/main/res/mipmap-hdpi/ic_launcher.png assets/favicon.png
|
3.3 入口文件适配#
index.js (Expo 入口):
1
2
3
4
5
|
import { registerRootComponent } from 'expo';
import App from './App';
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
registerRootComponent(App);
|
App.js (简化版本):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { StyleSheet, Text, View, Platform } from 'react-native';
export default function App() {
return (
<View style={styles.container}>
<Text style={styles.title}>Inventory App (Expo)</Text>
<Text style={styles.subtitle}>迁移成功!</Text>
<Text style={styles.platform}>Platform: {Platform.OS}</Text>
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
padding: 20,
},
// ... 其他样式
});
|
阶段4:原生项目生成与配置#
4.1 生成原生项目#
1
2
3
4
5
6
|
# 生成 Android 和 iOS 项目
npx expo prebuild
# 验证生成结果
ls android/ # 应该看到 Android 项目结构
ls ios/ # 应该看到 iOS 项目结构
|
4.2 原生模块集成#
Android 模块集成:
- 复制 RFID 模块文件到正确位置
- 更新
android/settings.gradle
- 更新
android/app/build.gradle
- 注册模块到
MainApplication.java
iOS 模块集成:
- 复制
.h 和 .m 文件
- 更新
ios/Podfile
- 运行
cd ios && pod install
阶段5:API 迁移与适配#
5.1 数据库 API 迁移#
原始代码 (react-native-quick-sqlite):
1
2
3
|
import { open } from 'react-native-quick-sqlite';
const db = open({ name: 'mydb.db' });
db.execute('SELECT * FROM items');
|
迁移后 (expo-sqlite):
1
2
3
4
5
6
7
|
import * as SQLite from 'expo-sqlite';
const db = SQLite.openDatabase('mydb.db');
db.transaction(tx => {
tx.executeSql('SELECT * FROM items', [], (_, results) => {
// 处理结果
});
});
|
5.2 文件系统 API 迁移#
原始代码 (react-native-fs):
1
2
|
import RNFS from 'react-native-fs';
RNFS.writeFile(path, data, 'utf8');
|
迁移后 (expo-file-system):
1
2
|
import * as FileSystem from 'expo-file-system';
FileSystem.writeAsStringAsync(uri, data);
|
5.3 相机 API 迁移#
原始代码 (react-native-vision-camera):
1
|
import { Camera } from 'react-native-vision-camera';
|
迁移后 (expo-camera):
1
|
import { Camera } from 'expo-camera';
|
阶段6:测试与验证#
6.1 基础功能测试#
1
2
3
4
5
6
7
8
9
10
11
|
# 启动开发服务器
npx expo start
# Android 测试
npx expo run:android
# iOS 测试
npx expo run:ios
# Web 测试
npx expo start --web
|
6.2 原生模块测试#
1
2
3
4
5
6
7
8
|
// 测试 RFID 模块是否正常工作
import { NativeModules } from 'react-native';
const { RFIDWithUHFUARTModule } = NativeModules;
// 测试模块调用
RFIDWithUHFUARTModule.testConnection()
.then(result => console.log('RFID 模块工作正常:', result))
.catch(error => console.error('RFID 模块错误:', error));
|
阶段7:生产构建准备#
7.1 EAS 构建配置#
1
2
3
4
5
6
7
8
|
# 安装 EAS CLI
npm install -g @expo/eas-cli
# 配置构建
eas build:configure
# 构建测试版本
eas build --platform android --profile preview
|
7.2 代码签名与发布准备#
- Android: 配置 keystore
- iOS: 配置 provisioning profiles
- 更新应用图标和启动画面
🎯 迁移方案选择与决策#
方案对比分析#
方案A:Expo Managed Workflow#
优点:
缺点:
- 无法使用自定义原生模块
- ❌ 不支持现有的 RFID 模块
决策: ❌ 不可行 - 由于项目依赖自定义 RFID 原生模块
方案B:Expo Bare Workflow (选择的方案)#
优点:
- ✅ 保留自定义原生模块支持
- ✅ 享受 Expo 开发工具链
- ✅ 支持 EAS 构建和更新
- ✅ 兼容现有 React Native 代码
缺点:
决策: ✅ 最佳选择 - 平衡了功能需求和开发体验
方案C:保持 React Native CLI#
优点:
缺点:
决策: ❌ 不推荐 - 无法享受 Expo 的现代化开发体验
技术栈选择说明#
Expo SDK 版本选择#
- 选择: Expo SDK 50
- 原因:
- 支持 React Native 0.72.x
- 满足 Android API 34+ 要求(Google Play 新要求)
- 长期支持版本
依赖替换策略#
| 原始依赖 |
Expo 替代方案 |
迁移难度 |
功能对比 |
| react-native-quick-sqlite |
expo-sqlite |
中 |
功能相当,API 略有差异 |
| react-native-fs |
expo-file-system |
中 |
功能受限但满足基本需求 |
| react-native-vision-camera |
expo-camera |
高 |
功能简化,需要重写相机逻辑 |
| react-native-image-picker |
expo-image-picker |
低 |
API 相似,易于迁移 |
🚨 主要问题分类#
1. 依赖冲突问题#
问题1.1: babel-plugin-macros 缺失#
1
|
ERROR: Cannot find module 'babel-plugin-macros'
|
原因分析:
- Babel 配置中使用了 ‘macros’ 插件但未安装依赖
- Metro bundler 在转换代码时找不到对应的插件
解决方案:
1
|
npm install babel-plugin-macros --save-dev
|
学习要点:
- Expo 项目的 Babel 配置需要确保所有插件都正确安装
babel-plugin-macros 是处理编译时宏的重要工具
问题1.2: TypeScript 配置缺失#
1
|
ERROR: Cannot find module '@tsconfig/react-native'
|
解决方案:
1
|
npm install @tsconfig/react-native --save-dev
|
学习要点:
- Expo 推荐使用预设的 TypeScript 配置
- 这个包提供了 React Native 项目的最佳实践配置
2. 权限与安全问题#
问题2.1: Android 权限拒绝错误#
1
|
ERROR: Permission Denial: registerScreenCaptureObserver requires android.permission.DETECT_SCREEN_CAPTURE
|
尝试的解决方案:
- 方案A: 添加权限到 app.json
1
2
3
4
5
6
7
|
{
"android": {
"permissions": [
"android.permission.DETECT_SCREEN_CAPTURE"
]
}
}
|
- 方案B: 移除有问题的插件
- 方案C: 简化权限列表
1
2
3
4
5
6
7
8
|
{
"android": {
"permissions": [
"android.permission.INTERNET",
"android.permission.ACCESS_NETWORK_STATE"
]
}
}
|
问题持续原因:
- 某些 Expo SDK 模块在初始化时会尝试注册系统级观察者
- 这些操作需要特殊的系统权限,不是普通应用权限能解决的
学习要点:
- Android 权限分为普通权限和特殊权限
- 某些 Expo 模块可能有隐式的权限要求
- 权限问题需要从源头(依赖选择)解决,而不仅仅是配置
3. 应用注册问题#
问题3.1: AppRegistry 注册失败#
1
2
3
|
ERROR: "main" has not been registered. This can happen if:
* Metro (the local dev server) is run from the wrong folder
* A module failed to load due to an error and `AppRegistry.registerComponent` wasn't called
|
原因分析:
- 应用入口点配置问题
- 某个模块加载失败导致 AppRegistry.registerComponent 未被调用
- Metro bundler 可能从错误的目录运行
解决方案:
- 确保正确的入口文件 (index.js):
1
2
3
4
|
import { registerRootComponent } from 'expo';
import App from './App';
registerRootComponent(App);
|
- 简化 App.js 避免复杂依赖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { StyleSheet, Text, View, Platform } from 'react-native';
export default function App() {
return (
<View style={styles.container}>
<Text style={styles.title}>Inventory App (Expo)</Text>
<Text style={styles.subtitle}>迁移成功!</Text>
<Text style={styles.platform}>Platform: {Platform.OS}</Text>
<StatusBar style="auto" />
</View>
);
}
|
- 清理缓存重新构建:
1
2
|
npx expo start --clear
npx expo prebuild --clean
|
学习要点:
- Expo 使用
registerRootComponent 而不是直接使用 AppRegistry
- 应用入口必须足够简单,避免在初始化阶段加载复杂模块
- 缓存问题经常导致注册失败
实际执行的命令记录#
项目创建阶段#
1
2
3
4
5
6
7
8
9
10
11
12
|
# 1. 创建新的 Expo 项目
npx create-expo-app InventoryExpo --template bare-minimum
# 2. 进入项目目录
cd InventoryExpo
# 3. 安装核心依赖
npm install expo@~50.0.0
npm install expo-sqlite@~13.0.0 expo-file-system@~16.0.0 expo-camera@~14.0.0
npm install expo-status-bar@~1.11.0 expo-splash-screen@~0.26.0
npm install react-native-web@~0.19.6 react-dom@18.2.0
npm install @expo/metro-runtime@~3.1.3
|
代码迁移阶段#
1
2
3
4
5
6
7
8
9
|
# 4. 复制原始代码(使用批处理脚本)
migrate-code.bat
# 5. 安装开发依赖
npm install --save-dev @babel/core@^7.20.0
npm install --save-dev babel-preset-expo@~9.5.0
npm install --save-dev @tsconfig/react-native@^3.0.6
npm install --save-dev typescript@^5.1.3
npm install --save-dev babel-plugin-macros@^3.1.0
|
原生项目生成阶段#
1
2
3
4
5
6
|
# 6. 生成原生项目
npx expo prebuild
# 7. 清理缓存重新生成(多次执行以解决问题)
npx expo prebuild --clean
npx expo start --clear
|
问题解决阶段#
1
2
3
4
5
6
7
8
9
10
11
12
|
# 8. 解决依赖问题
npm install babel-plugin-macros --save-dev
npm install @tsconfig/react-native --save-dev
# 9. 重新安装所有依赖
rm -rf node_modules
npm install
# 10. 测试不同平台
npx expo start # 开发服务器
npx expo run:android # Android 测试
npx expo start --web # Web 测试
|
配置调整阶段#
1
2
3
4
5
|
# 11. 环境变量设置(Android SDK)
setx ANDROID_HOME "D:\Android\Sdk"
# 12. 验证环境
echo %ANDROID_HOME%
|
迁移脚本内容#
migrate-code.bat#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
@echo off
chcp 65001 > nul
echo 开始迁移代码...
echo 正在复制应用代码...
robocopy "..\App\app" "app" /E /XD node_modules .git android ios
echo 正在复制依赖库...
robocopy "..\App\deps" "deps" /E /XD node_modules .git
echo 正在复制原生模块...
if exist "android\app\src\main\java" (
robocopy "..\App\android\app\src\main\java" "android\app\src\main\java" /E
)
echo 正在复制 iOS 原生代码...
if exist "ios" (
robocopy "..\App\ios" "ios" *.h *.m *.mm *.swift /S
)
echo 正在复制补丁文件...
robocopy "..\App\patches" "patches" /E
echo 代码迁移完成!
pause
|
🛠️ 解决策略总结#
1. 渐进式迁移策略#
第一阶段:最小化可行产品
- 只保留核心 Expo 依赖
- 移除所有非必要插件
- 使用简化的 App 组件
第二阶段:逐步添加功能
- 一次添加一个模块
- 每次添加后测试应用启动
- 记录哪些模块会导致问题
第三阶段:原生模块集成
- 在基本功能稳定后添加 RFID 等原生模块
- 使用 Expo Config Plugins 处理原生配置
2. 调试最佳实践#
缓存管理:
1
2
3
4
5
|
# 清理所有缓存
npx expo start --clear
rm -rf node_modules
npm install
npx expo prebuild --clean
|
日志分析:
- 关注 Metro bundler 输出
- 检查 Android Logcat 中的详细错误
- 使用
expo doctor 诊断配置问题
配置验证:
- 确保 package.json 中的 main 字段正确
- 检查 app.json 配置语法
- 验证 babel.config.js 插件都已安装
3. 依赖管理策略#
核心原则:
- 最小依赖原则: 只安装必需的包
- 版本兼容性: 使用 Expo SDK 推荐的版本
- 渐进添加: 一次添加一个依赖并测试
推荐的最小依赖集:
1
2
3
4
5
6
7
8
9
10
11
12
|
{
"dependencies": {
"expo": "~50.0.0",
"expo-status-bar": "~1.11.0",
"react": "18.2.0",
"react-native": "0.72.10"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"babel-preset-expo": "~9.5.0"
}
}
|
迁移过程中创建的文件清单#
新项目结构 (InventoryExpo/)#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
InventoryExpo/
├── 📱 核心配置文件
│ ├── app.json # Expo 项目配置
│ ├── eas.json # EAS 构建配置
│ ├── metro.config.js # Metro 打包配置
│ ├── babel.config.js # Babel 转换配置
│ ├── tsconfig.json # TypeScript 配置
│ └── package.json # 依赖管理
│
├── 📋 入口文件
│ ├── index.js # Expo 应用入口
│ └── App.js # 应用主组件(简化版)
│
├── 🖼️ 资源文件
│ └── assets/
│ ├── icon.png # 应用图标
│ ├── adaptive-icon.png # Android 自适应图标
│ ├── splash.png # 启动画面
│ └── favicon.png # Web 图标
│
├── 📱 原生项目(自动生成)
│ ├── android/ # Android 项目
│ └── ios/ # iOS 项目
│
├── 📚 迁移代码
│ ├── app/ # 从原项目复制的应用代码
│ ├── deps/ # 依赖库
│ └── patches/ # 补丁文件
│
└── 📖 文档文件
├── MIGRATION_SUCCESS.md # 迁移成功总结
├── DEPENDENCY_MIGRATION.md # 依赖迁移指南
├── MIGRATION_COMPLETION.md # 完成状态
└── NEXT_STEPS.md # 下一步计划
|
迁移工具文件#
1
2
3
4
|
Inventory/ (根目录)
├── migrate-code.bat # 代码迁移批处理脚本
├── EXPO_MIGRATION_TROUBLESHOOTING.md # 本故障排除文档
└── InventoryExpo/ # 新的 Expo 项目
|
配置文件对比#
package.json 变化#
原始项目依赖:
1
2
3
4
5
6
|
{
"react-native": "0.71.10",
"react-native-quick-sqlite": "^8.0.0-beta.2",
"react-native-fs": "^2.20.0",
"react-native-vision-camera": "^2.15.4"
}
|
迁移后依赖:
1
2
3
4
5
6
7
|
{
"expo": "~50.0.0",
"react-native": "0.72.10",
"expo-sqlite": "~13.0.0",
"expo-file-system": "~16.0.0",
"expo-camera": "~14.0.0"
}
|
构建脚本变化#
原始项目:
1
2
3
4
5
6
7
|
{
"scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios",
"start": "react-native start"
}
}
|
迁移后:
1
2
3
4
5
6
7
|
{
"scripts": {
"android": "expo run:android",
"ios": "expo run:ios",
"start": "expo start --dev-client"
}
}
|
📊 问题频率统计#
| 问题类型 |
发生次数 |
解决难度 |
影响程度 |
| 依赖缺失 |
3次 |
低 |
中 |
| 权限错误 |
2次 |
高 |
高 |
| 应用注册失败 |
2次 |
中 |
高 |
| 配置错误 |
1次 |
低 |
中 |
🎯 关键学习要点#
1. Expo Bare Workflow 特点#
- 保留原生代码: 可以使用自定义原生模块
- Expo 工具链: 享受 Expo 的开发和构建工具
- 配置复杂性: 需要同时管理 Expo 和原生配置
2. 迁移过程中的注意事项#
- 分阶段进行: 不要一次性迁移所有代码
- 权限管理: Android 权限问题可能很复杂
- 依赖版本: 严格按照 Expo SDK 版本要求
- 缓存清理: 经常清理各种缓存
3. 调试技巧#
- 错误信息分析: 仔细阅读完整的错误堆栈
- 隔离问题: 通过移除代码定位问题源头
- 文档查阅: 查阅 Expo 官方文档和 GitHub Issues
4. 最佳实践#
- 版本控制: 每个工作阶段都要提交代码
- 文档记录: 记录所有问题和解决方案
- 测试驱动: 每次更改后都要测试基本功能
🔄 当前状态和下一步#
当前问题状态#
- ✅ babel-plugin-macros 安装完成
- ✅ TypeScript 配置修复
- ✅ app.json 权限简化
- ❌ Android 权限拒绝错误(仍存在)
- ❌ 应用注册失败(仍存在)
下一步计划#
- 进一步简化依赖: 移除可能有问题的 Expo 模块
- 创建最小化测试应用: 从头开始验证基本功能
- 逐步添加原始功能: 确认每个模块的兼容性
- 原生模块集成: 在基本功能稳定后添加 RFID 模块
备选方案#
如果当前方法无法解决:
- 重新创建项目: 使用
expo init 创建新项目后迁移代码
- 降级 Expo SDK: 尝试使用较旧但更稳定的版本
- 混合方案: 保留部分 React Native CLI 结构
📚 参考资源#
官方文档#
社区资源#
调试工具#
expo doctor - 配置诊断
npx react-native info - 环境信息
- Android Studio Logcat - Android 日志
文档创建时间: 2025年7月23日
最后更新时间: 2025年7月23日
文档状态: 进行中
💡 提示: 这是一个活跃文档,会随着问题解决和新发现持续更新。建议在每次遇到新问题或找到解决方案时都更新此文档。