
本文详解在裸 React Native 项目中结合 react-native-fs 与 expo-av 实现视频本地缓存与播放的完整方案,重点解决路径格式错误、URI 协议兼容性及缓存策略优化问题,并提供可直接运行的代码示例。
本文详解在裸 react native 项目中结合 `react-native-fs` 与 `expo-av` 实现视频本地缓存与播放的完整方案,重点解决路径格式错误、uri 协议兼容性及缓存策略优化问题,并提供可直接运行的代码示例。
在 React Native 中缓存视频并非简单“下载 + 播放”,尤其当使用 expo-av(而非 react-native-video)时,文件路径协议、权限配置和 URI 格式要求更为严格。你遇到的“路径不工作”问题,根本原因在于:expo-av 要求传入 source.uri 的必须是 合法的 file:// 协议 URI,而你当前拼接的 ${cachePath}/${filename} 仅是原生文件系统路径(如 /Users/.../video.mp4),缺少 file:// 前缀,且未做平台适配处理。
✅ 正确做法是:使用 react-native-fs 下载视频后,通过 RNFS.fileURL()(iOS)或 RNFS.pathToUrl()(Android)生成标准 file URI,再传给 expo-av/Video 组件。
以下是经过验证的完整实现:
import React, { useState, useEffect } from 'react';
import { View } from 'react-native';
import * as RNFS from 'react-native-fs';
import { Video } from 'expo-av';
const App = ({ videoUrl }: { videoUrl: string }) => {
const [cachedUri, setCachedUri] = useState<string | null>(null);
useEffect(() => {
downloadAndCacheVideo();
}, []);
const downloadAndCacheVideo = async () => {
try {
const filename = videoUrl.substring(videoUrl.lastIndexOf('/') + 1);
const cachePath = RNFS.DocumentDirectoryPath;
const localPath = `${cachePath}/${filename}`;
// ✅ 检查文件是否存在(推荐用 stat 替代 exists,更可靠)
const stat = await RNFS.stat(localPath).catch(() => null);
if (stat && stat.isFile()) {
console.log('✅ Video already cached:', localPath);
// ✅ 生成跨平台 file:// URI
const fileUri = Platform.OS === 'ios'
? `file://${localPath}`
: RNFS.pathToUrl(localPath); // Android requires pathToUrl
setCachedUri(fileUri);
return;
}
// ? 下载视频(注意:fetch + blob 在某些 RN 版本存在兼容性问题,推荐用 RNFS.downloadFile)
console.log('⬇️ Downloading video...');
const downloadRes = await RNFS.downloadFile({
fromUrl: videoUrl,
toFile: localPath,
progress: (data) => {
console.log(`Download progress: ${Math.round(data.bytesWritten / data.contentLength * 100)}%`);
}
}).promise;
if (downloadRes.statusCode === 200) {
const fileUri = Platform.OS === 'ios'
? `file://${localPath}`
: RNFS.pathToUrl(localPath);
setCachedUri(fileUri);
console.log('✅ Cached video URI:', fileUri);
} else {
throw new Error(`Download failed: ${downloadRes.statusCode}`);
}
} catch (err) {
console.error('❌ Cache failed:', err);
}
};
return (
<View style={{ flex: 1 }}>
{cachedUri ? (
<Video
source={{ uri: cachedUri }}
resizeMode="cover"
shouldPlay
isMuted={false}
useNativeControls
style={{ width: '100%', height: 300 }}
/>
) : (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Loading cached video...</Text>
</View>
)}
</View>
);
};
export default App;? 关键注意事项与最佳实践:
- ⚠️ 避免 fetch + blob + writeFile 组合:blob 在 React Native 中序列化不稳定,尤其对大视频易失败;RNFS.downloadFile 是专为大文件设计的流式下载 API,支持断点续传与进度回调,强烈推荐。
-
⚠️ iOS 与 Android URI 格式差异:
- iOS:必须为 file:///absolute/path/to/file.mp4(三个斜杠,绝对路径);
- Android:需调用 RNFS.pathToUrl(path) 转换为 content:// 或 file:// 兼容 URI(取决于 RNFS 版本)。
-
⚠️ 权限配置(仅 Android):确保 AndroidManifest.xml 中已添加:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />(Android 10+ 推荐使用 android:requestLegacyExternalStorage="true" 或迁移到 Scoped Storage)
-
✅ 进阶缓存策略建议:
- 服务端配合 ETag + If-None-Match 实现条件请求(如答案中提及),避免重复下载未变更资源;
- 本地维护缓存元数据(如 URL → ETag → 本地路径映射表),结合 RNFS.stat() 判断时效性;
- 使用 expo-file-system 替代 react-native-fs(若项目已集成 Expo SDK),API 更统一且文档完善。
通过以上方案,你将获得一个稳定、可维护、跨平台兼容的视频缓存播放流程——既规避了 expo-av 对 URI 的严苛校验,又为后续增量更新与离线体验打下坚实基础。


















