你可能正处于 UI 已就绪、个人资料页也加上了“上传照片”按钮的阶段,这时原本简单的部分却突然不简单了。实际的图片选择流程涉及原生权限、由系统控制的界面、许多开发者意料之外的返回结构,以及一些只有在发布真实 build 后才会显现的构建期细节。
Expo Image Picker 正是为此而生。它是 Expo 的官方库,用于打开系统界面,从设备图库中选择图片或视频,或使用相机拍照,正如 Expo 包仓库。在实际应用中,这意味着你获得的是接入原生媒体输入的可靠桥梁,而不是在所有设备上表现完全一致的自定义媒体体验。
本指南面向首次实际集成,而非演示。它聚焦于生产环境中的关键决策:managed 与 bare 工作流的配置、不会让你日后意外的权限处理、安全的结果解析,以及用户选择文件后的实用上传模式。如果你在使用自定义原生环境,了解它与 Capgo 在其替代方案指南中对比了 Expo EAS update 选项.
目录
Expo Image Picker 入门指南
产品经理要求增加头像功能。一周后,同样的功能又需要支持收据上传、事故报告拍照,以及用户首次拒绝权限后的重试机制。图像输入需求增长极快,因为它涉及原生权限、系统自带的 UI、临时文件处理以及后端上传流程。
expo-image-picker Expo SDK 模块正是为此而生。它会打开平台选择器或相机 UI,并以你的 React Native code 能处理的格式返回所选媒体。JavaScript API 很小。主要挑战在于在 Managed 和 Bare 项目中正确处理原生配置、权限流程和结果处理。
核心权衡很直接:你让 iOS 和 Android 展示各自的媒体 UI,而不是构建自定义选择器。这通常效果更好:用户已经熟悉系统界面,权限提示的行为符合 OS 的预期,同时你的团队也无需在 JavaScript 中维护相册实现。
将其视为一个带有 React 接口的原生集成功能。
这种思维方式很有帮助,因为故障很少出在调用选择器的按钮上。它们通常来自以下三个地方之一:
- 原生配置: 插件配置缺失、权限字符串不正确,或更改配置后未重新构建
- 运行时行为: 用户可以拒绝访问、在 iOS 上授予有限的媒体库访问权限,或在不选择任何内容的情况下取消流程
- 结果解析: 当前的 API 返回一个
assets数组,因此直接读取result.uri的旧示例会失败
工作流选择也会改变配置路径。在托管 Expo 应用中,大部分原生工作位于应用配置中,配置更改时需要重新构建。在裸应用(bare app)中,你仍然可以使用 Expo 模块 API,但需要更直接地验证底层 iOS 和 Android 项目设置。如果你的团队使用的是自定义客户端而不是 Expo Go,本指南与 Capgo 的 Expo EAS 替代方案 非常搭配,适用于需要在 Expo Go 之外测试原生模块的团队。
这种区分对指南的其余部分至关重要,因为正常路径只是故事的一半。当选择器实现在两种工作流中都能正常工作、处理平台特定的权限怪癖而不让用户感到意外,并将可用的文件传递给上传层而不是停留在本地预览时,该实现才是稳固的。
安装与核心配置
安装只需一条命令。原生配置是否正确,才决定选择器能否在真机、自定义 dev client 和生产 build 中正常工作。
expo-image-picker 为 React 提供一层 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 时应用原生变更。
示例: app.json:
{
"expo": {
"plugins": ["expo-image-picker"]
}
}
这是最基本的配置。实际使用中,团队通常还会添加权限说明文本,尤其是在 iOS 上,系统 prompt 应说明应用为何需要访问权限。措辞要贴合用户操作。“上传头像”比“需要媒体访问权限”更合适。
有一个操作细节会浪费大量时间。修改 plugins、权限字符串或其他原生配置都需要重新 build。仅重新加载 JavaScript 无法应用这些变更。在 Expo Go 中,你还会受到客户端已内置内容的限制。而在 development build 或 production build 中,原生项目只有在重新 build 后才会反映你的配置。
Bare React Native 配置详情
在 bare app 中,包 API 是一样的,但你需要自己检查更多原生项目内容。首先要检查 iOS 的 usage descriptions。如果你的流程会打开相册、启动相机或录制带音频的视频,你的应用需要在 Info.plist 中添加相应的权限字符串,然后重新 build。
针对 bare 项目的实用检查清单如下:
- 安装
expo-image-picker配合npx expo install expo-image-picker. - 如果你的项目使用了 Expo config plugins,请添加插件配置。
- 确认 iOS 的 usage descriptions 与你实际提供的功能一致。
- 任何 native 配置更改后,都要重新构建 iOS 和 Android 应用。
权限文本缺失常常看起来像运行时 bug,因为 UI code 没问题,按钮处理函数也照常执行。真正出问题的地方在更底层。我通常会先检查 Info.plistapp config,以及当前 build 是否包含最新的 native 改动,然后再去动 code 组件。
几个习惯能让配置过程更可控:
- 为实际操作编写权限文本: 让用户明白为什么会看到该 prompt。
- 分别配置相机和相册权限: 这样即使其中一个失败,另一个仍能正常工作。
- 在 native 更改后重新构建: 热重载和快速刷新不会更新原生权限。
- 在真机上测试: 模拟器的行为可能会掩盖权限和相机问题。
如果选择器在开发阶段正常,但在 TestFlight 或 Play Store 构建中失效,请首先将其视为配置问题。大多数情况下确实如此。
访问摄像头与媒体库
当用户点击“上传照片”时,他们期望打开相机或图库,而你的应用此刻只有一个任务:打开正确的系统 UI,妥善处理拒绝或取消操作而不破坏界面,并返回一个可用于预览或上传的本地文件引用。
听起来很简单,直到你在 iOS 和 Android 上测试 managed 和 bare 构建。JavaScript API 保持紧凑,但运行时行为仍取决于系统提示、设备硬件以及之前配置的原生权限。

一个极简且安全的组件
核心流程在 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],因为选择器返回的是资源数组,而不是顶层uri.
相册与相机流程
如果你希望以最快的方式实现可用功能,建议从库流程入手。它更易于测试,兼容更多模拟器环境,且能规避相机硬件的边界情况。待结果处理路径稳定后,再添加相机支持。
相机路径在开发阶段更容易出错。iOS 模拟器支持有限,Android 模拟器可能无法呈现与真机一致的相机行为。在裸项目中,这些差异可能让你去检查组件 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' },
]);
};
这种分离让每个函数职责更聚焦。也便于后续添加分析、功能开关或后端特定规则。例如,有些团队允许从相册上传头像,但要求身份验证必须使用实时拍摄的照片。
如果你的整个应用还支持 Expo 之外的文件访问模式,或者你正在对比不同原生技术栈的约定,那么 Capacitor 照片库参考 会提供有用的背景信息。
向团队成员或 QA 展示这个流程时,一个简短的 demo 会很有帮助:
系统 UI 交互预期
expo-image-picker 打开平台选择器或相机 UI。你的应用无法控制该流程中的每个界面。这一区别很重要,因为“在我设备上能用”往往意味着“操作系统允许了我测试的路径”。
在 iOS 上,用户可能授予有限的相册访问权限,而不是完全访问权限。在 Android 上,选择器行为可能因系统版本和厂商定制而异。在托管工作流项目中,Expo 会替你处理更多原生配置。在裸工作流项目中,你需要确认构建出的应用包含你做的原生权限更改。两种情况下的 JavaScript 调用点可以完全相同,但运行时结果不同。
我通常会在认为功能完成前测试这些情况:
- 首次权限请求
- 权限被拒绝
- 用户取消
- 成功选择相册
- 在真机上成功拍摄
- 立即预览返回的本地 URI
这些情况直接对应真实的生产行为,也为下一步做好了清晰铺垫:如果你需要把文件发送到服务器、审核 pipeline 或发布端点,比如 Instagram 媒体发布 API.
处理选择器结果与选项
选择器返回的结果通常是需要编写实际生产逻辑的部分。系统 UI 返回的是一个结构化对象,而不仅仅是文件路径;这里的小错误会导致预览失效、上传内容为空,或用户取消后应用崩溃。
正确读取结果对象
当前 Expo 应用中关键的结果结构是 result.assets[0].uri,而不是顶层的 result.uri。这个细节对 managed 和 bare workflow 项目都有影响,因为底层原生配置虽然不同,但 JavaScript API 是相同的。
使用 guard-first 模式:
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 请求。
改变下游行为的选项
部分 picker 选项不仅影响选择界面,还会决定文件大小、编辑行为以及后端需要接受的内容。
| 选项 | 类型 | 影响范围 | 典型用途 |
|---|---|---|---|
mediaTypes |
数组 | 限制用户可选择的类型 | 如果 API 仅接受图片,请将选择范围限定为图片 |
allowsEditing |
布尔值 | 在系统支持时,允许操作系统提供裁剪或编辑界面 | 头像、方形封面、收据拍摄 |
quality |
数字 | 压缩支持的图像输出 | 减小移动网络下的上传体积 |
base64 |
布尔值 | 将编码后的图像数据添加到结果中 | 仅适用于明确要求内联图像数据的集成场景 |
有几个容易忽视的权衡点:
allowsEditing当图像插槽具有固定形状或尺寸时很有用。如果服务器有自己的裁剪 pipeline 且需要原始文件,则用处不大。quality影响上传时间、内存压力和服务器存储。quality: 1并不一定是最佳选择。mediaTypes应与后端规则保持一致。如果服务器拒绝视频,就不要让选择器返回它们。base64会增加内存中的 payload 大小。除非接收服务需要,否则请避免使用。
最后一点在低内存设备上尤其重要。本地文件 URI 通常是预览和 multipart upload 更合适的传递方式。Base64 有它的用武之地,但相比传递文件引用,开销更大。
URI 与 base64 对比
对于大多数应用,规则很简单:
- 使用 URI 用于预览。
- 使用 URI 用于文件上传。
- 使用 base64 仅在接收系统明确要求编码内容时使用。
这种模式能让 picker code 保持小巧且更易测试。它也与许多后端媒体流程的构建方式一致,包括最终会发布到外部平台的服务,例如 Instagram 媒体发布 API.
如果你的团队频繁发布 OTA 更新,或在应用交付过程中传输大量图片资源,那么这里的文件大小决策会影响到 pipeline 的后续环节。这篇关于 为应用更新优化图片 的指南可以作为 picker 配置的有力补充。
适用于真实应用的更安全结果模式
对于演示用的 code,仅存储 imageUri 也是可以的。但在生产环境中,请存储一个规范化对象,这样后续的预览、校验、上传或重试等步骤就不需要每次都重新解析 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,
});
这让你在应用内获得一个可预测的结构。它也让 managed 和 bare 项目更容易保持一致,因为当你在别处处理原生差异时,应用 code 仍保持稳定。
最后再检查一次会很有帮助。不要为了保险起见就启用额外的结果字段。只请求你确定需要的数据,让选择器专注于选择,而不是把它变成通用的文件处理步骤。
高级模式与平台差异
一旦第一张选中的图片需要应对重试、鉴权请求头、原生权限差异以及真实的上传接口时,选择器功能通常就不再简单了。 expo-image-picker 能很好地处理选择。其余功能就靠你的 App 了。

一种实用的上传模式
对于需要文件上传的 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 选中的资源中尽可能推导,在选择器函数之外附加鉴权信息,并将上传状态与选择器状态分离,这样请求失败时就不会迫使用户重新打开图库。
以下几项检查可以避免我在 Code Review 中常见的错误:
- 确认本地
uri在构建请求前是否存在 - 上传前渲染预览,让用户尽早发现选错文件
- 请求进行中时,防止重复点击
- 将网络故障与选择器取消或权限错误分开处理
- 预期后端验证会拒绝大文件、不支持的 MIME 类型或缺失的认证
如果你的后端要求 base64 而非 multipart,这通常是服务器限制,而非选择器的要求。Multipart 在内存上更节省,且在移动端更容易理解。
平台差异真正重要的地方
选择器 UI 是原生的,因此会继承原生行为。这既影响用户看到的内容,也影响你的 code 应该假设什么。
在 iOS 上,编辑流程和权限提示遵循 Apple 的规范。受限照片访问可能返回的资产比你的测试账号在完全授权设备上看到的更少。在 Android 上,选择器行为更受系统版本和厂商定制影响,尤其是在相册、文件名以及相机拍摄结果的返回方式上。Bare React Native 应用会更直接地感受到这些差异,因为你要负责更多原生配置;但托管 Expo 应用仍然需要 code,把选择器看作因平台而异,而不是完全统一。
实际规则很简单:依赖你能验证的字段,而不是跨设备完全一致的 UI 或元数据。
实际应用中有几个例子很关键:
- 编辑和裁剪: iOS 和 Android 的 UI 与裁剪行为并不完全相同
- 返回的元数据:
fileName,mimeType,以及fileSize可能缺失或不一致,因此要加兜底逻辑 - 权限: iOS 照片访问可限制为仅选中的项目,而 Android 行为更取决于系统版本和系统选择器支持
- 相机输出: 拍摄的图片在命名、方向或压缩特性上可能与图库资源不同
如果你的团队也在 Expo 之外工作, DesignStack 应用开发指南 提供了处理媒体决策时有用的 Android 背景信息,这些信息在单一库之外同样适用。
Managed 与 Bare 工作流的区别
到这一步,配置选择在运维层面就开始变得重要了。
在 managed workflow 中,权限字符串和插件配置通常放在 app config 里,原生变更会在创建新 build 时应用。这样能保持 JavaScript 层干净,但也意味着配置修复要等到下一次 Native Build 才可见。OTA 更新无法修补缺失的原生权限。
在 bare workflow 中,同一个功能有更多环节要处理。你需要自己验证 iOS 原生 usage descriptions、Android manifest 行为、包安装和 rebuild 时机。好处是可控,代价是 picker 问题可能来自原生配置,而不是 JavaScript 调用点。
在 Expo 和 Capacitor 之间切换的团队,往往会低估这些抽象层之间的差异有多大。Capgo 对以下内容有很实用的解释: Capacitor 如何处理平台差异如果你正在决定团队要自己承担多少原生配置,这是一个很好的对比参考。
我对这两种 workflow 的偏好是一致的:保持 picker 的 code 范围尽量窄,对结果做一次归一化,通过专门的 API 层上传,并把平台特定行为当作需要显式配置和测试的内容,而不是靠假设糊弄过去。
常见问题排查
大多数 Expo Image Picker 的 bug 都集中在少数几类。最快的修复方法通常是确定是哪一层出了问题:配置、权限、结果处理还是渲染。

常见故障快速检查
如果选择器打不开或权限申请失败,先检查原生配置。尤其是在 bare 应用中,缺少 iOS usage descriptions 是常见根因。
如果用户关闭选择器后应用崩溃,请检查结果处理逻辑。许多实现仍假设返回的是直接 URI,而跳过了 canceled 检查。
快速对照如下:
- 权限被拒绝错误: 验证 App 配置和原生权限字符串,然后重新构建。
undefined图片 URI: 从以下位置读取result.assets?.[0]?.uri,而不是result.uri.- 取消后无响应: 这可能是对的。把取消当作 no-op 状态处理。
- 图片无法渲染: 确认 URI 已存入 state 并传入
<Image source={{ uri }} />. - Camera 在模拟器中表现异常: 先上真机测试,再去排查库的 bug。
简短的生产环境检查清单
发布前用它做最后一遍检查:
- 用 Expo 工具安装: 使用
npx expo install expo-image-picker. - 配置原生部分: 添加插件及必要的权限描述。
- 按需请求权限: 分离相机和媒体库流程。
- 校验每个结果: 查看
result.canceled并安全读取assets[0]. - 优先使用基于 URI 的上传: 仅在特殊情况下保留 base64。
- 在真机上测试: 尤其是相机拍摄和权限提示。
如果你的团队在 React Native 项目之外还发布 Capacitor 或 Electron 应用, Capgo 是一个可选方案,用于交付 JavaScript、CSS、配置和资产更新,而无需为每次更改等待商店审核。当图像相关的修复位于 Web 层时(例如上传 UI、验证规则、文案或选择器流程周围的资产处理),此方案尤为相关。