Expo iOS 构建报错怎么办?prebuild、config plugin 与 Expo 原生模块
Expo iOS 构建报错,多半是没弄清 ios 目录由 prebuild 按 app.json 和 config plugin 生成,手改会被下次生成覆盖。本文讲清 CNG 规则,按现象、原因、改法整理 12 个常见问题,版本信息截至 SDK 58。
Expo iOS 构建报错,多数是因为没弄清一件事:ios 目录是 npx expo prebuild 按 app.json 和 config plugin 生成的,手改会被下一次 prebuild --clean 覆盖。解决办法是把权限说明、entitlements、Bundle ID 这类原生配置写进 app 配置,改完重新生成、重新 pod install,再完整构建。下面先讲清 CNG 的规则,再按「现象 → 原因 → 改法」整理 12 个常见问题,最后说明在 KXBuild 里构建 Expo 的要点。版本信息以 Expo 官方文档为准,截至 2026 年 10 月,最新的是 SDK 58。
你的 Expo SDK 是哪个版本?
在 package.json 里看 expo 的版本号。最近三个 SDK 的对应关系如下,来自 Expo 官方的版本表。
| Expo SDK | 发布时间 | React Native | Node.js 最低版本 |
|---|---|---|---|
| 58 | 2026 年 10 月 | 0.88 | 22.13 |
| 57 | 2026 年 6 月 | 0.86 | 22.13 |
| 56 | 2026 年 5 月 | 0.85 | 20.19 |
SDK 58 要求的最低 iOS 版本是 16.4。Expo 底层就是 React Native,Pods、Hermes、新架构这类问题和纯 RN 项目一样,见 React Native 新架构 iOS 常见问题。这篇只讲 Expo 特有的部分。
Expo 的 CNG 有哪些规则?
Expo 把这种做法叫 Continuous Native Generation(CNG)。你维护的是原生配置的「定义」,也就是 app.json(或 app.config.js)和 config plugin,原生工程在需要时由 npx expo prebuild 生成。记住四条规则。
- ios 目录是产物。新建的 Expo 项目会自动把 ios、android 目录写进
.gitignore。 - 手改会丢。直接改 ios 目录里的 Info.plist、AppDelegate、Podfile,下一次
prebuild --clean就没了。 - 原生配置写进 app 配置。权限说明、entitlements、Bundle ID、部署目标都有对应字段;字段覆盖不到的,用 config plugin。
- 改完要重新生成。config plugin 只在 prebuild 时执行,改了 app.json 不重新生成,App 里不会有变化。
| 你想改的东西 | 写在哪里 |
|---|---|
| Bundle ID、版本号、构建号 | ios.bundleIdentifier、version、ios.buildNumber |
| 权限用途说明、其他 Info.plist 键 | ios.infoPlist,或库的 config plugin 选项 |
| 能力(推送、关联域名、App Groups 等) | ios.entitlements、ios.associatedDomains |
| 最低 iOS 版本 | ios.deploymentTarget(SDK 56 起内置) |
| 隐私清单 | ios.privacyManifests |
| Podfile 的 use_frameworks、额外的 Pod | expo-build-properties 插件 |
| 以上都覆盖不到的原生改动 | 自己写 config plugin |
Expo iOS 构建报错有哪些常见原因?
| # | 现象 | 原因 | 改法 |
|---|---|---|---|
| 1 | 改了 app.json,App 里没变化 | 没有重新生成 ios 目录 | prebuild --clean 后重新 pod install、构建 |
| 2 | 手改的原生代码不见了 | 被 prebuild --clean 覆盖 |
改动写进 app 配置或 config plugin |
| 3 | config plugin 没生效 | 没写进 plugins、没重新生成,或 app.config.js 丢了配置 | 用 npx expo config 查最终配置 |
| 4 | 权限说明没进 Info.plist,调用即闪退 | 写错层级,或库的插件选项覆盖了 | 写在 expo.ios.infoPlist 下 |
| 5 | 推送、关联域名等能力不生效,签名报 entitlements 不符 | entitlements 没写,或描述文件没开对应能力 | ios.entitlements + 重新生成描述文件 |
| 6 | Expo Go 里报 Cannot find native module |
Expo Go 不含你装的原生库 | 改用开发构建,装 expo-dev-client |
| 7 | pod install 失败 | 没装 JS 依赖、部署目标不够、Pods 残留 | 先 npm install,查部署目标,删 Pods 重装 |
| 8 | 库装了但原生部分没链接进来 | 库不在 autolinking 结果里 | 用 expo-modules-autolinking 命令排查 |
| 9 | Release 包打开白屏或闪退 | JS bundle 没打进去,或环境变量缺失 | 查打包阶段日志和 .env |
| 10 | 签名报 Bundle ID 与描述文件不匹配 | app 配置里的 Bundle ID 和描述文件的 App ID 不一致 | 统一 Bundle ID,重新生成 |
| 11 | eas.json 里的配置在本地构建不生效 | 那些配置只给 EAS 用 | 本地用 .env 或命令行变量 |
| 12 | 升级 SDK 后编译报大量错误 | ios 目录还是旧模板 | npx expo install --fix 后 prebuild --clean |
prebuild 和 config plugin 出问题怎么办?
1. 改了 app.json,App 里没变化
app.json 只是定义,真正进 App 的是 ios 目录里生成的 Info.plist、工程文件和 Podfile。改了 Bundle ID、权限说明、图标、插件选项,或者新装了带原生代码的库,都要重新生成。
1npx expo prebuild --clean --platform ios
--clean 先删掉现有的原生目录再生成。Expo 文档把它称为最稳妥的用法:不带 --clean 时改动会叠加到已有文件上,部分 config plugin 重复执行会产生不一致的结果。生成后重新 pod install,再完整构建一次,不要只刷新 JS。
2. 手改的原生代码不见了
你在 ios 目录里加的 Swift 文件、改的 AppDelegate、Podfile,执行 --clean 后都会消失。git 里有未提交的改动时,prebuild 会先提示你,不要直接跳过这个提示。
两种项目选一种,不要混着用。
- 按 CNG 维护。ios 目录不进版本库,所有原生改动写进 app 配置或 config plugin。
- 自己维护 ios 目录。把 ios 目录提交进版本库,以后直接改原生文件,不再运行
prebuild --clean。这时 app.json 里的原生字段不会再自动同步到 ios 目录,要改的地方直接在原生文件里改。
3. config plugin 没生效
config plugin 写在 app 配置的 plugins 数组里。不带选项时只写包名,带选项时写成「包名 + 选项对象」的二元数组。
1{
2 "expo": {
3 "plugins": [
4 "expo-dev-client",
5 [
6 "expo-build-properties",
7 {
8 "ios": { "useFrameworks": "static" }
9 }
10 ]
11 ]
12 }
13}
插件不生效,按顺序查四处。
- 重新生成了没有。插件里的修改只在 prebuild 同步阶段执行,见第 1 条。
- 写进 plugins 了没有。只 npm install 一个库,不会自动把它的插件加进 plugins,按库的文档加上。
- app.config.js 有没有丢掉 app.json 的内容。Expo 先读 app.json,再把结果传给 app.config.js。动态配置要写成
({ config }) => ({ ...config, ... }),在传入的 config 上修改;返回一个全新对象,app.json 里的 plugins 就丢了。同时存在 app.config.ts 和 app.config.js 时,用的是 ts。 - 插件版本对不对。用
npx expo install 包名安装,它会选和当前 SDK 匹配的版本。
运行下面的命令可以看到 Expo CLI 解析后的最终配置,先确认插件和字段确实在里面。
1npx expo config
4. 权限说明没进 Info.plist
权限说明写在 expo.ios.infoPlist 下,键名和 Info.plist 完全一致。
1{
2 "expo": {
3 "ios": {
4 "infoPlist": {
5 "NSCameraUsageDescription": "用于扫描快递单上的条形码",
6 "NSLocationWhenInUseUsageDescription": "用于显示离你最近的门店"
7 }
8 }
9 }
10}
常见的写法错误是把 infoPlist 放在 expo 下面一层而不是 ios 里,或者键名拼错。很多 Expo 库的 config plugin 也会写入权限说明,并带一句英文的默认文字。Expo 文档提醒这些默认文字大多需要按你的场景改写,用插件选项改即可,例如 expo-media-library 的 photosPermission。
同一个键在两处都写了,以最终生成的 Info.plist 为准,生成后打开 ios/<App>/Info.plist 核对一次。
缺少用途说明,App 在访问对应数据时会被系统终止,上传时也会收到 ITMS-90683。中文文案怎么写见 Info.plist 权限用途说明。
5. entitlements 与能力
推送、App Groups、iCloud、Keychain 共享这类能力,要同时满足两件事:App 的 entitlements 里有这个键,描述文件对应的 App ID 也开了这项能力。
1{
2 "expo": {
3 "ios": {
4 "entitlements": {
5 "aps-environment": "development",
6 "com.apple.security.application-groups": ["group.com.example.myapp"]
7 },
8 "associatedDomains": ["applinks:www.example.com"]
9 }
10 }
11}
很多库(例如推送、通过 Apple 登录)会通过自己的 config plugin 加 entitlements,你只要把插件加进 plugins。加完重新生成,再去开发者后台给 App ID 打开同一项能力,重新生成并下载描述文件。描述文件里没有的能力,签名时会报 entitlements 和描述文件不符。各项能力和描述文件的关系见 Bundle ID、App ID 与描述文件的关系。
Expo Go 跑不起来时为什么要用开发构建?
6. Expo Go 里报找不到原生模块
1Error: Cannot find native module 'ExpoXxx'
Expo Go 是一个预装了固定原生库的通用 App。你装的库要是不在里面,或者你改了原生配置,Expo Go 里就跑不起来。Expo 官方的说法是,开发构建(development build)相当于你自己的 Expo Go,可以用任何原生库、改任何原生配置。
做法是在项目里装 expo-dev-client,然后自己构建一个开发版装到手机上。
1npx expo install expo-dev-client
2npx expo start
装了 expo-dev-client 之后,npx expo start 默认就面向你的开发构建,而不是 Expo Go。
| Expo Go | 开发构建 | |
|---|---|---|
| 原生库 | 只有 Expo Go 自带的 | 你项目里装的全部 |
| 改原生配置 | 不行 | 可以,重新构建即可 |
| 怎么得到 | 从 App Store 下载 | 自己构建、签名、安装 |
| 适合 | 学习、纯 JS 的原型 | 正式项目 |
pod install 和 autolinking 失败怎么查?
7. pod install 失败
Expo 生成的 Podfile 会调用 node 去 node_modules 里找 expo 的 autolinking 脚本。按报错分三类处理。
- 报
Cannot find module或 autolinking 脚本加载失败。还没装 JS 依赖,先在项目根目录npm install,再进 ios 目录 pod install。 - 报
required a higher minimum deployment target。某个库要求的 iOS 版本比工程高。在 app 配置里提高ios.deploymentTarget,重新生成再 pod install。SDK 55 及以前的项目用expo-build-properties的ios.deploymentTarget。 - 升级或换分支之后报版本冲突。删掉 ios 目录重新生成,旧的 Pods 和 Podfile.lock 会一起清掉。
和 React Native 共有的 Pods 问题,例如 Codegen 产物缺失、use_frameworks 冲突,见 React Native 新架构 iOS 常见问题。
8. 库装了,原生部分没进来
Expo 用 expo-modules-autolinking 在 pod install 时把库的原生代码链接进来,SDK 52 起它也替代了 React Native 社区 CLI 的 autolinking。库没被链接上,运行时同样会报找不到原生模块。
在项目根目录运行下面的命令排查。
1npx expo-modules-autolinking search
2npx expo-modules-autolinking resolve --platform apple
3npx expo-modules-autolinking verify
search 列出找到的 Expo 模块,resolve 给出每个模块用的 podspec 路径,verify 检查有没有重复的原生模块。
按 Expo 文档,一个包要被链接,根目录要有 expo-module.config.json,并且 platforms 里包含对应平台。monorepo 里放在 node_modules 以外的本地模块,在 package.json 的 expo.autolinking.searchPaths 里加上它的目录。npx expo-doctor 也会列出重复的原生模块和安装路径。
Release 包、签名和 EAS 配置出错怎么改?
9. Release 包白屏或闪退
Release 包不连 Metro,JS 要在构建时打包进 App。白屏多数是这一步失败了,构建日志里找 Bundle React Native code and images 阶段的第一条报错。找不到 node、设了 SKIP_BUNDLING 这类原因和 RN 一样,见 React Native 新架构 iOS 常见问题。
Expo 项目多一个常见原因:环境变量。Expo CLI 打包时,会把代码里的 process.env.EXPO_PUBLIC_XXX 替换成 .env 文件里的值。构建机上没有 .env,这些值就是 undefined,接口地址为空,App 一启动就出错。
确认构建机上有正确的 .env。SDK 58 起,.env.staging 这类自定义名字的文件不再被加载,只用标准的 .env 文件。另外别把密钥放进 EXPO_PUBLIC_ 变量,它们会以明文打进 JS bundle。
10. Bundle ID 与描述文件不匹配
签名时报找不到匹配的描述文件,或描述文件的 App ID 与 App 不一致,先比对三个地方。
- app 配置里的
ios.bundleIdentifier。 - 生成后 ios 工程里的
PRODUCT_BUNDLE_IDENTIFIER。没有重新生成时,它还是旧值。 - 描述文件对应的 App ID。
用 app.config.js 按环境切换 Bundle ID 的项目要格外留意。Expo 的多变体教程用 APP_VARIANT 环境变量区分开发版和正式版,例如开发版用 com.example.myapp.dev。这个变量在 eas.json 里设,本地构建时没有它,生成的就是另一个 Bundle ID。生成前在当前终端设好同样的变量,再重新生成。
签名报错的通用排查见 iOS 签名报错与 ipa 安装失败。
11. EAS 专属的配置在本地构建不生效
这几类配置只在 EAS Build 上起作用,本地构建读不到。
| 配置 | 在 EAS 上 | 本地构建时怎么办 |
|---|---|---|
eas.json 构建配置里的 env |
构建时注入 | 在终端里设同名变量,或写进 .env |
| EAS 上保存的环境变量 | 构建时可用 | 按可见性设置,用 eas env:pull 拉到本地 |
| EAS 托管的证书和描述文件 | 自动签名 | 自己准备 .p12 和 .mobileprovision |
| 没有 ios 目录时自动 prebuild | 每次构建前生成 | 自己生成,见第 1 条 |
还有一点容易混淆:按 Expo 文档,项目里已经有 ios 目录时,EAS Build 不会再运行 prebuild,以免覆盖你的改动。所以把 ios 目录提交进版本库之后,EAS 上也不会再同步 app.json 的原生字段。
12. 升级 SDK 后编译报错
升级 SDK 时先更新 expo 包,再让 Expo 把其他依赖对齐到这个 SDK 的版本,最后用新模板重新生成。
1npm install expo@^58.0.0
2npx expo install --fix
3npx expo prebuild --clean --platform ios
--clean 会用新 SDK 的模板生成 ios 目录,旧模板留下的 AppDelegate、Podfile 差异就没了。自己维护 ios 目录的项目不能这么做,要对照 Expo 的升级说明手动改原生文件。升级后运行 npx expo-doctor 检查依赖版本和重复模块。
在 KXBuild 里怎么构建 Expo 的 iOS 端?
在 Windows、Linux 上用 KXBuild 构建 Expo 的 iOS 端,要点有四条。
- 第一次先体检。带项目路径运行
kxbuild doctor D:\work\expohello,或在 KXBuild 视图运行「体检并修复开发环境」,它会检查 Expo 环境并生成 ios 目录。之后在 ios 目录执行pod install,构建用.xcworkspace。 - 重新生成。体检只在 ios 目录不存在时生成它,改了 app.json 或装了新的原生库,删掉 ios 目录后重新体检,再 pod install。
- 调试。另开终端运行
npx expo start,选中设备按 F5。原生代码在 VS Code 里断点,JS 用 Expo 自带的调试器。 - 出 ipa。
kxbuild build .\ios\expohello.xcworkspace -c Release --export app-store,证书和描述文件自己放进证书目录。
Expo iOS 构建的完整步骤见 Expo 不用 EAS,在 Windows 本地构建 iOS 安装包和 Expo 技术栈。体检失败时加 -v 看完整日志,见排错与 FAQ。
常见问题
Expo 的 ios 目录可以手动改吗?
可以改,但按 CNG 维护的项目,下一次 prebuild –clean 会覆盖手改的内容。长期保留的原生配置写进 app.json 的 ios 字段或 config plugin;确实要自己维护 ios 目录,就把它提交进版本库,以后不再运行 prebuild –clean。
改了 app.json 为什么 iOS 上没有生效?
app.json 要经过 prebuild 才会写进 ios 工程。运行 npx expo prebuild –clean –platform ios 重新生成,再 pod install 并完整构建;在 KXBuild 里是删掉 ios 目录后重新体检。
Expo Go 和 expo-dev-client 有什么区别?
Expo Go 是预装了固定原生库的通用 App,不能加你自己的原生库。装了 expo-dev-client 后自己构建的开发版相当于你自己的 Expo Go,项目里的所有原生库和原生配置都能用。
config plugin 不生效怎么排查?
先运行 npx expo config 看最终配置里有没有这个插件,再确认改完重新运行过 prebuild。用 app.config.js 的项目,要在传入的 config 上修改并返回,否则 app.json 里的 plugins 会丢失。
Expo SDK 58 支持的最低 iOS 版本是多少?
按 Expo 官方版本表,SDK 58 要求 iOS 16.4 或更高,对应 React Native 0.88。某个库要求更高时,在 app 配置的 ios.deploymentTarget 里提高,重新生成后再 pod install。