乐于分享
好东西不私藏

并排安装开发版与生产版:用App Variants彻底告别反复卸载

并排安装开发版与生产版:用App Variants彻底告别反复卸载

"

并排安装,是开发体验的真正升级——不再为调试而反复卸载生产应用。

你刚把应用发布到生产环境,用户正在安装它。但很快,第一个 bug 报告就来了。你打开手机上的生产构建,确实,问题就在那里。你起草了修复方案,安装开发构建,开始调试——然后发现,手机上同一时间只能安装一个版本。于是你卸载生产应用,装上 dev build,调试完再装回生产应用。一周三次,一定有更好的办法。

Expo 的 App Variants通过给每个构建赋予自己的标识符来解决这个问题。这样不同变体(例如 dev、preview 和 production)可以并排安装在同一设备上,每个都被视为独立的应用。本文将完整讲解配置流程与实战技巧。

📌 本文看点

01

给每个构建独立标识,实现并排安装

02

从动态配置到 EAS 构建全流程

03

QR 码、图标、更新对齐实战

01

THE PROBLEM

为什么需要 App 变体

手机上同一时间只能安装一个版本的应用。听起来合理,直到你一周内第三次为了调试 dev build 而卸载生产应用,然后再重新安装,再卸载。App Variants 通过给每个构建赋予自己的标识符来解决这个问题——不同变体可以并排安装在同一设备上,每个都被视为独立的应用。

「这是一个真正的开发体验升级,也是我在实际项目中第一个配置的东西。」

02

ANATOMY

App 变体的两个维度

每个构建有两个独立的设置:身份(Identity)环境(Environment)。理解这两个维度,是掌握 App Variants 的前提。

身份(Identity)

指 iOS 上的 bundle identifier 或 Android 上的 package name,例如 com.myapp.app。标识被固化在原生构建中,它决定什么代表一个唯一应用,以及新安装是否替换旧安装。设备每个标识只保留一个应用——如果开发和生产构建共享一个标识,安装任一个都会替换另一个。

环境(Environment)

指当你的 app config 被评估时加载的一组变量。EAS 有三个内置环境:developmentpreviewproduction。环境决定了应用运行后的行为方式,诸如 API URL 或 analytics key 就存在于环境中。

💡 重要说明:expo start 时做的任何操作都不会改变标识。本指南适用于你在本机或通过 EAS Build 创建的自定义构建,不适用于 Expo Go。

03

CONFIG

配置应用支持多变体

你的项目可能目前有一个静态的 app.json。静态文件适合稳定值,但变体并排安装需要各自的标识符,而标识符在 app config 中设置,静态文件只能持有一个。所以你需要动态配置(Dynamic Config),推荐使用 app.config.ts

两个配置文件的协作

当两个文件都存在时,Expo 先读取 app.json,将其作为 { config }传递给动态配置,然后使用动态配置返回的结果。app.json 是基础层,app.config.ts 是上层的薄覆盖层

基于 APP_VARIANT 切换

创建 App Variant 需要告诉 config 正在构建哪一个。为此引入 APP_VARIANT——一个配置读取的普通环境变量。使用 switch语句将每个变体放在自己的行上,共享的 ID 部分放在 APP_ID_PREFIX中,每个 case 只设置后缀。

调试配置时可以用以下命令查看 Expo 将使用的已解析配置:

bash

npx expo config       # 读取已解析配置

npx expo config --json  # 机器可读输出

npx expo config --json | jq .name # 提取单个字段

为什么保留两个文件

可以只用 app.config.ts,很多应用确实这样做。但问题是:Expo 工具只会写入静态 app.json。某些服务更进一步需要静态 app.json——例如 Expo Launch 没有该文件就无法运行,因为它需要将标识字段写入配置,而动态配置无法接受这些。

默认为开发环境

让缺失的 APP_VARIANT落入 development 不是必需的,但作者更倾向于只在明确要求时才使用生产标识。本地运行的命令不要求选择变体,所以默认值就是你得到的——本地几乎每次都是 dev build。落入 development 让你保持在开发后端;落入 production 会将你发送到真实后端,可能悄悄成功并用测试数据污染生产环境。

04

EAS BUILD

在 EAS 上构建每个变体

配置现在可以对 APP_VARIANT做出反应,EAS Build 需要为每次构建设置该变量。最简单的方式是在 eas.json中为每个 profile 设置 env块。运行 eas build --profile development时,EAS 会在评估配置前设置 APP_VARIANT=development,dev build 安装在生产旁边而不会冲突。

变量存放位置

eas.json中每个 profile 的 env块足以开始构建变体,但有限制:env 块仅适用于 eas build,eas update 无法看到它,本地命令也需要从 shell 读取。更好的方案是使用 EAS 环境变量:

...bash

# 创建变量

eas env:create --name APP_VARIANT \

 --value development \

 --environment development \

 --visibility plaintext

# 本地拉取环境变量

eas env:pull --environment development

这样 expo start和 expo run从 .env.local读取 APP_VARIANT,构建从 EAS 上的同一环境读取。只需在一个地方更改值

在本机构建变体

本地使用 expo run构建一旦变量在 .env.local中就不需要额外设置。切换变体时需用 prebuild --clean重新生成原生项目:

bash

APP_VARIANT= npx expo prebuild --clean

npx expo run:[platform]

本地构建 Release 配置(嵌入 JS 和配置,无需 dev server):

bash

# iOS

npx expo run:ios --configuration Release

# Android

npx expo run:android --variant release

!踩坑提示 🕳

CNG(Continuous Native Generation)按需从 app config 生成原生项目,而不是将它们提交到源代码控制。切换回开发环境时,务必运行 APP_VARIANT=development npx expo prebuild --clean重新生成。

05

RUNTIME

环境如何到达运行中的应用

身份在构建时固定。开发时需注意:如果已用 npx expo prebuild生成了本地原生项目,expo start从磁盘上的原生项目获取其启动 scheme,APP_VARIANT不影响打开哪个已安装的应用。

运行时通过 expo-constants读取的配置取决于构建类型:

构建类型
配置来源
行为
普通构建
编译时固化
配置在构建时固定,只有重新构建才能改变
开发构建
从 dev server 下载
配置反映服务器环境,而非编译时环境

⚠️ 关键注意点:以 preview 启动服务器后打开 dev build,它会无报错地使用 preview 环境的值运行——dev 菜单甚至会显示「MyApp (Preview)」在一个标识为 dev 的构建上。修复方法是匹配服务器变体与构建变体。

EXPO_PUBLIC_ 变量与 extra 访问

EXPO_PUBLIC_变量在 bundle time 内联到 JS bundle 中。没有该前缀的变量(包括 APP_VARIANT)永远不会直接到达 JS,需通过配置的 extra字段暴露:

...javascript

import Constants from "expo-constants";

variant: {Constants.expoConfig?.extra?.variant}

APP_VARIANT: {String(process.env.APP_VARIANT)}

EXPO_PUBLIC_APP_VARIANT: {String(process.env.EXPO_PUBLIC_APP_VARIANT)}

APP_VARIANT渲染为 undefined(缺少 EXPO_PUBLIC_前缀),而 EXPO_PUBLIC_APP_VARIANT显示其值。Preview 或生产安装继续显示编译时的变体,只有 dev build 从服务器获取新值。

06

PRACTICAL TIPS

QR 码、服务注册与图标

让 QR 码打开你的 dev build

安装多个变体时,Expo CLI 的 QR 码可能打开错误的应用。原因是 expo-dev-client默认添加一个从应用 slug 生成的 scheme exp+,slug 在变体间相同,所以每个变体注册相同的 scheme 并响应相同的链接。OS 打开哪个应用不可靠。修复方法是让 dev build 成为唯一响应该 scheme 的应用:

...javascript

plugins: [

 [

  "expo-dev-client",

  {

   addGeneratedScheme: process.env.APP_VARIANT === "development",

  },

 ],

],

每次开发会话前重新生成 development 原生项目:APP_VARIANT=development npx expo prebuild --clean。如果变量在 EAS 上,先运行 eas env:pull --environment development

向服务注册每个变体

APP_VARIANT可以使配置返回不同的 bundle identifier 或 package name,但无法创建这些标识符所需的外部注册。如果服务在设置时要求你的 package name 或 bundle identifier,就需要为每个变体单独设置。对于记录或发送生产数据的任何内容(如分析、崩溃报告、推送通知),单独注册通常是值得的,这样 dev build 就不会污染生产数据。

给每个变体自己的图标

安装两个或三个并排时,不同的图标是避免点错的最快方式。实现方式:使用动态配置中的一个额外 helper,变体图像可以简单到相同图标配不同背景色。生产环境返回 undefined,让 app.json中的图标原样通过。注意 ios.icon和 android.adaptiveIcon优先于顶层 icon,所以也要覆盖它们。

07

UPDATES

在更新中保持变体对齐

EAS Update 是构建时间之外保持对齐的又一个地方。两个关键概念:Channel(通道)决定更新到达哪个变体,来自构建 profile;Environment(环境)决定更新携带的值,通过 --environment设置。

保持名称一致,整条链匹配preview变体由 previewprofile 构建,加载 preview环境,在 preview通道接收更新:

bash

eas update --channel preview --environment preview

「通道路由更新,环境决定行为,保持它们对齐是你的责任。将通道指向错误的环境,preview 变体会愉快地接受携带开发环境值的更新,没有任何报错。」

THE END

总结

这看起来可能很多,但入门真的很简单。你添加一个读取一个变量的动态配置、每个环境一个构建 profile、以及匹配的通道,你关心的每个构建就都存在一部手机上,每个都诚实地表明自己的身份。它很少需要更新,作者现在将相同的配置复制到新项目中只需少量修改。

「当生产 bug 在你开发功能中途出现时,你切换到生产环境,复现它,然后切回你之前的状态——没有卸载任何东西,没有丢失任何东西。」

App Variants 是你们都应得的开发体验的重要组成部分

END

我是李青仪,热衷于分享 React Native 与 Expo 开发干货。

如果你觉得今天这篇有收获,欢迎点赞、在看、转发三连,我们下篇见。