你可能已经完成了 UI 开发,个人资料页面也加上了“上传照片”按钮,但接下来的环节却突然变得棘手起来。实际的图片选择流程涉及原生权限、系统级界面、与开发者预期不同的返回数据结构,以及只有在发布真实 build 后才会暴露的构建细节。
这正是 Expo Image Picker 的用武之地。它是 Expo 官方提供的库,用于调用系统 UI 从设备相册选择图片或视频,或使用相机拍摄照片,正如在 Expo 软件包仓库中所述。在实际应用中,这意味着你可以获得一个可靠的原生媒体输入桥接方案,但它并不能提供在所有设备上表现完全一致的自定义媒体体验。
本指南面向实际落地场景而非简单的 Demo。它专注于生产环境中的关键决策:managed 与 bare workflow 的配置、不会让你措手不及的权限处理、安全的结果解析,以及用户选择文件后的实用上传模式。如果你在使用自定义原生环境,了解它与 Expo EAS update 选项的区别也很有帮助,Capgo 会在其替代方案指南中进行对比.
目录
Expo Image Picker 入门指南
产品经理要求添加头像功能。一周后,同样的功能又需要支持上传收据、事故报告的相机拍摄,以及用户首次拒绝权限后的重试机制。图像输入需求增长迅速,因为它涉及原生权限、系统级 UI、临时文件处理以及后端上传流程。
expo-image-picker 这就是 Expo SDK 模块的用武之地。它会调用平台的选择器或相机 UI,并以你的 React Native code 可以处理的格式返回所选媒体。JavaScript API 非常小。主要的挑战在于如何在 managed 和 bare 项目中都正确配置原生环境、权限流和结果处理。
主要的权衡非常直接。你让 iOS 和 Android 使用各自的媒体 UI,而不是构建自定义选择器。这通常效果更好:用户已经熟悉系统界面,权限 prompt 的行为符合 OS 预期,而且你的团队无需在 JavaScript 中维护一套相册实现。
将其视为具有 React 接口的原生集成功能。
这种思维方式很有帮助,因为故障点很少出现在调用选择器的按钮上。它们通常源于以下三个方面:
- 原生配置: 缺少插件设置、权限字符串错误,或修改配置后构建版本过旧
- 运行时行为: 用户可能会拒绝访问、在 iOS 上仅授予有限的库访问权限,或在未选择任何内容的情况下取消流程
- 结果解析: 当前的 API 返回的是一个
assets数组,因此那些直接读取result.uri旧版示例会直接报错
工作流的选择也会改变配置路径。在托管的 Expo 应用中,大部分原生工作都在 app config 中完成,且配置更改后需要重新构建。在 bare app 中,你仍然可以使用 Expo 模块 API,但需要更直接地验证底层的 iOS 和 Android 项目设置。如果你的团队使用的是自定义客户端而非 Expo Go,本指南可以配合 Capgo 的 Expo EAS 替代方案使用 适用于需要在 Expo Go 之外进行原生模块测试的团队。
这种区分对本指南后续内容至关重要,因为“理想路径”只是冰山一角。一个稳健的图片选择器实现应当能在两种工作流中正常运行,能够处理平台特有的权限差异而不让用户感到困惑,并且能将可用的文件传递给你的 upload 层,而不仅仅停留在本地预览阶段。
安装与核心配置
安装只需一条命令。而能否正确配置原生环境,决定了该选择器能否在真机、自定义 dev client 以及你的生产构建版本中正常工作。
expo-image-picker 为 React 提供了一套面向 API 的 API,用于调用平台的照片、视频和相机拍摄功能。JavaScript 调用非常简单,但配置却并不简单,因为照片和相机的访问权限是由 iOS 和 Android 控制的,而非 React Native。

首先使用 Expo 的版本感知安装程序:
npx expo install expo-image-picker
使用 expo install 而不是 npm install 或 yarn add。Expo 会将包版本与你的 SDK 进行匹配,从而避免常见的原生兼容性问题。如果你正在评估 Expo 模块如何融入你的发布流程,那么这 Expo 工具概览 是一个有用的参考。
Managed workflow 配置
在 managed workflow 中,需在 app config 中声明该 plugin,以便 Expo 在 build 时应用 native 变更。
示例: app.json:
{
"expo": {
"plugins": ["expo-image-picker"]
}
}
这是最基本的配置。在实际开发中,团队通常还会添加权限说明文本,尤其是在 iOS 上,系统 prompt 需要解释应用为何需要访问权限。请确保文案与用户操作紧密相关。例如,“上传头像”比“需要媒体访问权限”更合适。
有一个操作细节非常耗费时间。修改 plugins、权限字符串或其他 native 配置都需要重新 build。仅重新加载 JavaScript 是无法应用这些变更的。在 Expo Go 中,你还会受到客户端内置内容的限制。而在 development build 或 production build 中,只有在完成新 build 后,native 项目才会反映你的配置。
Bare React Native 配置详情
在 bare app 中,API 包是一样的,但你需要自行验证更多 native 项目的内容。首先要检查的是 iOS 的 usage descriptions。如果你的流程涉及打开相册、启动相机或录制带音频的视频,你的应用需要在 Info.plist 中添加相应的权限字符串,然后重新 build。
针对 bare 项目的实用检查清单如下:
- 安装
expo-image-picker带npx expo install expo-image-picker. - 如果你的项目使用了 Expo config plugins,请添加插件配置。
- 确认 iOS 的 usage descriptions 与你提供的功能相匹配。
- 任何原生配置更改后,请重新构建 iOS 和 Android 应用。
权限文本缺失通常看起来像运行时 bug,因为 UI code 正常且按钮回调也会执行。失败点其实在更底层的堆栈中。我通常会检查 Info.plist应用配置,以及当前 build 是否包含了最新的原生更改,然后再去动 code 组件。
养成以下习惯可以让配置过程更可控:
- 为实际操作编写权限文本: 用户应该明白为什么会看到该 prompt。
- 分别配置相机和相册权限: 这样即使其中一个失败,另一个仍能正常工作。
- 原生更改后重新构建: hot reload 和 fast refresh 无法更新原生权限设置。
- 在真机上测试: 模拟器的行为可能会掩盖权限和摄像头相关的问题。
如果选择器在开发阶段正常,但在 TestFlight 或 Play Store 版本中失效,请首先将其视为配置问题。大多数情况下确实如此。
访问摄像头与媒体库
当用户点击“上传照片”并期望打开摄像头或相册时,你的 App 只有一项任务:打开正确的系统 UI,在用户拒绝或取消时确保界面不崩溃,并返回一个可用于预览或上传的本地文件引用。
这听起来很简单,直到你在 iOS 和 Android 上同时测试 managed 和 bare build。虽然 JavaScript API 保持精简,但运行时行为仍取决于 OS prompt、设备硬件以及你之前配置的原生权限。

一个极简且安全的组件
无论是在 Expo managed 还是 bare workflow 项目中,核心流程都是一致的。请求相关权限,启动选择器,检查用户是否取消,然后从以下路径读取第一个资源: result.assets.
基础组件示例如下:
import { useState } from 'react';
import { View, Button, Image, Alert } from 'react-native';
import * as ImagePicker from 'expo-image-picker';
export default function PhotoInput() {
const [imageUri, setImageUri] = useState<string | null>(null);
const pickFromLibrary = async () => {
const permission = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (!permission.granted) {
Alert.alert('Permission required', 'Please allow photo library access.');
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'],
allowsEditing: true,
quality: 1,
});
if (result.canceled) return;
const asset = result.assets?.[0];
if (!asset?.uri) return;
setImageUri(asset.uri);
};
const takePhoto = async () => {
const permission = await ImagePicker.requestCameraPermissionsAsync();
if (!permission.granted) {
Alert.alert('Permission required', 'Please allow camera access.');
return;
}
const result = await ImagePicker.launchCameraAsync({
allowsEditing: true,
quality: 1,
});
if (result.canceled) return;
const asset = result.assets?.[0];
if (!asset?.uri) return;
setImageUri(asset.uri);
};
return (
<View>
<Button title="Choose from library" onPress={pickFromLibrary} />
<Button title="Take photo" onPress={takePhoto} />
{imageUri ? (
<Image
source={{ uri: imageUri }}
style={{ width: 200, height: 200 }}
/>
) : null}
</View>
);
}
这里有三个细节至关重要。
- 分别请求相册和相机权限。它们是独立失败的。
- 将取消操作视为正常的交互行为,而非错误状态。
- 读取自
assets[0],因为选择器返回的是 asset 数组,而不是顶层的uri.
相册与相机流程
如果你想以最快速度实现功能,请从相册流程开始。它更易于测试,在更多模拟器环境下都能运行,且能避开相机硬件的边缘情况。待结果处理逻辑稳定后,再添加相机支持。
相机路径在开发过程中更容易出错。iOS Simulator 支持有限,Android 模拟器的相机行为也可能与真机不符。在 bare 项目中,这些差异可能会让你误以为是组件 code 的问题,而实际问题出在原生配置或测试环境上。
一种简洁的 UI 设计模式是在调用 API 选择器之前,先询问用户来源:
const showPickerOptions = () => {
Alert.alert('Upload image', 'Choose a source', [
{ text: 'Camera', onPress: takePhoto },
{ text: 'Photo Library', onPress: pickFromLibrary },
{ text: 'Cancel', style: 'cancel' },
]);
};
这种解耦让每个函数职责更明确,也便于后续添加 analytics、feature flags 或后端特定规则。例如,有些团队允许使用相册上传头像,但在身份验证时则要求必须使用相机实时拍摄。
如果你的应用还支持 Expo 之外的文件访问模式,或者你正在对比不同原生技术栈的惯例,那么这个 Capacitor 相册引用 是非常有用的参考上下文。
在向队友或 QA 展示此流程时,一段简短的 Demo 会很有帮助:
了解系统 UI 的预期行为
expo-image-picker 会打开平台选择器或相机 UI。你的 App 无法控制该流程中的每一个界面。这一点非常重要,因为“在我的设备上运行正常”往往意味着“操作系统允许了我测试的那条路径”。
在 iOS 上,用户可能会授予有限的库访问权限而非全部权限。在 Android 上,选择器的行为会因 OS 版本和厂商定制系统而异。在 managed workflow 项目中,Expo 会为你处理大部分原生配置。在 bare workflow 项目中,你需要确认构建后的 App 已包含你所做的原生权限更改。尽管两者的 JavaScript 调用点可能完全相同,但运行时的结果却可能不同。
在宣布该功能开发完成之前,我通常会测试以下场景:
- 首次权限请求
- 拒绝权限
- 用户取消操作
- 成功从库中选择图片
- 在真机上成功使用相机拍摄
- 立即预览返回的本地 URI
这些场景直接对应真实的生产环境行为。如果你需要将文件发送到服务器、审核 pipeline 或发布端点(例如 Instagram 媒体发布 API.
处理选择器结果与配置项
选择器的结果处理通常是生产环境逻辑的核心。系统 UI 返回的是一个结构化对象,而非仅仅是文件路径;这里的小错误会导致预览失效、上传为空或用户取消操作后应用崩溃。
正确读取结果对象
当前 Expo 应用中关键的结果结构为 result.assets[0].uri,而非顶层的 result.uri。这一细节会影响 managed 和 bare workflow 项目,因为尽管底层 native 配置不同,但 JavaScript API 是一致的。
使用“先守卫”模式:
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'],
allowsEditing: true,
quality: 1,
});
if (result.canceled) {
return;
}
const asset = result.assets?.[0];
if (!asset) {
return;
}
const { uri } = asset;
setImageUri(uri);
这处理了我最常遇到的两种失败情况。取消选择不会返回可读取的 asset,而假设 code result.assets[0] 始终存在会导致运行时错误。
获取 URI 后,渲染预览非常简单:
<Image source={{ uri: imageUri }} style={{ width: 240, height: 240 }} />
如果你计划稍后进行 upload,请保留整个 asset 不仅仅是 URI,而是一个完整的对象。在实际应用中, fileName, mimeType, width, height,以及 fileSize 通常用于校验、记录日志或构建更简洁的 multipart 请求。
会影响后续行为的选项
部分选择器选项的影响不仅限于选择界面。它们还会决定文件大小、编辑行为以及后端需要接收的数据格式。
| 选项 | 类型 | 变更内容 | 典型用法 |
|---|---|---|---|
mediaTypes |
数组 | 限制用户的选择范围 | 如果你的 API 仅支持图片,请将选择范围限制为图片 |
allowsEditing |
布尔值 | 在支持的系统上调用 OS 原生的裁剪或编辑界面 | 头像、方形封面、收据拍摄 |
quality |
number | 压缩支持的图像输出格式 | 减小移动网络下的 upload 体积 |
base64 |
布尔值 | 在结果中添加编码后的图像数据 | 仅适用于明确需要内联图像数据的集成场景 |
一些容易被忽视的权衡点:
allowsEditing当图像槽位有固定形状或大小时非常有用。如果你的服务器已有自己的裁剪 pipeline 且你需要原始文件,则用处较小。quality会影响 upload 时间、内存压力以及服务器存储。quality: 1并不一定是最佳选择。mediaTypes应与后端规则保持一致。如果服务器拒绝视频,请不要让选择器返回它们。base64会增加内存中的有效载荷大小。除非接收服务有此要求,否则请避免使用。
最后一点在低内存设备上尤为重要。使用本地文件 URI 通常是预览和 multipart upload 更佳的传递方式。Base64 虽然有其应用场景,但与传递文件引用相比,其开销过大。
URI 与 base64 的对比
对于大多数应用,规则很简单:
- 使用 URI 用于预览。
- 使用 URI 用于文件 upload。
- 使用 base64 仅在接收系统明确要求编码内容时使用。
这种模式能让 picker code 保持精简且更易于测试。它也符合许多后端媒体流的设计逻辑,包括最终发布到外部平台的各种服务,例如 Instagram 媒体发布 API.
如果你的团队频繁发布 OTA 更新,或者在应用交付过程中处理大量图片资源,那么这里的文件大小决策将影响整个 pipeline。这篇关于 优化应用更新中的图片 的指南可以作为 picker 配置的有力补充。
适用于真实应用的更安全结果模式
对于演示 code,仅存储 imageUri 即可。但在生产环境中,请存储一个规范化的对象,这样在进行预览、校验、upload 或重试等后续步骤时,就不需要每次都重新解析原始的 picker 响应。
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'],
allowsEditing: true,
quality: 0.8,
});
if (result.canceled || !result.assets?.length) {
return;
}
const asset = result.assets[0];
setSelectedImage({
uri: asset.uri,
fileName: asset.fileName ?? 'upload.jpg',
mimeType: asset.mimeType ?? 'image/jpeg',
width: asset.width,
height: asset.height,
fileSize: asset.fileSize ?? null,
});
这能让你在应用内部获得一个可预测的数据结构。同时,由于应用 code 在你处理其他原生差异时保持稳定,这也能让 managed 和 bare 项目更容易保持同步。
最后进行一次检查。为了保险起见,不要启用额外的结果字段。只请求你确定需要的数据,让选择器专注于选择功能,而不是将其变成一个通用的文件处理步骤。
高级模式与平台差异
一旦第一张选中的图片需要应对重试、鉴权 Header、原生权限差异以及真实的 upload 接口时,选择器功能通常就不再那么简单了。 expo-image-picker 能很好地处理选择逻辑。其余的功能实现则取决于你的 App。

一种实用的 upload 模式
对于需要文件 upload 的 API, FormData 仍然是最安全的默认方案。它适用于常见的 Rails、Node、Laravel、Django 和 Go 后端,并能让选择器与传输逻辑解耦。
async function uploadImage(imageUri: string) {
const formData = new FormData();
formData.append('file', {
uri: imageUri,
name: 'upload.jpg',
type: 'image/jpeg',
} as any);
const response = await fetch('https://your-api.example.com/uploads', {
method: 'POST',
body: formData,
headers: {
Accept: 'application/json',
},
});
if (!response.ok) {
throw new Error('Upload failed');
}
return response.json();
}
code 足以证明路径是通的,但生产环境的应用通常需要多加一层。建议 name 和 type 尽可能从选中的资源中提取数据,在选择器函数之外附加鉴权信息,并将 upload 状态与选择器状态分离,这样请求失败时就不会迫使用户重新打开媒体库。
以下几项检查可以避免我在 Code Review 中常见的错误:
- 确认本地文件
uri在构建请求前是否存在 - 在 upload 前渲染预览,以便用户及早发现选错文件
- 在请求进行中防止重复点击
- 将网络故障与选择器取消或权限错误分开处理
- 预期后端会拒绝大文件、不支持的 MIME 类型或缺失认证
如果你的后端需要 base64 而不是 multipart,这通常是服务器限制,而非选择器的要求。Multipart 对内存更友好,在移动端也更容易处理。
平台差异真正产生影响的地方
选择器 UI 是原生的,因此继承了原生行为。这既影响用户体验,也影响你的 code 应有的假设。
在 iOS 上,编辑流程和权限 prompt 遵循 Apple 的规范。受限的照片访问权限返回的资源集可能比你在完全授权设备上看到的测试账号更少。在 Android 上,选择器行为随 OS 版本和厂商定制系统(skin)的变化更大,尤其是在相册、文件名以及相机拍摄返回方式方面。Bare React Native 应用由于涉及更多原生配置,会更直接地感受到这些差异;但 managed Expo 应用仍需要 code 将选择器视为具有平台特性而非完全统一的组件。
实际的操作原则很简单:依赖你可以验证的字段,而不是依赖跨设备完全一致的 UI 或元数据。
在实际应用中,以下几个例子非常重要:
- 编辑与裁剪: iOS 和 Android 的 UI 及裁剪行为并不完全一致
- 返回的元数据:
fileName,mimeType,以及fileSize可能会缺失或不一致,请务必添加 fallback 机制 - 权限控制: iOS 的照片访问权限可以限制为仅选中的项目,而 Android 的行为则更多取决于 OS 版本和系统选择器的支持情况
- 相机输出: 拍摄的照片在命名、方向或压缩特性上,可能与相册中的资源有所不同
如果你的团队也在 Expo 之外进行开发,那么 DesignStack 应用开发指南 能为处理跨库媒体逻辑时的 Android 相关决策提供有用的参考。
Managed 与 Bare 工作流的区别
此时,配置选择将开始影响运维效率。
在 Managed 工作流中,权限字符串和插件配置通常位于 app config 中,原生层面的变更会在创建新 build 时生效。这能保持 JavaScript 层面的简洁,但也意味着配置修复在下次 Native Build 之前不会生效。OTA 更新无法修复缺失的原生权限。
在 Bare 工作流中,实现相同功能涉及更多环节。你需要自行验证 iOS 的 usage descriptions、Android manifest 行为、包安装情况以及 rebuild 时机。优势在于掌控力,代价则是 Picker 的问题可能源于原生配置,而非 JavaScript 调用处。
在 Expo 和 Capacitor 之间切换的团队,往往低估了这些抽象层之间的差异。Capgo 对以下内容有很好的解释: Capacitor 如何处理平台差异,如果你正在权衡团队想要承担多少原生配置工作,这是一个很好的参考点。
我对这两种工作流的偏好是一致的:保持 Picker 的 code 职责单一,对结果进行一次性归一化处理,通过专门的 API 层进行 upload,并将平台差异视为需要显式配置和测试的内容,而不是靠假设去掩盖。
常见问题排查
大多数 Expo Image Picker 的 bug 都集中在少数几类。最快的修复方法通常是确定是哪一层出了问题:配置、权限、结果处理还是渲染。

常见故障快速检查
如果选择器无法打开或权限请求失败,请先检查原生配置。在 Bare 项目中,缺少 iOS usage descriptions 是最常见的原因。
如果用户关闭选择器后应用崩溃,请检查结果处理逻辑。许多实现仍假设返回的是直接 URI,而忽略了 canceled 校验步骤。
以下是一些快速对照指南:
- 权限被拒绝 (Permission denied) 错误: 验证 App 配置和原生权限字符串,然后重新构建。
undefined图片 URI: 从以下路径读取result.assets?.[0]?.uri,而不是result.uri.- 取消后没有任何反应: 这可能是对的。将取消操作视为无操作状态。
- 图片无法渲染: 确认 URI 已存储在 state 中并传递给
<Image source={{ uri }} />. - 模拟器中的 Camera 表现异常: 在排查库的 bug 之前,先在真机上进行测试。
生产环境简易检查清单
在发布前进行最后一次检查:
- 使用 Expo 工具安装: 使用
npx expo install expo-image-picker. - 配置原生部分: 添加 plugin 及必要的权限描述。
- 有意识地请求权限: 将相机与媒体库流程分开。
- 对每个结果进行校验: 检查
result.canceled并安全地读取assets[0]. - 优先使用基于 URI 的上传: 仅在特殊情况下保留 base64。
- 在真机上进行测试: 特别是相机拍摄和权限提示功能。
如果你的团队在 React Native 项目中同时发布 Capacitor 或 Electron 应用, Capgo 这是一种无需等待应用商店审核即可交付 JavaScript、CSS、配置和资源更新的方案。当涉及图片相关的修复(如上传 UI、校验规则、文案或选择器流程中的资源处理)位于 Web 层时,该方案非常适用。