返回

React Native Metro 选项不全?端口冲突与缓存清理指南

Linux

Metro Server 为何只显示部分 React Native 应用选项?

开发 React Native 应用时,你可能碰到过一个怪事:明明用差不多的方式创建了两个项目,启动 Metro Server 后,一个项目的控制台能看到完整的操作选项(比如 i - iOS, a - Android),另一个却只显示部分选项(比如 r - reload, d - Dev Menu)。这到底是怎么回事?

问题现象

假设你先后创建了两个 React Native CLI 项目(非 Expo):

npx @react-native-community/cli@latest init AwesomeProject # 假设1月21日创建
npx @react-native-community/cli@latest init PasswordGen    # 假设1月22日创建

接着,你尝试分别为这两个项目启动 Metro Server 和运行 Android 应用:

对于 AwesomeProject

cd AwesomeProject
npx react-native start   # 启动 Metro
# 在另一个终端
npx react-native run-android # 编译安装并运行 App

这时,AwesomeProject 的 Metro Server 控制台显示了预期的完整选项:

 i- open on iOS
 a - open on Android
 r - reload app(s)
 d - open Dev Menu
 j - open DevTools

但当你对 PasswordGen 项目做同样的操作:

cd ../PasswordGen
npx react-native start   # 启动 Metro
# 在另一个终端
npx react-native run-android # 编译安装并运行 App

PasswordGen 的 Metro Server 控制台却可能只显示了:

 r - reload app(s)
 d - open Dev Menu
 j - open DevTools

少了 ia 选项,感觉 Metro 好像没完全“认识”这个新 App。就算换台电脑,问题也可能依旧存在。

分析问题根源

这种情况通常不是 React Native 本身的 Bug,而是开发环境中的一些配置或状态冲突导致的。主要原因可能包括:

  1. 端口冲突或残留进程: 最常见的原因。Metro 默认使用 8081 端口。如果前一个项目的 Metro Server 没有完全关闭,或者有其他程序占用了 8081 端口,新启动的 Metro Server 可能无法正常绑定或与其他服务(如 run-android 启动的调试桥)建立完整连接。即使 Metro 尝试在不同端口启动,应用本身可能仍然配置为连接 8081,导致连接失败。
  2. 缓存问题: Metro、Gradle(Android 构建系统)、Watchman(文件监视器)甚至 npm/Yarn 都有自己的缓存。这些缓存如果包含了过时或冲突的信息,可能会干扰新项目的启动和连接过程。
  3. ADB (Android Debug Bridge) 连接混乱: ADB 负责模拟器/设备与开发机之间的通信。如果同时操作多个项目或模拟器/设备,ADB 的状态可能变得混乱,无法正确地将 Metro Server 与目标应用关联起来。
  4. Watchman 监视问题: Watchman 用于高效地检测文件变化。在多项目环境下,如果 Watchman 监视的文件过多或遇到权限问题,也可能间接影响 Metro 的正常运行。

解决方案

针对上述可能的原因,可以尝试以下几种方法来解决。建议按顺序尝试,通常前一两种方法就能解决大部分问题。

方案一:检查并处理端口冲突

这是首要排查的方向。确保 Metro 要使用的端口(默认 8081)是干净的。

1. 查找占用端口的进程

  • macOS / Linux:
    打开终端,运行以下命令查看哪个进程占用了 8081 端口:

    sudo lsof -i :8081
    

    你会看到类似 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME 的输出。记下 PID 列的数字。

  • Windows:
    打开命令提示符 (cmd) 或 PowerShell,运行:

    netstat -ano | findstr "8081"
    

    在输出结果中找到状态为 LISTENINGESTABLISHED 的行,记下最后一列的 PID (进程标识符)。

2. 结束占用端口的进程

  • macOS / Linux:
    使用 kill 命令强制结束进程(将 <PID> 替换为上一步找到的数字):

    kill -9 <PID>
    
  • Windows:
    打开任务管理器 (Ctrl+Shift+Esc),切换到“详细信息”或“进程”选项卡。找到对应 PID 的进程(可能需要先在菜单“查看”->“选择列”中勾选“PID”),然后选中它,点击“结束任务”。
    或者,在管理员权限的命令提示符或 PowerShell 中运行:

    taskkill /PID <PID> /F
    

3. (可选)在不同端口启动 Metro

如果 8081 端口确实被其他必要服务占用,或者你想同时运行两个项目的 Metro,可以为其中一个指定不同的端口。

  • 启动 Metro 时指定端口:

    cd PasswordGen
    npx react-native start --port 8088
    

    这里使用了 8088 端口,你可以选择其他未被占用的端口。

  • 让设备/模拟器连接新端口:
    仅仅让 Metro 监听新端口还不够,你需要告诉设备上的应用去连接这个新端口。

    • 方法一(推荐): 如果你的 React Native 版本较新(0.60+),run-androidrun-ios 命令通常会自动检测正在运行的 Metro Server 实例,包括非默认端口的实例,并进行配置。因此,先启动 Metro 指定端口再运行 run-androidrun-ios 通常就能工作。
    • 方法二(手动 ADB reverse): 如果自动检测不生效,或者你想更明确地控制,需要手动设置端口转发。确保你的设备或模拟器已连接 (adb devices 能看到)。
      adb reverse tcp:8088 tcp:8088
      
      这个命令告诉设备,将发往设备本地 8088 端口的 TCP 请求转发到开发机的 8088 端口。对于 iOS,端口转发通常由 run-ios 处理,手动配置较少见。

安全建议

  • 结束进程时要小心,确认你结束的是不再需要的 Metro 或 Node 进程,而不是系统关键进程。
  • 如果使用非默认端口,确保该端口未被防火墙阻止。

进阶使用技巧

  • adb reverse --list 可以查看当前设置的所有端口转发规则。
  • adb reverse --remove <DEVICE_PORT>adb reverse --remove-all 可以移除转发规则。
  • 当你确定只需要一个 Metro 实例时,养成彻底关闭前一个实例(在 Metro 控制台按 Ctrl+C 并确认退出)的好习惯,能有效避免端口冲突。

方案二:清理各种缓存

缓存是性能优化的好东西,但有时也会变成问题的源头。清理缓存可以解决由过时或损坏数据引起的问题。

1. 清理 Metro 缓存

  • 使用 --reset-cache 启动:
    这是最直接的方式,在启动 Metro 时告诉它忽略现有缓存。

    npx react-native start --reset-cache
    

    或者,结合指定端口:

    npx react-native start --port 8088 --reset-cache
    
  • 手动删除缓存文件:
    Metro 的缓存通常位于系统的临时目录下。

    • macOS: rm -rf $TMPDIR/react-*
    • Linux: rm -rf /tmp/react-*
    • Windows: 通常在 %TEMP%\react-native-packager-cache-* 或类似路径,可以在文件管理器中搜索 react-native-packager-cache 并删除相关文件夹。

2. 清理 Android 构建缓存 (Gradle)

Android 项目构建时会产生大量缓存。

cd android
./gradlew clean  # 在 macOS/Linux 上
# 或者
gradlew.bat clean # 在 Windows 上
cd ..

这会清理掉 android/app/build 目录下的编译产物。更彻底的清理可以删除 ~/.gradle/caches 目录,但下次构建时间会显著增加。谨慎操作。

3. 清理 npm/Yarn 缓存和 node_modules

依赖问题也可能导致奇怪的行为。

rm -rf node_modules # 或者用 rimraf node_modules (跨平台)
npm cache clean --force # 如果用 npm
# 或者
yarn cache clean        # 如果用 yarn

npm install # 重新安装依赖
# 或者
yarn install

4. 重置 Watchman 状态

Watchman 用于监控文件变更。

watchman watch-del-all # 删除所有监视项
watchman shutdown-server # 关闭 Watchman 服务(它会在需要时自动重启)

进阶使用技巧

  • 理解不同缓存的作用:Metro 缓存 JS 包和转换结果;Gradle 缓存编译后的 Java/Kotlin 代码和依赖;npm/Yarn 缓存下载的包。清理要有针对性。
  • ./gradlew cleanBuildCache 可以尝试清理 Gradle 的构建缓存,比 clean 更深入一些,但可能需要较新版本的 Gradle。
  • 对于大型项目,频繁清理 node_modulesgradle clean 会拖慢开发流程。优先尝试 Metro 的 --reset-cache

方案三:确保没有旧进程残留

有时,即使你在终端按了 Ctrl+C,Metro Server 或相关的 Node.js 进程也可能没有完全退出,变成了“僵尸进程”。

1. 查找 Node 进程

  • macOS / Linux:

    ps aux | grep node
    

    查找与 React Native 或 Metro Server 相关的进程。留意那些看起来像是在运行 Metro 或你的项目脚本的行。

  • Windows:
    打开任务管理器,在“详细信息”或“进程”里查找 node.exe。注意区分,你可能有其他 Node.js 应用在运行。

2. 结束相关进程

  • macOS / Linux:
    使用 kill <PID>kill -9 <PID>(强制)结束找到的可疑 Node 进程 PID。

  • Windows:
    在任务管理器中选中目标 node.exe 进程,点击“结束任务”。

安全建议

  • pkill nodekillall node 这类命令会杀死所有 Node.js 进程,可能中断你正在进行的其他工作,务必谨慎使用。最好是根据 ps 或任务管理器的信息,精确地杀死目标进程。

方案四:重置 ADB 连接

如果怀疑是设备连接或 ADB 本身出了问题。

adb kill-server   # 关闭 ADB 服务
adb start-server  # 重新启动 ADB 服务
adb devices       # 检查设备是否重新连接成功

之后再尝试 npx react-native run-android

方案五:检查和重置 Watchman

虽然不如端口和缓存常见,但 Watchman 问题也值得检查。

# 先尝试重置
watchman watch-del-all
watchman shutdown-server

# 如果问题依旧,并且怀疑是 Watchman 安装问题
# 可以尝试卸载重装 (以 brew 为例)
brew uninstall watchman
brew install watchman

预防措施和调试思路

  • 尽量一次只运行一个项目: 如果不需要同时调试两个 RN 项目,养成在切换项目前彻底关闭前一个项目的 Metro Server (Ctrl+C) 和相关模拟器/应用的习惯。
  • 为不同项目使用不同端口: 如果确实需要同时运行,从一开始就规划好,使用 --port 参数为每个项目分配不同的端口,并确保设备连接到了正确的端口(通常通过先启动 Metro 再运行 run-android/ios 实现)。
  • 善用清理命令: 遇到奇怪的构建或连接问题时,--reset-cachegradle clean 是你的好朋友。
  • 观察 Metro 输出: Metro Server 启动时的日志信息其实包含了它正在使用的端口、是否找到了设备等关键信息。仔细阅读这些输出,有助于定位问题。
  • 隔离变量: 如果上述方法都无效,尝试创建一个全新的 React Native 项目,看它是否能正常工作。如果新项目正常,说明问题可能出在旧项目的特定配置或代码里。如果新项目也有问题,那环境问题的可能性更大(比如 Node 版本、全局安装的 CLI 工具、系统配置等)。

通过系统地排查端口、缓存、残留进程和 ADB 连接,大部分 Metro Server 无法完全识别应用(缺少 a/i 选项)的问题都可以得到解决。保持开发环境的清洁和良好的操作习惯是避免这类问题的关键。