Expo 小程序媒体库功能设计与实现记录
这篇文章记录一个基于 Expo、React Native 和 Expo Router 的媒体库演示功能。当前阶段重点不是完整后端上传链路,而是先把前端交互、媒体选择、录音、预览、缓存、列表展示和删除流程跑通,为后续接入后端和数据库做功能验证。
项目背景
项目基于 Expo 搭建,使用 Expo Router 管理页面路由。当前应用主要有两个页面:
Home:选择图片、视频、音频,或录制音频,并预览最后一个选择的媒体。About:展示已保存的媒体列表,支持轮播预览、缩略图切换和删除。
这个功能更接近一个"媒体素材管理"的雏形。用户可以先把本地图片、视频、音频放进应用,再在媒体库里查看和管理。
当前核心功能
1. 图片和视频多选
首页通过 expo-image-picker 打开系统媒体选择器:
- 支持图片
- 支持视频
- 支持多选
- 选择完成后,首页预览最后一个选中的文件
- 点击保存后,把本次选择的所有媒体写入本地缓存
这里的重点是"选择"和"保存"分离。用户选择媒体后,可以先在首页确认预览,再决定是否保存到媒体库。
2. 音频文件选择
演示阶段 Web 端使用原生 input[type=file] 选择音频文件:
- 支持
audio/* - 支持多个音频文件
- 选择完成后,最后一个音频会显示在首页预览区
- 保存逻辑和图片、视频保持一致
当前项目还没有安装原生端音频文件选择依赖,所以 iOS / Android 端会先提示需要补充 expo-document-picker。这样做是为了避免演示阶段引入太多依赖,先把主流程跑通。
3. Web 端音频录制
录音功能在 Web 端使用浏览器的 MediaRecorder 和 navigator.mediaDevices.getUserMedia:
- 点击
Record audio请求麦克风权限 - 开始录音后按钮变为
Stop recording - 停止录音后生成音频文件
- 录音结果自动进入待保存媒体列表
- 首页预览区显示录音音频
原生 iOS / Android 端录音后续可以接入 expo-audio,当前演示阶段先保留提示。
4. 本地缓存
当前媒体数据统一保存到 LocalStorage。保存的不是完整后端数据,也不是数据库记录,而是一个前端演示用的媒体元数据数组。
核心数据结构如下:
ts
export type StoredMedia = {
id: string;
uri: string;
type: "image" | "video" | "audio";
fileName?: string | null;
width?: number;
height?: number;
duration?: number | null;
createdAt: string;
};
这样设计的好处是后续替换成后端接口比较自然。以后后端返回的媒体记录也大概率会包含:
- 媒体 ID
- 文件 URL
- 媒体类型
- 文件名
- 宽高、时长等元信息
- 创建时间
5. 媒体库轮播展示
About 页面读取缓存里的媒体列表,并提供一个简单的媒体库视图:
- 顶部显示媒体数量和当前位置
- 中间展示当前媒体
- 左右按钮切换上一条 / 下一条
- 底部缩略图列表快速切换
- 删除按钮移除当前媒体
图片直接使用 expo-image 展示。Web 端视频使用 <video controls>,音频使用 <audio controls>。原生端当前对视频和音频先展示类型占位,后续可以接入专门播放器。
核心模块划分
app/(tabs)/index.tsx
首页负责媒体入口和临时预览。
主要职责:
- 打开图片 / 视频选择器
- 打开音频文件选择器
- 触发 Web 端录音
- 维护本次待保存媒体数组
- 预览最后一个待保存媒体
- 点击保存后写入缓存
这个页面不直接关心媒体库如何展示,只负责生成 StoredMedia[] 并保存。
app/(tabs)/about.tsx
About 页面负责媒体库展示。
主要职责:
- 从缓存读取媒体列表
- 展示当前媒体
- 处理上一条 / 下一条切换
- 展示缩略图
- 删除当前媒体
这个页面不负责选择文件,也不负责录音,只消费已经保存好的媒体数据。
components/ImageViewer.tsx
这是首页预览组件。
它根据媒体类型做不同展示:
image:展示图片video:Web 端使用视频播放器,原生端显示视频占位audio:Web 端使用音频播放器,原生端显示音频占位
这样首页不用写太多媒体类型判断,页面逻辑更清晰。
lib/mediaStore.ts
这是当前演示阶段最关键的一层。
主要职责:
- 定义统一媒体类型
StoredMedia - 读取本地缓存
- 写入本地缓存
- 批量新增媒体
- 删除媒体
后续接入后端时,最适合优先替换的就是这一层。比如把 getStoredMedia() 改成请求列表接口,把 addStoredMediaItems() 改成上传文件并保存数据库记录。
数据流
当前完整流程如下:
- 用户在首页选择图片 / 视频 / 音频,或录制音频。
- 前端把文件转换成统一的
StoredMedia结构。 - 首页展示本次选择列表中的最后一个媒体。
- 用户点击保存。
- 应用把本次媒体数组写入
LocalStorage。 - 用户进入 About 页面。
- About 页面读取缓存并展示轮播列表。
- 用户可以切换媒体或删除媒体。
当前阶段的取舍
为什么先用 LocalStorage
因为现在目标是演示功能,而不是正式文件存储。LocalStorage 的优点是:
- 实现快
- 不依赖后端
- 方便验证交互流程
- 数据结构后续容易迁移
但它也有明显限制:
- 容量有限
- 不适合保存大视频
- 原生端不能完全等价使用
- 浏览器清缓存后数据会丢
- 不适合作为正式文件存储方案
所以当前的缓存更准确地说是"演示缓存",不是生产级存储。
为什么音频原生端暂时只提示
图片和视频可以直接用 expo-image-picker。但音频文件选择和录音在原生端需要额外能力:
- 选择音频文件通常需要
expo-document-picker - 录音需要
expo-audio或类似音频模块 - 长期保存录音文件还需要
expo-file-system
这些能力后续可以加,但演示阶段先用 Web 标准 API 把交互闭环跑通,避免过早扩大实现范围。
为什么保存的是 uri 和元数据
当前保存的是媒体引用和元数据,不是上传后的永久 URL。正式接后端后,uri 会逐步变成服务器返回的资源地址,例如:
ts
{
id: "media_001",
uri: "https://cdn.example.com/media/xxx.mp4",
type: "video",
fileName: "demo.mp4",
duration: 120000,
createdAt: "2026-05-06T10:00:00.000Z"
}
这样前端展示层几乎不用大改。
后续接入后端的演进方向
后续如果要做成正式功能,可以按下面顺序升级。
1. 文件上传接口
新增上传接口:
txt
POST /api/media/upload
前端提交文件,后端返回媒体记录:
json
{
"id": "media_001",
"url": "https://cdn.example.com/media/demo.mp4",
"type": "video",
"fileName": "demo.mp4",
"duration": 120000,
"createdAt": "2026-05-06T10:00:00.000Z"
}
2. 媒体列表接口
txt
GET /api/media
About 页面从接口读取列表,不再从 LocalStorage 读取。
3. 删除接口
txt
DELETE /api/media/:id
删除时既删除数据库记录,也根据业务需要清理对象存储中的文件。
4. 对象存储
图片、视频、音频文件不建议直接存进数据库。更常见的方案是:
- 文件放对象存储
- 数据库保存文件 URL 和元数据
- 前端通过 URL 播放或展示
5. 原生端能力补全
如果要支持 iOS / Android:
- 用
expo-document-picker选择音频文件 - 用
expo-audio实现录音 - 用
expo-file-system做临时文件复制和缓存管理 - 用视频 / 音频播放器组件补齐原生播放体验
注意点
1. 大文件不要长期放 LocalStorage
视频和音频可能很大,正式项目不能依赖 LocalStorage 保存大文件。演示阶段可以接受,但生产环境一定要上传到后端或对象存储。
2. Web 和原生平台能力不完全一致
Web 可以直接使用 <audio>、<video> 和 MediaRecorder。React Native 原生端没有这些 DOM API,需要 Expo 或原生模块提供对应能力。
3. 预览和保存要分离
当前设计里,选择媒体后只是放到待保存状态,用户点击 Save 才会进入媒体库。这样可以避免用户误选文件后立即污染列表。
4. 媒体类型要统一抽象
图片、视频、音频虽然展示方式不同,但都可以抽象成同一种业务数据:
ts
type MediaType = "image" | "video" | "audio";
统一抽象后,列表、删除、缓存、后端接口都能复用同一套逻辑。
5. 后续需要考虑权限
正式做原生端时,需要处理:
- 相册权限
- 麦克风权限
- 文件访问权限
- 用户拒绝权限后的提示
权限处理不能只靠默认报错,否则用户体验会很差。
小结
这个媒体库功能目前完成的是前端演示闭环:
- 多选图片 / 视频
- 选择音频文件
- Web 端录音
- 统一媒体数据结构
- 本地缓存
- 轮播展示
- 删除管理
它不是最终的生产架构,但已经把核心交互和数据模型跑通了。后续接入后端时,可以优先替换 mediaStore.ts,再逐步补齐上传接口、数据库、对象存储和原生端音视频能力。