与单体仓库合作
编辑
了解如何在使用工作区的单体仓库中设置 Expo 项目。
单体仓库,或称 “单体代码库”,是一个包含多个应用程序或包的单一代码库。它们可以加快大型项目的开发速度,更容易共享代码,并作为单一事实来源。本指南将设置一个简单的单体仓库,并包含一个 Expo 项目。Expo 对于管理工作区的包管理器(如:Bun,npm,pnpm 和 Yarn(经典版v1和Berry版))的单体仓库提供了一流的支持。Expo 会自动检测单体仓库,并为添加到单体仓库的新应用项目进行配置。该检测基于项目中的工作区配置。
并不是所有项目都适合使用单体仓库。如果多个应用程序位于一个代码库中并共享代码,或者有助于将原生模块与您的应用程序共同定位,那么它们是有用的。其权衡是在设置和配置工具时复杂性增加。在设置单体仓库之前,请检查您的工具和库是否能够很好地使用单体仓库。
自动配置(迁移到 SDK 52+)
自 SDK 52 起,Expo 会自动为单体仓库配置 Metro。如果您使用 expo/metro-config ,则在使用单体仓库时无需手动配置 Metro。
如果您正在迁移到 SDK 52 之后的 Expo 版本,并且拥有一个手动修改以下属性之一的 metro.config.js,请将其从配置中删除:
watchFoldersresolver.nodeModulesPathresolver.extraNodeModulesresolver.disableHierarchicalLookup
删除这些选项后,您需要运行 Expo 以 npx expo start --clear 一次,以清除过时的 Metro 缓存。如果您的应用在之后继续按预期运行,那么它就是一个常规的 Node 单体仓库,今后将不需要任何特殊配置。
手动配置(在 SDK 52 之前)
自 SDK 52 起,Expo 的 Metro 配置支持 Bun、npm、pnpm 和 Yarn 的单体仓库,且会自动配置。如果您使用 expo/metro-config 的配置,则无需手动配置单体仓库支持。
在 SDK 52 之前,手动使用 Metro 配置单体仓库需要进行两个手动更改:
- 必须手动配置 Metro 以监视单体仓库中的代码(例如,不只是 apps/cool-app)。
- 必须调整 Metro 的解析以找到其他工作区和多个
node_modules目录中的包(例如,apps/cool-app/node_modules 或 node_modules)。
通过创建一个 metro.config.js,其内容如下,进行了配置调整:
const { getDefaultConfig } = require('expo/metro-config'); const path = require('path'); // 这可以替换为 `find-yarn-workspace-root` const monorepoRoot = path.resolve(__dirname, '../..'); const config = getDefaultConfig(__dirname); // 1. 监视单体仓库中的所有文件 config.watchFolders = [monorepoRoot]; // 2. 让 Metro 知道在哪里解析包以及顺序 config.resolver.nodeModulesPaths = [ path.resolve(projectRoot, 'node_modules'), path.resolve(monorepoRoot, 'node_modules'), ]; module.exports = config;
了解更多关于 自定义 Metro。
设置单体仓库
在单体仓库中,您的应用程序通常会是您代码库下的一个子目录,并且您的包管理器被配置为允许您从单体仓库中添加对其他包的依赖项。例如,包含 Expo 应用的单体仓库的基本结构可能如下所示:
- apps:包含多个项目,包括 Expo 应用。
- packages:包含应用使用的不同包。
- package.json:根包文件。
所有单体仓库都应该有一个“根” package.json 文件。它是单体仓库的主要配置,可能包含为代码库中的所有项目安装的工具。根据您使用的包管理器,设置工作区的步骤可能会有所不同,但对于 Bun,npm,和 Yarn,应将 workspaces 属性添加到根 package.json 文件中,以指定单体仓库中所有工作区的 glob 模式:
{ "name": "monorepo", "private": true, "version": "0.0.0", "workspaces": ["apps/*", "packages/*"] }
对于 pnpm,您需要创建一个 pnpm-workspace.yaml:
packages: - 'apps/*' - 'packages/*'
创建您的第一个应用
现在您已经设置了基本的单体仓库结构,可以添加您的第一个应用。
在创建应用之前,您必须创建 apps 目录。此目录包含属于此单体仓库的所有独立应用或网站。在这个 apps 目录内,您可以创建一个包含 Expo 应用的子目录。
- npx create-expo-app@latest apps/cool-app- yarn create expo-app apps/cool-app- pnpm create expo-app apps/cool-app- bun create expo apps/cool-app如果您有现有应用,可以将所有这些文件复制到 apps 中的一个目录。
在复制或创建第一个应用后,从单体仓库的根目录使用包管理器安装依赖项,以检查常见的警告。
创建一个包
单体仓库可以帮助我们在一个代码库中分组代码。这包括应用程序,但也包括独立的包。它们也不需要发布。Expo 仓库 也使用这一点。所有的 Expo SDK 包都位于我们代码库中的 packages 目录下。这有助于我们在发布之前在我们 apps 目录中测试代码。
让我们回到根目录并创建 packages 目录。此目录可以包含您想要创建的所有独立包。一旦进入此目录,我们需要添加一个新的子目录。子目录是我们可以在应用中使用的独立包。在下面的示例中,我们将其命名为 cool-package。
# 创建您的新包目录- mkdir -p packages/cool-package- cd packages/cool-package# 创建新包- npm init# 创建您的新包目录- mkdir -p packages/cool-package- cd packages/cool-package# 创建新包- yarn init# 创建您的新包目录- mkdir -p packages/cool-package- cd packages/cool-package# 创建新包- pnpm init# 创建您的新包目录- mkdir -p packages/cool-package- cd packages/cool-package# 创建新包- bun init --minimal我们不会对创建包展开过多细节。如果您对此不熟悉,请考虑使用一个不带单体仓库的简单应用。不过,为了使示例完整,我们添加一个 index.js 文件,其内容如下:
export const greeting = 'Hello!';
使用该包
像标准包一样,我们需要将我们的 cool-package 添加为我们的 cool-app 的依赖项。标准包与单体仓库中的包之间的主要区别在于,您总是希望使用 “当前状态的包”,而不是某个版本。让我们通过将 "cool-package": "*" 添加到我们的应用 package.json 文件中,来将 cool-package 添加到我们的应用程序:
{ "name": "cool-app", "version": "1.0.0", "scripts": { "start": "expo start", "android": "expo start --android", "ios": "expo start --ios", "web": "expo start --web" }, "dependencies": { "cool-package": "*", "expo": "~54.0.0", "expo-status-bar": "~3.0.6", "react": "19.1.0", "react-native": "0.81.1" } }
Bun、npm 和 pnpm 支持使用 "workspace:*" 指定工作区依赖关系,而不是 "*"。这将确保工作区包永远不会从 npm 注册表解析同名的已发布包,但这是可选的。
添加包之后,再次从单体仓库的根目录使用包管理器安装依赖项,以检查常见警告。
现在您应该能够在应用中使用该包!为了测试这一点,让我们编辑您的应用中的 App.js,并渲染我们 cool-package 中的 greeting 文本。
import { greeting } from 'cool-package'; import { StatusBar } from 'expo-status-bar'; import React from 'react'; import { Text, View } from 'react-native'; export default function App() { return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <Text>{greeting}</Text> <StatusBar style="auto" /> </View> ); }
常见问题
单体仓库可能会导致常规项目不会出现的解析和依赖问题。它们需要更深入的知识,并需要特定的工具配置。您会承受更高的复杂性,并需要解决您在没有工作区的情况下不会遇到的问题。以下是您可能遇到的一些常见问题。
具有孤立依赖项的包管理器
从 SDK 54 开始,Expo 支持孤立依赖项和孤立安装。
对于 SDK 53,建议禁用孤立依赖项,否则您可能会遇到原生构建错误和依赖冲突。
Bun 和 pnpm 对孤立安装提供了原生支持。对于 pnpm,这也是默认的安装策略,除非禁用。
使用孤立依赖项时,包管理器不会将嵌套的 node_modules 目录中的包提升到更高的目录中。相反,它们会创建一个中心目录来包含您的 Node 模块,并创建指向该目录的链接。这种依赖结构强制包只能访问其显式声明的依赖项。这是一种比传统 hoisted 安装策略更严格的安装策略,后者是 npm 和 Yarn 的默认方式,通过扁平结构安装依赖项。
hoisted 安装的一个副作用是,您可能会无意中依赖于在自己 package.json 的 dependencies 或 peerDependencies 中未指定的 Node 模块。相反,其他包依赖的许多更多依赖项被提升并变得可访问。这可能导致非确定性行为,并允许您拥有破损的依赖关系链,这些链更加脆弱,并且在更新或升级包时可能会导致解析错误。这在单体仓库中尤其常见。
从 SDK 54 开始,Expo 支持孤立依赖项。不幸的是,并非所有您安装的包都能正常工作,某些 React Native 库在与孤立依赖项一起使用时可能会导致构建或解析错误。如果您在使用 pnpm 时遇到孤立安装问题,请通过在代码库根目录中的 .npmrc 文件中更改 node-linker 设置切换到 hoisted 安装策略:
node-linker=hoisted
单体仓库内重复的原生包
Expo 改进了对更完整的 node_modules 模式(例如孤立模块)的支持。不幸的是,如果您的应用包含重复的依赖项,仍可能会发生问题:
- 在单个单体仓库中不支持重复的 React Native 版本
- 在单个应用中重复的 React 版本将导致运行时错误
- 重复版本的 Turbo 和 Expo 模块可能导致运行时或构建错误
您可以检查您的单体仓库是否有多个同一包的版本,例如 react-native,以及它们为何被您使用的包管理器安装。
- npm why react-native- yarn why react-native- pnpm why --depth=10 react-native- bun pm why react-native这些命令的输出在不同的包管理器之间会非常不同,但您可以通过查看同一包的多个版本(例如 react-native@0.79.5 和 react-native@0.81.0)在它们的任意输出中找到重复的包。
npm,
为同伴依赖项添加依赖解析
如果重复的依赖项无法通过更改您的依赖项进行解决,则可能需要添加解析。例如,并非所有包都已更新其 peerDependencies 以支持 React 19。为了解决此问题,您可以创建一个解析,强制安装单个版本的 react。
{ "name": "monorepo", "private": true, "version": "0.0.0", "workspaces": ["apps/*", "packages/*"], "resolutions": { "react": "^19.1.0" } }
对于 npm,您必须使用名为 overrides 的属性,而不是 resolutions。
去重自动链接的原生模块
这是从 SDK 54 及以后版本开始的实验性功能。该过程将在未来版本中自动化并提供更好的支持。
通常,重复的依赖项不会引起任何问题。然而,原生模块绝不应重复,因为一次只能为应用构建编译一个版本的原生模块。与 JavaScript 依赖项不同,原生构建不能包含单个原生模块的两个冲突版本。
从 SDK 54 开始,您可以在 app.json 中将 experiments.autolinkingModuleResolution 设置为 true,以自动将自动链接应用于 Expo CLI 和 Metro 打包工具。这将强制 Metro 解析的依赖项与 自动链接 为您的原生构建链接的原生模块匹配。
脚本 '...' 不存在
React Native 使用包来发布 JavaScript 和原生文件。这些原生文件也需要链接,就像 android/app/build.Gradle 中的 react-native/react.Gradle 文件一样。通常,此路径是硬编码为类似于:
Android (source)
apply from: "../../node_modules/react-native/react.gradle"
iOS (source)
require_relative '../node_modules/react-native/scripts/react_native_pods'
不幸的是,由于 提升,在单体仓库中此路径可能会有所不同。它也不使用 Node 模块解析。您可以通过使用 Node 找到包的位置,而不是硬编码路径来避免此问题:
Android (source)
apply from: new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../react.gradle")
iOS (source)
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
在上述代码片段中,您可以看到我们使用 Node 自己的 require.resolve() 方法查找包的位置。我们明确引用 package.json 是因为我们想找到包的根位置,而不是入口点的位置。通过该根位置,我们可以解析到包内的预期相对路径。了解更多关于这些引用的信息。
所有 Expo SDK 模块和模板都有这些动态引用,并且在单体仓库中工作。然而,有时您可能会遇到仍使用硬编码路径的包。您可以使用 patch-package 手动编辑它,或向包维护者提及此问题。