RN 项目在模拟器上跑得好好的,一到打包就开始出状况:包打出来了,装到手机上一打开是红屏,提示连不上 Metro;或者 Archive 那一步在 Xcode 里是灰的,根本点不动。这两个问题的成因完全不同,一个是 JS 资源没打进包里,一个是设备目标选错了,但初次打包的人往往分不清,只能一遍遍重试。
这篇是把 RN 打 iOS 包这条路完整走一遍留下的记录,重点在打包和分发这一段:怎么把 JS 打成离线 bundle、怎么正确加进 Xcode 工程、AppDelegate 里那个 #ifdef DEBUG 在切什么、Archive 导出 ipa 的每一屏怎么选、最后怎么发给测试同学装上。证书那一层单独有一篇讲透了,这里只做速通。
结合这篇文章一起看 Ionic 打包 iOS 全流程
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- 开发模式的 Debug Server 和发布包的离线 bundle 差在哪,为什么打包必须换掉
react-native bundle每个参数的含义,怎么固化成一条 npm script- 为什么加 bundle 目录必须用
Create folder references,用错了会怎样 AppDelegate.m里jsCodeLocation那段条件编译的作用- 证书和描述文件的速通路径(对照截图快速过一遍)
- Xcode 里签名配置的四个关键位置
- Archive 导出 ipa 的完整点击路径,以及分发方式那一屏怎么选
- 拿到 ipa 之后怎么装到手机、怎么发到内测分发平台
# 一、发布包和调试包到底差在哪
开发 React Native 应用时,js 代码和图片资源通过 Debug Server 提供,但是当我们需要发布应用时,就需要将 js 等资源和应用一起打包。
这句话是整篇的起点,值得展开讲讲。你 yarn start 起来的那个东西叫 Metro(早期叫 packager 或者 Debug Server),它本质是一个跑在 8081 端口的本地服务,App 启动时去它那里实时请求 JS bundle。这套机制让你能热更新、能改一行代码马上看到效果,但它有个前提:手机和你的电脑得在同一个网络里,而且你的电脑得开着。
所以测试同学拿到包之后连不上你的电脑,App 一启动就红屏报 Could not connect to development server。
发布包要做的事就是把这条实时链路砍掉,提前把所有 JS 和图片资源编译成一个静态文件塞进 ipa 里,App 启动时从自己的包体里读。这就是下面要打的 jsbundle。
# 二、打离线资源
# 2.1 把打包命令固化到 package.json
通过 react-native bundle 命令可以打包离线资源。为了日后打包方便,我们把打包指令填在 package.json 下。
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
"test": "jest",
"bundle-ios": "node node_modules/react-native/local-cli/cli.js bundle --entry-file index.js --platform ios --dev false --bundle-output ./ios/bundle/index.jsbundle --assets-dest ./ios/bundle"
},
这段的意义在于把一条又长又容易记错的命令固定下来,以后谁来打包都不会漏参数。
bundle-ios 命令参数含义:
--entry-file:入口文件。--platform:平台名称(ios 或者 android)。--dev:是否是开发模式,设置为 false 的时候将会对 JavaScript 代码进行优化处理。--bundle-output:生成的jsbundle文件的名称。--assets-dest:图片以及其他资源存放的目录
这几个里最容易出事的是 --dev。它设成 false 才会走生产优化,把开发期的警告、__DEV__ 分支、YellowBox 那套东西剔掉,包体和运行时性能都差一截。忘了写这个参数默认是 true,打出来的包会带一堆开发期代码。
--assets-dest 也别漏,它管的是图片资源。只打 JS 不打资源的话,App 里所有 require('./img/x.png') 引的图会全部变成空白,而且不报错,非常难查。
补一句时效性说明:上面这条命令里的 node_modules/react-native/local-cli/cli.js 是 RN 0.5x 那个时期的路径。后来 RN 把 CLI 拆成了独立的 @react-native-community/cli 包,现在直接写 npx react-native bundle --entry-file index.js ... 就行,参数完全一样。老项目里这条路径还能跑,新项目照着官方文档写。
这样打包只需要在根目录下输入 npm run bundle-ios 即可(切记一定要先在 项目 --> ios 下新建 bundle 文件夹,不然会报错)。

那个「先新建 bundle 文件夹」的提醒是有原因的:--bundle-output 只会创建文件,不会替你创建父目录,目录不存在就直接报 ENOENT: no such file or directory。命令跑起来之后终端会显示进度条,几十秒到一两分钟不等,看项目大小。
之后你会发现 bundle 文件下面已经有了内容(如下图)。

正常情况下这个目录里应该有两样东西:一个 index.jsbundle 文件(就是打包好的 JS,通常几 MB),还有一个 assets 目录(项目里所有被 require 引用过的图片)。如果 assets 是空的或者压根没生成,回去检查 --assets-dest 参数。
# 2.2 添加离线资源到项目中
在 Xcode 中添加资源到项目中,必须使用 Create folder references 的方式(也就是文件夹的方式)添加 bundle 文件夹。

操作路径是在 Xcode 左侧工程目录上右键,选 Add Files to "项目名",然后在弹窗里选中刚生成的 bundle 文件夹。
必须使用 Create folder references 的方式添加:

这一屏是整节的关键,弹窗底部有两个单选项,一定要选 Create folder references,不要选默认的 Create groups。
那这两个选项差在哪呢?Create groups(黄色文件夹)只是 Xcode 里的一个逻辑分组,它会把目录里的文件平铺着加进工程,目录层级在最终的 ipa 里是不存在的。Create folder references(蓝色文件夹)则是保留真实目录结构,整个文件夹原样拷进包体。
RN 的资源引用是带路径的,assets/img/logo.png 这种,层级被拍平之后运行时就找不到图了。表现是 App 能启动、能跑,但所有图片都不显示。这个坑我踩过,排查了一下午才发现是加文件时选错了单选项。
添加成功后 bundle 文件夹为蓝色(如下图)。

蓝色就是对的,黄色就是选错了,删掉重加一次。这是唯一一个不用运行就能确认对错的地方,加完顺手瞄一眼颜色,能省掉后面一堆无头案。
# 2.3 修改 AppDelegate.m 文件
在开发的过程中可以在这里配置 Debug Server 的地址,当发布上线的时候,就需要使用离线的 jsbundle 文件,因此需要设置 jsCodeLocation 为本地的离线 jsbundle。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSURL *jsCodeLocation;
// jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
// jsCodeLocation = [NSURL URLWithString:@"http://192.0.0.0:8081/index.bundle?platform=ios&dev=true"];//真机Hot reloading
#ifdef DEBUG
jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];//开发调试
#else
jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"bundle/index" withExtension:@"jsbundle"];//上线打包
#endif
........
}
这段代码在做的事,是让同一份工程按构建配置自动切换 JS 的来源。
#ifdef DEBUG 这个宏在 Debug 构建时成立,走上面那行,从 RCTBundleURLProvider 拿地址,也就是连本地 Metro;Release 构建时宏不成立,走 #else 分支,从 App 包体里读 bundle/index.jsbundle。这样你不用每次打包前手动改代码,切个 Scheme 就行。
有两个细节要对齐。一是 URLForResource:@"bundle/index" 里的 bundle/ 前缀,它必须和你在上一节加进来的蓝色文件夹名一致,文件夹叫别的名字这里就得跟着改,否则运行时返回 nil,App 直接白屏闪退。二是被注释掉的那行写死 IP 的写法(http://192.0.0.0:8081/index.bundle),那是早年真机热重载的做法,把 IP 换成你电脑的局域网地址就能在真机上连 Metro。现在 RN 已经能自动发现开发机地址,一般用不上了,留着当个应急手段。
Release 构建其实还有一条更省事的路:Xcode 的 RN 模板里默认带了一个 Bundle React Native code and images 的 Build Phase 脚本,它会在每次 Release 构建时自动执行 bundle 命令并把产物打进包里。也就是说,如果你的工程里这个脚本是好的,第二节手动打 bundle、手动加文件夹这两步都可以省掉。手动这套的价值在于可控,脚本出问题(比如 nvm 装的 node 在 Xcode 环境里找不到)的时候,你有条退路。
# 三、iOS 证书配置速通
建议阅读这篇文章更详细。React Native 打包前奏之 iOS 证书配置
这一节按截图把证书那条路快速过一遍,概念层面的解释(CSR 和公私钥的关系、Explicit 和 Wildcard 的区别、描述文件为什么是组合体)都在上面那篇里,这里只给操作路径。如果你更想让 Xcode 自动管理签名、不手动折腾 CSR,可以看 RN 构建 iOS 包发布到 AppStore 全流程 那篇的自动签名路线。
首先你得有一个开发者账号才可以进行以下步骤。
# 3.1 用钥匙串生成 CSR
在 mac 上搜索钥匙串打开。

打开之后走菜单 钥匙串访问 -> 证书助理 -> 从证书颁发机构请求证书。
