先说结论
Windows 不能直接编译 iOS,不等于不能稳定发版。可落地方案是:
- 本地(Windows)负责开发、提交代码、触发流水线
- 云端(macOS)负责 prebuild、签名、归档、导出 IPA 与上传到 App Store Connect
只要把流程标准化,Windows 团队同样可以稳定交付 iOS 版本。
平台分工建议
- Expo:项目配置与构建入口(可选 EAS)
- Codemagic:主构建通道(按构建时间计费)
- Bitrise:备份构建通道(按积分计费)
建议固定一个主通道,另一个只做兜底,避免同版本多平台并行构建导致记录混乱。
为什么选择 Expo + Codemagic + Bitrise
选择这三个平台,不是因为“工具多”,而是因为它们刚好覆盖了 iOS 发布链路里的三个关键问题:开发效率、构建稳定性、成本与风险控制。
- 选择 Expo:降低跨平台开发与维护成本
Expo 把大量原生配置收敛到统一配置层,团队在 Windows 环境也能高效推进业务开发。 - 选择 Codemagic:更贴近移动端的 CI 体验
对移动构建和签名场景支持直接,配置落地快,适合做主构建通道。 - 选择 Bitrise:作为可替换的第二构建通道
当主通道出现临时故障、排队拥堵或额度不足时,可以快速切换,降低发版中断风险。 - 三者组合的价值:解耦单点依赖
Expo 负责“怎么构建”,Codemagic/Bitrise 负责“在哪构建”,组合后可以在不改业务代码的前提下切换构建平台。
没有 Mac,怎么上传到 App Store Connect
可以直接走云上传,有两条常用路径:
expo submit(CLI 方式)- 香蕉云编上传(Web 方式,页面按钮文案:
上传ipa到appstore)
两条路径都不要求本地 Mac,核心差异在于操作习惯:
- 偏自动化:优先
expo submit - 偏可视化:优先香蕉云编
免费额度如何折算成“可构建次数”
三家平台免费口径不同,需要统一换算。
1. 免费口径(2026-03-03 快照)
- Expo(EAS Free):每月
15 Android + 15 iOS - Codemagic(Individuals):每月
500 macOS M2 分钟 - Bitrise(Hobby):每月
300 credits
2. 折算公式
- Expo iOS:
次数 = 15 - Codemagic iOS:
次数 = floor(500 / 单次构建分钟) - Bitrise iOS:
次数 = floor(300 / 单次构建credit_cost)
注:Bitrise 的单次消耗可从构建数据里的 credit_cost 字段读取。
3. 估算示例
- Expo:
15 次/月 - Codemagic(单次 30 分钟):
floor(500/30)=16 次/月 - Bitrise(单次 30 credits):
floor(300/30)=10 次/月
4. 参考链接
- https://expo.dev/pricing
- https://codemagic.io/pricing/
- https://www.bitrise.io/pricing
- https://docs.bitrise.io/en/api/managing-an-app-s-builds.html
开发 -> 构建 -> 上架 SOP
A. 开发与合并
- 功能分支开发并完成自测
- 判断是否有原生变更
有原生变更必须重打 IPA,不走 OTA - PR 评审通过后合并主分支
B. 版本确认
- 检查
app.json的version、ios.buildNumber、ios.bundleIdentifier - 确保
buildNumber每次递增
C. 云端构建 IPA
- 推送主分支触发 CI(或手动触发)
- 确认关键节点通过:
expo prebuild --platform ios --cleanpod install- workspace/scheme 解析
- 证书与描述文件签名
- xcode archive 导出 IPA
- 下载 IPA 与构建日志归档
D. 上传 App Store Connect
无 Mac 场景建议固定以下两种上传方式之一:
- 方式一:
expo submit上传
示例命令:eas submit -p ios --latest --profile production - 方式二:香蕉云编上传
入口:https://www.yunedit.com/update/appstore/list
登录后在该页面点击按钮:上传ipa到appstore,按页面指引上传 IPA 到 App Store Connect 构建版本。 - 上传完成后,在 App Store Connect 选择构建号,填写元数据、隐私问卷、审核备注并提交审核
E. 内部发布后台登记(可选)
如果团队使用香蕉云编后台做发布台账,建议同步维护:
- 列表入口:
https://www.yunedit.com/update/appstore/list - 未登录时会跳转登录页,登录后进入 AppStore 列表
建议记录字段:
- Bundle ID
- 展示版本
- iOS buildNumber
- 更新说明
- 发布状态(待审/已审/已发布)
- 商店链接
F. 发布后归档
- 标记发布完成
- 归档 IPA、日志、审核沟通记录
- 仅 JS/资源变更时,走 OTA 发布流程
关键代码清单(可直接参考)
下面的代码段是最关键的最小集合,能直接拼出一套可用流水线。
1. app.json(iOS 关键字段)
JSON
1{2 "expo": {3 "version": "1.0.0",4 "newArchEnabled": true,5 "ios": {6 "bundleIdentifier": "com.example.app",7 "buildNumber": "20260303123000"8 },9 "runtimeVersion": {10 "policy": "appVersion"11 }12 }13}
2. package.json(常用脚本)
JSON
1{2 "scripts": {3 "ios:set-build-number": "node ./scripts/set-ios-build-number.js",4 "build:ios:cloud:preview": "npm run ios:set-build-number && eas build -p ios --profile preview",5 "build:ios:cloud:production": "npm run ios:set-build-number && eas build -p ios --profile production",6 "submit:ios:latest": "eas submit -p ios --latest --profile production"7 }8}
3. scripts/set-ios-build-number.js(自动递增构建号)
JavaScript
1const fs = require("fs");2const path = require("path");34const appJsonPath = path.join(process.cwd(), "app.json");5const raw = fs.readFileSync(appJsonPath, "utf8");6const config = JSON.parse(raw);78const now = new Date();9const pad2 = (v) => String(v).padStart(2, "0");10const ts = [11 now.getFullYear(),12 pad2(now.getMonth() + 1),13 pad2(now.getDate()),14 pad2(now.getHours()),15 pad2(now.getMinutes()),16 pad2(now.getSeconds()),17].join("");1819config.expo = config.expo || {};20config.expo.ios = config.expo.ios || {};21config.expo.ios.buildNumber = String(process.env.IOS_BUILD_NUMBER || ts);2223fs.writeFileSync(appJsonPath, `${JSON.stringify(config, null, 2)}\n`);24console.log(`ios buildNumber -> ${config.expo.ios.buildNumber}`);
4. codemagic.yaml(最小可用 iOS 工作流)
YAML
1workflows:2 ios_build:3 name: iOS Build4 environment:5 node: 20.18.06 xcode: "26.0.1"7 ios_signing:8 certificates:9 - ios_distribution_cert10 provisioning_profiles:11 - ios_appstore_profile12 scripts:13 - name: Install14 script: npm ci15 - name: Set build number16 script: npm run ios:set-build-number17 - name: Prebuild iOS18 script: npx expo prebuild --platform ios --non-interactive --clean19 - name: Install Pods20 script: cd ios && pod install --repo-update21 - name: Apply signing22 script: xcode-project use-profiles23 - name: Build IPA24 script: xcode-project build-ipa --workspace ios/*.xcworkspace --scheme app25 artifacts:26 - build/ios/ipa/*.ipa
5. bitrise.yml(最小可用 iOS 工作流)
YAML
1format_version: "13"2workflows:3 ios_build:4 steps:5 - git-clone@8: {}6 - script@1:7 inputs:8 - content: |9 #!/usr/bin/env bash10 set -euxo pipefail11 npm ci12 npm run ios:set-build-number13 npx expo prebuild --platform ios --clean14 - cocoapods-install@2:15 inputs:16 - source_root_path: "$BITRISE_SOURCE_DIR/ios"17 - certificate-and-profile-installer@1: {}18 - xcode-archive@5:19 inputs:20 - project_path: "$BITRISE_SOURCE_DIR/ios/*.xcworkspace"21 - scheme: "app"22 - distribution_method: "app-store"23 - deploy-to-bitrise-io@2: {}
最容易踩坑的点
buildNumber没递增,上传直接被拒- 用了 Development Profile,签名或上传失败
- Bundle ID 与证书/Profile 不一致
- workspace/scheme 解析错误,archive 失败
APPLE_DEVELOPMENT_TEAM未配置- 多平台同时构建同一版本,后续无法追溯
一句话收尾
Windows 做 iOS 发布,关键不是“本地编译”,而是“云端构建链路的可重复与可追溯”。流程搭好后,发版会非常稳定。