Skip to main content

从文件系统加载照片

我们已经实现了拍照和保存到文件系统的功能。还有最后一部分功能缺失:照片存储在文件系统中,但我们需要一种方式来保存每个文件的指针,以便它们可以在照片库中再次显示。

🌐 We’ve implemented photo taking and saving to the filesystem. There’s one last piece of functionality missing: the photos are stored in the filesystem, but we need a way to save pointers to each file so that they can be displayed again in the photo gallery.

幸运的是,这很容易:我们将利用 Capacitor 的 Preferences API 在键值存储中存储我们的照片数组。

🌐 Fortunately, this is easy: we’ll leverage the Capacitor Preferences API to store our array of Photos in a key-value store.

首选项 API

🌐 Preferences API

打开 usePhotoGallery.ts 并首先定义一个常量变量,它将作为存储的键。

🌐 Open usePhotoGallery.ts and begin by defining a constant variable that will act as the key for the store.

export function usePhotoGallery() {
const [photos, setPhotos] = useState<UserPhoto[]>([]);
// CHANGE: Add a key for photo storage
const PHOTO_STORAGE = 'photos';

// ...existing code...
}

接下来,在 addNewToGallery() 方法的末尾,添加对 Preferences.set() 方法的调用以保存 photos 数组。通过在这里添加,每次拍摄新照片时都会存储 photos 数组。这样,无论应用用户何时关闭或切换到其他应用——所有照片数据都会被保存。

🌐 Next, at the end of the addNewToGallery() method, add a call to the Preferences.set() method to save the photos array. By adding it here, the photos array is stored each time a new photo is taken. This way, it doesn’t matter when the app user closes or switches to a different app - all photo data is saved.

import { useState } from 'react';
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
import type { Photo } from '@capacitor/camera';
import { Filesystem, Directory } from '@capacitor/filesystem';
// CHANGE: Add import
import { Preferences } from '@capacitor/preferences';

export function usePhotoGallery() {
// ...existing code...

const addNewToGallery = async () => {
// ...existing code...

// CHANGE: Add method to cache all photo data for future retrieval
Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) });
};

// ...existing code...

return {
addNewToGallery,
photos,
};
}

在保存了照片数组数据后,在 usePhotoGallery() 中创建一个名为 loadSaved() 的新方法,用于检索照片数据。我们使用相同的键来检索 JSON 格式的 photos 数组,然后将其解析为数组。

🌐 With the photo array data saved, create a new method in the usePhotoGallery() called loadSaved() that can retrieve the photo data. We use the same key to retrieve the photos array in JSON format, then parse it into an array.

// CHANGE: Update import
import { useState, useEffect } from 'react';
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
import type { Photo } from '@capacitor/camera';
import { Filesystem, Directory } from '@capacitor/filesystem';
import { Preferences } from '@capacitor/preferences';

export function usePhotoGallery() {
const [photos, setPhotos] = useState<UserPhoto[]>([]);

const PHOTO_STORAGE = 'photos';

// CHANGE: Add useEffect hook
useEffect(() => {
// CHANGE: Add `loadSaved()` method
const loadSaved = async () => {
const { value: photoList } = await Preferences.get({ key: PHOTO_STORAGE });
const photosInPreferences = (photoList ? JSON.parse(photoList) : []) as UserPhoto[];
};

loadSaved();
}, []);

// ...existing code...
}

第二个参数,空的依赖数组([]),告诉 React 仅运行函数一次。通常,useEffect 钩子 会在每次渲染后运行,但是传入一个空数组会阻止它再次运行,因为没有依赖,也就是钩子依赖的值,会发生任何变化。

🌐 The second parameter, the empty dependency array ([]), is what tells React to only run the function once. Normally, useEffect hooks run after every render, but passing an empty array prevents it from running again because none of the dependencies, the values the hook relies on, will ever change.

在移动端(接下来会讲到!),我们可以直接将图片标签的源 - <img src="x" /> - 设置为 Filesystem 上的每个照片文件,从而自动显示它们。然而,在网页端,我们必须将每个图片从 Filesystem 读取为 base64 格式,使用 Photo 对象上的一个新属性 base64。这是因为 Filesystem API 在底层使用了 IndexedDB。添加以下代码以完成 loadSaved() 方法。

🌐 On mobile (coming up next!), we can directly set the source of an image tag - <img src="x" /> - to each photo file on the Filesystem, displaying them automatically. On the web, however, we must read each image from the Filesystem into base64 format, using a new base64 property on the Photo object. This is because the Filesystem API uses IndexedDB under the hood. Add the following code to complete the loadSaved() method.

export function usePhotoGallery() {
const [photos, setPhotos] = useState<UserPhoto[]>([]);

const PHOTO_STORAGE = 'photos';

useEffect(() => {
// CHANGE: Update `loadSaved()` method
const loadSaved = async () => {
const { value: photoList } = await Preferences.get({ key: PHOTO_STORAGE });
const photosInPreferences = (photoList ? JSON.parse(photoList) : []) as UserPhoto[];

// CHANGE: Display the photo by reading into base64 format
for (const photo of photosInPreferences) {
const readFile = await Filesystem.readFile({
path: photo.filepath,
directory: Directory.Data,
});
photo.webviewPath = `data:image/jpeg;base64,${readFile.data}`;
}

setPhotos(photosInPreferences);
};

loadSaved();
}, []);

// ...existing code...
}

usePhotoGallery.ts 现在应该看起来像这样:

import { useState, useEffect } from 'react';
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
import type { Photo } from '@capacitor/camera';
import { Filesystem, Directory } from '@capacitor/filesystem';
import { Preferences } from '@capacitor/preferences';

export function usePhotoGallery() {
const [photos, setPhotos] = useState<UserPhoto[]>([]);

const PHOTO_STORAGE = 'photos';

useEffect(() => {
const loadSaved = async () => {
const { value: photoList } = await Preferences.get({ key: PHOTO_STORAGE });
const photosInPreferences = (photoList ? JSON.parse(photoList) : []) as UserPhoto[];

for (const photo of photosInPreferences) {
const readFile = await Filesystem.readFile({
path: photo.filepath,
directory: Directory.Data,
});
photo.webviewPath = `data:image/jpeg;base64,${readFile.data}`;
}

setPhotos(photosInPreferences);
};

loadSaved();
}, []);

const addNewToGallery = async () => {
// Take a photo
const capturedPhoto = await Camera.getPhoto({
resultType: CameraResultType.Uri,
source: CameraSource.Camera,
quality: 100,
});

const fileName = Date.now() + '.jpeg';
// Save the picture and add it to photo collection
const savedImageFile = await savePicture(capturedPhoto, fileName);

const newPhotos = [savedImageFile, ...photos];
setPhotos(newPhotos);

Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) });
};

const savePicture = async (photo: Photo, fileName: string): Promise<UserPhoto> => {
// Fetch the photo, read as a blob, then convert to base64 format
const response = await fetch(photo.webPath!);
const blob = await response.blob();
const base64Data = (await convertBlobToBase64(blob)) as string;

const savedFile = await Filesystem.writeFile({
path: fileName,
data: base64Data,
directory: Directory.Data,
});

// Use webPath to display the new image instead of base64 since it's
// already loaded into memory
return {
filepath: fileName,
webviewPath: photo.webPath,
};
};

const convertBlobToBase64 = (blob: Blob) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = reject;
reader.onload = () => {
resolve(reader.result);
};
reader.readAsDataURL(blob);
});
};

return {
addNewToGallery,
photos,
};
}

export interface UserPhoto {
filepath: string;
webviewPath?: string;
}
note

如果在按照这些步骤操作后仍然看到损坏的图片链接或缺失的照片,你可能需要打开浏览器的开发者工具,并清除 localStorageIndexedDB

在 localStorage 中,查找域 http://localhost:8100 和键 CapacitorStorage.photos。在 IndexedDB 中,找到一个名为 "FileStorage" 的存储。你的照片会有一个类似 /DATA/123456789012.jpeg 的键。

🌐 In localStorage, look for domain http://localhost:8100 and key CapacitorStorage.photos. In IndexedDB, find a store called "FileStorage". Your photos will have a key like /DATA/123456789012.jpeg.

就是这样!我们已经在我们的 Ionic 应用中构建了一个完整的照片图库功能,它可以在网页上运行。接下来,我们将把它转化为适用于 iOS 和 Android 的移动应用!

🌐 That’s it! We’ve built a complete Photo Gallery feature in our Ionic app that works on the web. Next up, we’ll transform it into a mobile app for iOS and Android!