You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native Expo离线存图至相册并免UI加载问题求助

实现方案(React Native Expo)

1. 权限配置

先在app.json中配置必要权限,确保能访问和写入相册:

{
  "expo": {
    "permissions": [
      "MEDIA_LIBRARY",
      "MEDIA_LIBRARY_WRITE_ONLY",
      "CAMERA"
    ],
    "ios": {
      "infoPlist": {
        "NSPhotoLibraryUsageDescription": "需要访问相册存储图片",
        "NSPhotoLibraryAddUsageDescription": "需要将图片保存到相册"
      }
    },
    "android": {
      "permissions": [
        "android.permission.READ_MEDIA_IMAGES",
        "android.permission.WRITE_EXTERNAL_STORAGE"
      ]
    }
  }
}

2. 保存图片到相册并记录元数据

核心是用expo-media-library保存图片,同时把图片的资产ID/URI存入AsyncStorage,方便后续无UI加载:

示例:相机拍摄后保存

import { CameraView, useCameraPermissions } from 'expo-camera';
import * as MediaLibrary from 'expo-media-library';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Button, View, useRef } from 'react-native';

export default function CameraScreen() {
  const camera = useRef(null);
  const [cameraPermission, requestCameraPermission] = useCameraPermissions();
  const [libraryPermission, requestLibraryPermission] = MediaLibrary.usePermissions();

  const takePictureAndSave = async () => {
    // 校验权限
    if (!cameraPermission?.granted) {
      await requestCameraPermission();
      return;
    }
    if (!libraryPermission?.granted) {
      await requestLibraryPermission();
      return;
    }

    // 拍摄并保存到相册
    const photo = await camera.current.takePictureAsync();
    const asset = await MediaLibrary.saveToLibraryAsync(photo.uri);
    
    // 记录图片元数据到离线存储
    const pendingUploads = await AsyncStorage.getItem('pendingUploads');
    const uploadList = pendingUploads ? JSON.parse(pendingUploads) : [];
    uploadList.push({
      id: asset.id,
      uri: asset.uri,
      createdAt: new Date().toISOString(),
      uploaded: false
    });
    await AsyncStorage.setItem('pendingUploads', JSON.stringify(uploadList));
  };

  return (
    <View style={{ flex: 1 }}>
      <CameraView style={{ flex: 1 }} ref={camera} />
      <Button title="拍摄并保存" onPress={takePictureAndSave} />
    </View>
  );
}

示例:保存本地生成/缓存的图片

如果是本地生成的图片(比如Canvas绘制、离线缓存的图片),直接传入图片URI到saveToLibraryAsync即可,步骤和上面一致。

3. 无需UI选择直接加载相册图片

利用之前存在AsyncStorage里的资产ID或URI,直接获取并加载图片,完全跳过选择UI:

import * as MediaLibrary from 'expo-media-library';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Image, View, FlatList, useState, useEffect } from 'react-native';

export default function OfflineImagesScreen() {
  const [images, setImages] = useState([]);

  useEffect(() => {
    loadOfflineImages();
  }, []);

  const loadOfflineImages = async () => {
    const pendingUploads = await AsyncStorage.getItem('pendingUploads');
    if (!pendingUploads) return;
    
    const uploadList = JSON.parse(pendingUploads);
    // 通过资产ID获取图片详情(比直接存URI更可靠,URI可能随系统变化)
    const imageAssets = await Promise.all(
      uploadList.map(item => MediaLibrary.getAssetInfoAsync(item.id))
    );
    setImages(imageAssets);
  };

  return (
    <FlatList
      data={images}
      renderItem={({ item }) => (
        <Image source={{ uri: item.uri }} style={{ width: 200, height: 200, margin: 10 }} />
      )}
      keyExtractor={item => item.id}
    />
  );
}

4. 联网后自动上传图片

结合expo-network监听网络状态,联网后遍历待上传列表完成上传:

import * as Network from 'expo-network';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useEffect } from 'react';

const uploadPendingImages = async () => {
  const pendingUploads = await AsyncStorage.getItem('pendingUploads');
  if (!pendingUploads) return;
  
  const uploadList = JSON.parse(pendingUploads);
  const updatedList = [];

  for (const item of uploadList) {
    if (item.uploaded) {
      updatedList.push(item);
      continue;
    }
    
    try {
      // 构建FormData上传
      const formData = new FormData();
      formData.append('image', {
        uri: item.uri,
        name: `image-${item.id}.jpg`,
        type: 'image/jpeg'
      });

      await fetch('你的上传接口地址', {
        method: 'POST',
        body: formData,
        headers: {
          'Content-Type': 'multipart/form-data'
        }
      });

      // 上传成功:标记状态+可选删除本地图片
      updatedList.push({ ...item, uploaded: true });
      await MediaLibrary.deleteAssetsAsync([item.id]);
    } catch (error) {
      console.error('上传失败:', error);
      // 保留失败条目,下次联网重试
      updatedList.push(item);
    }
  }

  // 更新离线存储的待上传列表
  await AsyncStorage.setItem('pendingUploads', JSON.stringify(updatedList));
};

export default function UploadListener() {
  useEffect(() => {
    // 初始检查网络状态
    const checkInitialNetwork = async () => {
      const state = await Network.getNetworkStateAsync();
      if (state.isConnected) await uploadPendingImages();
    };
    checkInitialNetwork();

    // 监听网络状态变化
    const subscription = Network.addNetworkStateListener(state => {
      if (state.isConnected) uploadPendingImages();
    });

    return () => subscription.remove();
  }, []);

  return null;
}

关键注意事项

  • 权限兼容:Android 13+使用READ_MEDIA_IMAGES权限,旧版本需用WRITE_EXTERNAL_STORAGE;iOS必须在infoPlist中添加权限描述。
  • 存储可靠性:优先用资产ID而非URI存储,系统相册的URI可能发生变化,资产ID是持久化标识。
  • 重试机制:可给失败上传的条目添加重试次数限制,避免无限重试。
  • 存储选择:如果待上传图片数量多,建议用expo-sqlite替代AsyncStorage,支持更复杂的查询和批量操作。

内容的提问来源于stack exchange,提问作者Nicolas Cebreiro

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 12:25:21