使用大仓
了解如何在包含工作区的 monorepo 中设置 Expo 项目。
单一代码仓库,或称“单体仓库”,是包含多个应用或软件包的单一仓库。它们可以加快大型项目的开发速度,使代码共享更容易,并充当单一的可信来源。本指南将演示如何使用 Expo 项目设置一个简单的单一代码仓库。Expo 对由支持工作区的包管理器管理的单一代码仓库提供一流支持:Bun、npm、pnpm 和 Yarn(v1 Classic 和 Berry)。Expo 会自动检测单一代码仓库,并配置添加到仓库中的新应用项目。检测是基于项目中的工作区配置进行的。
🌐 Monorepos, or "monolithic repositories", are single repositories containing multiple apps or packages. They can help speed up development for larger projects, make it easier to share code, and act as a single source of truth. This guide will set up a simple monorepo with an Expo project. Expo has first-class support for monorepos managed with package managers supporting workspaces: Bun, npm, pnpm, and Yarn (v1 Classic and Berry). Expo automatically detects monorepos and configures new app projects added to a monorepo. The detection is based on the workspace configuration in your project.
信息 单体仓库并不适用于所有项目。如果多个应用位于同一个仓库中并共享代码,或者将本地模块与应用放在一起会更方便,那么它们会很有用。其权衡是,在设置和配置工具时会增加复杂性。在建立单体仓库之前,请检查你的工具和库是否在单体仓库中运行良好。
自动配置(迁移到 SDK 52 及以上版本)
自 SDK 52 起,Expo 会自动为 monorepos 配置 Metro。如果你使用 expo/metro-config,在使用 monorepos 时无需手动配置 Metro。
🌐 Since SDK 52, Expo configures Metro automatically for monorepos. You don't have to manually configure Metro when using monorepos if you use expo/metro-config.
如果你正在迁移到 Expo SDK 52 之后的版本,并且你的 metro.config.js 手动修改了以下属性之一,请从你的配置中删除这些内容:
🌐 If you're migrating to an Expo SDK version after 52 and have a metro.config.js that manually modifies one of the following properties, delete these from your configuration:
watchFoldersresolver.nodeModulesPathresolver.extraNodeModulesresolver.disableHierarchicalLookup
删除这些选项后,你需要运行一次带有 npx expo start --clear 的 Expo,以清除过时的 Metro 缓存。如果之后你的应用继续正常运行,它就是一个普通的 Node 单体仓库,今后无需任何特殊配置。
🌐 After deleting these options, you'll need to run Expo with npx expo start --clear once to erase the outdated Metro cache. If your app continues working as expected afterwards, it's a regular Node monorepo and won't need any special configuration going forward.
手动配置(SDK 52 之前)
自 SDK 52 起,Expo 的 Metro 配置已支持 Bun、npm、pnpm 和 Yarn 的 monorepo,并会自动进行配置。如果你使用 expo/metro-config 中的配置,在使用 monorepo 时无需手动配置 Metro。如果是这种情况,你也不需要手动配置 monorepo 支持。
🌐 Since SDK 52, Expo's Metro config has monorepo support for Bun, npm, pnpm and Yarn and configures itself automatically. You don't have to manually configure Metro when using monorepos if you use the config from expo/metro-config. If that's the case, you don't need to manually configure monorepo support.
在 SDK 52 之前,要手动配置带有 Metro 的 monorepo,需要进行两个手动更改:
🌐 Before SDK 52, to configure a monorepo with Metro manually, there were two manual changes:
- Metro 必须手动配置以监控 monorepo 中的代码(例如,不仅仅是 apps/cool-app)。
- 必须调整 Metro 的分辨率,以便在其他工作区和多个
node_modules文件夹中查找包(例如,apps/cool-app/node_modules 或 node_modules)。
配置已通过创建一个 metro.config.js 并包含以下内容)进行调整:
🌐 The configuration was adjusted by creating a metro.config.js with the following content:
const { getDefaultConfig } = require('expo/metro-config'); const path = require('path'); // This can be replaced with `find-yarn-workspace-root` const monorepoRoot = path.resolve(__dirname, '../..'); const config = getDefaultConfig(__dirname); // 1. Watch all files within the monorepo config.watchFolders = [monorepoRoot]; // 2. Let Metro know where to resolve packages and in what order config.resolver.nodeModulesPaths = [ path.resolve(projectRoot, 'node_modules'), path.resolve(monorepoRoot, 'node_modules'), ]; module.exports = config;
了解有关自定义Metro的更多信息。
设置单一代码仓库
🌐 Setting up a monorepo
在单一代码库(monorepo)中,你的应用通常会位于仓库的一个子目录中,并且你的包管理器配置为允许你在单一代码库内为其他包添加依赖。例如,一个包含 Expo 应用的单一代码库的基本结构可能如下所示:
🌐 In a monorepo, your app will typically be a in sub-directory of your repository and your package manager is configured to allow you to add dependencies to other packages from within your monorepo. For example, a basic structure of a monorepo containing Expo apps may look like this:
- 应用:包含多个项目,包括 Expo 应用。
- packages:包含应用使用的不同软件包。
- package.json:根包文件。
所有的单体仓库都应该有一个“根” package.json 文件。它是单体仓库的主要配置文件,并且可能包含为仓库中所有项目安装的工具。根据你使用的包管理器不同,设置工作区的步骤可能有所不同,但对于 Bun、npm 和 Yarn,应该在根 package.json 文件中添加一个 workspaces 属性,该属性指定你单体仓库中所有工作区的 glob 模式:
🌐 All monorepos should have a "root" package.json file. It is the main configuration for monorepos and may contain tools installed for all projects in the repository. Depending on which package manager you're using, the steps for setting up workspaces might differ, but for Bun, npm, and Yarn, a workspaces property should be added to the root package.json file that specifies glob patterns for all workspaces in your monorepo:
{ "name": "monorepo", "private": true, "version": "0.0.0", "workspaces": ["apps/*", "packages/*"] }
对于 pnpm,你需要创建一个 pnpm-workspace.yaml 文件:
🌐 For pnpm, you'll have to create a pnpm-workspace.yaml instead:
packages: - 'apps/*' - 'packages/*'
创建你的第一个应用
🌐 Create your first app
现在你已经设置好了基本的单体仓库结构,添加你的第一个应用。
🌐 Now that you have the basic monorepo structure set up, add your first app.
在创建你的应用之前,你必须先创建 apps 目录。这个目录包含属于该多仓库的所有独立应用或网站。在这个 apps 目录内,你可以创建一个子目录来存放 Expo 应用。
🌐 Before you create your app, you have to create the apps directory. This directory contains all separate apps or websites that belong to this monorepo. Inside this apps directory, you can create a sub-directory that contains the Expo app.
- npx create-expo-app@latest apps/cool-app如果你已经有一个现有的应用,你可以将所有这些文件复制到 apps 目录中的某个文件夹里。
在复制或创建第一个应用后,从你的单仓库根目录使用你的包管理器安装依赖,以检查常见警告。
🌐 After copying or creating the first app, install your dependencies with your package manager from the root directory of your monorepo to check for common warnings.
创建一个包
🌐 Create a package
Monorepos 可以帮助我们将代码集中在一个仓库中。这不仅包括应用,还包括独立的包。它们也不需要被发布。Expo 仓库 也是这样使用的。所有的 Expo SDK 包都位于我们仓库的 packages 目录下。它可以帮助我们在发布之前测试位于 apps 目录中的应用代码。
🌐 Monorepos can help us group code in a single repository. That includes apps but also separate packages. They also don't need to be published. The Expo repository uses this as well. All the Expo SDK packages live inside the packages directory in our repo. It helps us test the code inside one of our apps directory before we publish them.
让我们回到根目录并创建 packages 目录。这个目录可以包含你想要制作的所有独立包。一旦你进入这个目录,我们需要添加一个新的子目录。这个子目录是一个可以在我们的应用中使用的独立包。在下面的示例中,我们将其命名为 cool-package。
🌐 Let's go back to the root and create the packages directory. This directory can contain all the separate packages that you want to make. Once you are inside this directory, we need to add a new sub-directory. The sub-directory is a separate package that we can use inside our app. In the example below, we named it cool-package.
- mkdir -p packages/cool-package && cd packages/cool-package && npm init我们不会在创建包的过程中深入细节。如果你不熟悉这方面,可以考虑使用不带单体仓库的简单应用。但为了使示例完整,我们还是添加一个内容如下的 index.js 文件:
🌐 We won't go into too much detail in creating a package. If you are not familiar with this, consider using a simple app without monorepos. But, to make the example complete, let's add an index.js file with the following content:
export const greeting = 'Hello!';
使用该软件包
🌐 Using the package
像标准包一样,我们需要将我们的 cool-package 添加为 cool-app 的依赖。标准包和 monorepo 包之间的主要区别在于,你总是希望使用包的_“当前状态”_而不是某个版本。让我们通过在应用的 package.json 文件中添加 "cool-package": "*" 来将 cool-package 添加到我们的应用中:
🌐 Like standard packages, we need to add our cool-package as a dependency to our cool-app. The main difference between a standard package, and one from the monorepo, is you'll always want to use the "current state of the package" instead of a version. Let's add cool-package to our app by adding "cool-package": "*" to our app package.json file:
{ "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 注册表的同名已发布包,但这是可选的。
🌐 Bun, npm, and pnpm support specifying workspace dependencies using "workspace:*" instead of "*". This will ensure that the workspace package never resolves a published package of the same name from the npm registry, but is optional.
添加包后,从 monorepo 的根目录使用你的包管理器安装依赖,以再次检查常见警告。
🌐 After adding the package, install your dependencies with your package manager from the root directory of your monorepo to check for common warnings once again.
现在你应该可以在你的应用中使用这个包了!为了测试这一点,让我们编辑应用中的 App.js,并渲染我们 cool-package 中的 greeting 文本。
🌐 Now you should be able to use the package inside your app! To test this, let's edit the App.js in your app and render the greeting text from our cool-package.
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> ); }
常见问题
🌐 Common issues
单一仓库可能会引起普通项目不会遇到的解析和依赖问题。它们需要更深入的知识,并且需要特定的工具配置。你需要应对更高的复杂性,并解决如果没有工作区你不会遇到的问题。以下是你可能遇到的一些常见问题。
🌐 Monorepos may cause resolution and dependency issues that a regular project won't. They require more in-depth knowledge and require specific tooling configuration. You take on increased complexity and will need to solve issues you wouldn't run into without workspaces. Here are a couple of common issues you might encounter.
具有隔离依赖的包管理器
🌐 Package managers with isolated dependencies
信息 从 SDK 54 开始,Expo 支持隔离依赖和隔离安装。
在 SDK 53 中,建议禁用隔离依赖,否则可能会遇到本地构建错误和依赖冲突。
Bun 和 pnpm 对隔离安装提供一流支持。对于 pnpm,这是默认的安装策略,除非被禁用。
使用隔离依赖时,包管理器不会将嵌套的 node_modules 目录中的包提升到更高级的目录。相反,它们会创建一个包含你的 Node 模块的中央目录,并创建指向该目录的链接。这种依赖结构确保包只能访问其明确声明的依赖。这种安装策略比传统的提升式安装策略要严格得多,而传统策略(npm 和 Yarn 默认使用)会以扁平化的结构安装依赖。
🌐 With isolated dependencies, package managers don't hoist packages from nested node_modules directories into higher ones. Instead, they create a central directory that contains your Node modules and create links to this directory. This dependency structure enforces that packages may only access their explicitly declared dependencies. This is a much stricter installation strategy than the traditional hoisted installation strategy, which are npm's and Yarn's default, to install dependencies in a flattened structure.
提升安装的一个副作用是,你可能会意外依赖于自己在 package.json 的 dependencies 或 peerDependencies 中未指定的 Node 模块。相反,许多其他包依赖的更多依赖被提升,并变得对你可访问。这可能导致非确定性行为,并可能使你的依赖链中断,从而更加脆弱,在更新或升级包时可能引发解析错误。这在单体仓库(monorepo)中尤其常见。
🌐 A side-effect of hoisted installations is that you can accidentally depend on Node modules you haven't specified in your own package.json's dependencies or peerDependencies. Instead, many more dependencies that other packages rely on are hoisted and become accessible to you. This can cause non-deterministic behavior, and allow you to have broken dependency chains, which are more fragile and can cause resolution errors when updating or upgrading packages. This is especially common in monorepos.
从 SDK 54 开始,Expo 支持隔离依赖。不幸的是,并不是所有你安装的包都能正常工作,一些 React Native 库在与隔离依赖一起使用时可能会导致构建或解析错误。如果你在使用 pnpm 进行隔离安装时遇到问题,可以通过在仓库根目录的 .npmrc 文件中更改 node-linker 设置,切换到 提升 安装策略:
node-linker=hoisted
在单体仓库中重复的本地包
🌐 Duplicate native packages within monorepos
Expo 对更完整的 node_modules 模式提供了更好的支持,例如独立模块。不幸的是,如果你的应用包含重复的依赖,仍然可能出现问题:
🌐 Expo has improved support for more complete node_modules patterns, such as isolated modules. Unfortunately, if your app contains duplicate dependencies, issues may still occur:
- 单个 monorepo 中不支持重复的 React Native 版本
- 在同一个应用中使用重复的 React 版本会导致运行时错误
- Turbo 和 Expo 模块的重复版本可能会导致运行时或构建错误
你可以检查你的多包仓库是否有同一个包的多个版本,例如 react-native,以及它们为什么会通过你使用的包管理器被安装。
🌐 You can check if your monorepo has multiple versions of a package, for example, react-native, and why they're installed through the package manager you use.
- npm why react-native这些命令的输出在不同的包管理器之间会有很大差异,但你可以通过查看包的多个版本来在它们的输出中发现重复的包,例如 react-native@0.79.5 和 react-native@0.81.0。
npm,
🌐 The output of these commands will be very different from one package manager to another, but you can spot duplicate packages in any of their outputs by looking for multiple versions of the package, for example react-native@0.79.5 and react-native@0.81.0.
npm,
为同级依赖添加依赖解析
🌐 Adding dependency resolutions for peer dependencies
如果你无法通过更改依赖来解决重复依赖问题,你可能需要添加一个解决方案。例如,并非所有软件包都已更新其 peerDependencies 以支持 React 19。为了解决这个问题,你可以创建一个解决方案以强制安装单一版本的 react。
🌐 If the duplicate dependency is not resolvable by you changing your dependencies, you may have to add a resolution. For example, not all packages have updated their peerDependencies to support React 19. To work around this, you can create a resolution to force a single version of react to be installed.
{ "name": "monorepo", "private": true, "version": "0.0.0", "workspaces": ["apps/*", "packages/*"], "resolutions": { "react": "^19.1.0" } }
对于 npm,你必须使用名为 overrides 的属性,而不是 resolutions。
🌐 For npm, you have to use a property named overrides rather than resolutions.
去重自动链接的原生模块
🌐 Deduplicating auto-linked native modules
通常,重复的依赖不会引起任何问题。然而,本地模块绝不应该被重复,因为在一次应用构建中,只有一个版本的本地模块能够被编译。与 JavaScript 依赖不同,本地构建不能包含单个本地模块的两个冲突版本。
🌐 Often, duplicate dependencies won't cause any problems. However, native modules should never be duplicated, because only one version of a native module can be compiled for an app build at a time. Unlike JavaScript dependencies, native builds cannot contain two conflicting versions of a single native module.
从 SDK 54 开始,你可以在 app.json 中将 experiments.autolinkingModuleResolution 设置为 true,以自动将 Expo CLI 和 Metro 打包器应用于自动链接。这将强制 Metro 解析的依赖与 自动链接 为你的原生构建链接的原生模块相匹配。
🌐 From SDK 54, you can set experiments.autolinkingModuleResolution to true in your app.json to apply autolinking to Expo CLI and Metro bundler automatically. This will force dependencies that Metro resolves to match the native modules that autolinking links for your native builds.
从 SDK 55 开始,对于单一代码库中的应用,这将自动启用。
🌐 From SDK 55, this is enabled automatically for apps in monorepos.
脚本 '...' 不存在
🌐 Script '...' does not exist
React Native 使用包来传输 JavaScript 和本地文件。这些本地文件也需要进行链接,例如 android/app/build.Gradle 中的 react-native/react.Gradle 文件。通常,这个路径是硬编码为类似这样的:
🌐 React Native uses packages to ship both JavaScript and native files. These native files also need to be linked, like the react-native/react.Gradle file from android/app/build.Gradle. Usually, this path is hardcoded to something like:
安卓 (来源)
apply from: "../../node_modules/react-native/react.gradle"
iOS (来源)
require_relative '../node_modules/react-native/scripts/react_native_pods'
不幸的是,由于提升,在单体仓库中这个路径可能会有所不同。它也不使用Node 模块解析。你可以通过使用 Node 来查找包的位置,而不是硬编码以下内容,从而避免这个问题:
🌐 Unfortunately, this path can be different in monorepos because of hoisting. It also doesn't use the Node module resolution. You can avoid this issue by using Node to find the location of the package instead of hardcoding this:
安卓 (来源)
apply from: new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../react.gradle")
iOS (来源)
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
在上面的代码片段中,你可以看到我们使用 Node 自身的 require.resolve() 方法来查找包的位置。我们明确提到 package.json,因为我们想要找到包的根位置,而不是入口点的位置。通过这个根位置,我们可以解析到包内预期的相对路径。在这里了解更多关于这些引用的信息。
🌐 In the snippets above, you can see that we use Node's own require.resolve() method to find the package location. We explicitly refer to package.json because we want to find the root location of the package, not the location of the entry point. And with that root location, we can resolve to the expected relative path within the package. Learn more about these references here.
所有 Expo SDK 模块和模板都具有这些动态引用,并且可以与 monorepo 一起使用。然而,有时你可能会遇到仍然使用硬编码路径的包。你可以使用 patch-package 手动编辑它,或者将此问题告知包的维护者。
🌐 All Expo SDK modules and templates have these dynamic references and work with monorepos. However, occasionally, you might run into packages that still use the hardcoded path. You can manually edit it with patch-package or mention this to the package maintainers.