前言

最近在翻看 Expo 文档的时候发现官方更新了 SDK 50 和 Expo Router V3 版本,遂上手尝试了一下,但是发现了一系列的报错,发现是由于 Expo 默认不支持 Monorepo 引起的。其实解决方案很简单,但搜索了一下网上都没有什么结果,在这里简单分享一下。

TL;DR

示例代码参考:expo-router-monorepo-example

参考官网文档一直操作到添加完 metro.config.js 文件。

接着如下添加 index.js 文件:

import "expo-router/entry";

其余步骤依旧参照官方文档。完成后保存并重新运行即可。

排查思路

首先看一开始页面上的报错毫无头绪,到具体的 node_modules 找 @expo/cli 这个包也看不到 package.json 里导出了 src 这个源文件目录。

TypeError: ExpoResponse is not a constructor
    at getHtml (node_modules/@expo/cli/src/start/server/metro/createServerRouteMiddleware.ts:107:20)
    at runNextTicks (node:internal/process/task_queues:60:5)
    at processTimers (node:internal/timers:511:9)
    at handler (node_modules/@expo/server/src/index.ts:149:26)
    at node_modules/@expo/server/src/vendor/http.ts:36:24

其实页面上的报错只是一个障眼法,具体的错误可以在命令行里看到:

Metro error: Unable to resolve module ./node_modules/expo-router/entry from apps/mobile/.:

稍微看两眼文档就知道这是由于 Expo 默认不兼容 Monorepo 引起的路径问题。但如果沿着 Expo 的官方文档一步步走下去,到了添加 index.js 这一步就有问题了,因为官方文档使用的不是 Expo Router 版本的项目,没有 App.[jt]sx 文件,只有一个 app 目录。

因为很明显可以知道 registerRootComponent 是一个用于指定根组件的函数,因此这个时候的思路就在于,怎么去找到 Expo Router 里的入口文件,也就是此时该从哪里去 import 这个目前不存在的 App。

然后我们就可以去看一看官网让我们替换 package.json 中原来的 main 入口,也就是 expo-router/entry 这个东西。直接去 node_modules 里就能找到这个文件。

// `@expo/metro-runtime` MUST be the first import to ensure Fast Refresh works
// on web.
import "@expo/metro-runtime";

import { App } from "expo-router/build/qualified-entry";
import { renderRootComponent } from "expo-router/build/renderRootComponent";

// This file should only import and register the root. No components or exports
// should be added here.
renderRootComponent(App);

可以看到这里的逻辑只有一个,就是调用 renderRootComponent 这个方法,它的参数同样是一个 App 入口。那么问题就真相大白了:对于非 Monorepo 的情况来说,入口文件路径是固定的,也就是这里引入的 App。但是在 Monorepo 里,由于依赖都安装在一个 node_modules 中,项目的 node_modules 中找不到这个入口,于是就只能我们通过手动添加一个入口的方式去注册这个根组件。

那么最简单的解决方式就是直接在 index.js 里手动引入一下这个 expo-router/entry,让所有的路径解析都让 Monorepo 的包管理工具(yarn 或者 pnpm)去解决,我们则不需要关心。