从文件系统加载照片
我们已经实现了拍照和保存到文件系统的功能。还有最后一部分功能缺失:照片存储在文件系统中,但我们需要一种方式来保存每个文件的指针,以便它们可以在照片库中再次显示。
🌐 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 const usePhotoGallery = () => {
const photos = ref<UserPhoto[]>([]);
// CHANGE: Add a key for photo storage
const PHOTO_STORAGE = 'photos';
// ...existing code...
};
接下来,在 usePhotoGallery() 方法的末尾,添加对 cachePhotos 方法的调用以保存 photos 数组。通过在这里添加,每次拍摄新照片时都会存储 photos 数组。这样,无论应用用户何时关闭或切换到其他应用——所有照片数据都会被保存。
🌐 Next, at the end of the usePhotoGallery() method, add a call to the cachePhotos 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 { ref } from 'vue';
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 const usePhotoGallery = () => {
// ...existing code...
// CHANGE: Add `cachePhotos()` method
const cachePhotos = () => {
Preferences.set({
key: PHOTO_STORAGE,
value: JSON.stringify(photos.value),
});
};
return {
photos,
addNewToGallery,
};
};
接下来,使用 Vue 的 watch 方法 来监听 photos 数组。每当数组被修改(在本例中是拍照或删除照片时),触发 cachePhotos 方法。这样不仅可以重用代码,而且无论应用用户何时关闭或切换到其他应用,照片数据始终会被保存。
🌐 Next, use the Vue watch method to watch the photos array. Whenever the array is modified (in this case, taking or deleting photos), trigger the cachePhotos method. Not only do we get to reuse code, but it also doesn’t matter when the app user closes or switches to a different app - photo data is always saved.
在 usePhotoGallery() 中的 return 语句上方添加对 watch() 方法的调用。
🌐 Add the call to the watch() method above the return statement in usePhotoGallery().
// CHANGE: Update import
import { ref, watch } from 'vue';
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 const usePhotoGallery = () => {
// ...existing code...
// CHANGE: Add call to `watch` with `photos` array and `cachePhotos` method
watch(photos, cachePhotos);
return {
photos,
addNewToGallery,
};
};
在保存了照片数组数据后,在 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.
export const usePhotoGallery = () => {
// ...existing code...
// CHANGE: Add `loadSaved()` method
const loadSaved = async () => {
const photoList = await Preferences.get({ key: PHOTO_STORAGE });
const photosInPreferences = photoList.value ? JSON.parse(photoList.value) : [];
};
watch(photos, cachePhotos);
return {
photos,
addNewToGallery,
};
};
在移动端(接下来会讲到!),我们可以直接将图片标签的源 - <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 const usePhotoGallery = () => {
// ...existing code...
// CHANGE: Update `loadSaved()` method
const loadSaved = async () => {
const photoList = await Preferences.get({ key: PHOTO_STORAGE });
const photosInPreferences = photoList.value ? JSON.parse(photoList.value) : [];
// 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}`;
}
photos.value = photosInPreferences;
};
watch(photos, cachePhotos);
return {
photos,
addNewToGallery,
};
};
usePhotoGallery.ts 现在应该看起来像这样:
import { ref, watch } from 'vue';
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 const usePhotoGallery = () => {
const photos = ref<UserPhoto[]>([]);
const PHOTO_STORAGE = 'photos';
const addNewToGallery = async () => {
// Take a photo
const capturedPhoto = await Camera.getPhoto({
resultType: CameraResultType.Uri,
source: CameraSource.Camera,
quality: 100,
});
const fileName = Date.now() + '.jpeg';
const savedImageFile = await savePicture(capturedPhoto, fileName);
photos.value = [savedImageFile, ...photos.value];
};
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);
});
};
const cachePhotos = () => {
Preferences.set({
key: PHOTO_STORAGE,
value: JSON.stringify(photos.value),
});
};
const loadSaved = async () => {
const photoList = await Preferences.get({ key: PHOTO_STORAGE });
const photosInPreferences = photoList.value ? JSON.parse(photoList.value) : [];
for (const photo of photosInPreferences) {
const readFile = await Filesystem.readFile({
path: photo.filepath,
directory: Directory.Data,
});
photo.webviewPath = `data:image/jpeg;base64,${readFile.data}`;
}
photos.value = photosInPreferences;
};
watch(photos, cachePhotos);
return {
addNewToGallery,
photos,
};
};
export interface UserPhoto {
filepath: string;
webviewPath?: string;
}
我们的 usePhotoGallery() 现在可以加载保存的图片,但我们需要更新文件以使新代码生效。我们将在 onMounted 生命周期方法中调用 loadSaved,这样当用户首次进入照片库时,所有照片都会被加载并显示在屏幕上。
🌐 Our usePhotoGallery() can now load the saved images, but we'll need to update the file to put that new code to work. We'll call loadSaved within the onMounted lifecycle method so that when the user first navigates to the Photo Gallery, all photos are loaded and displayed on the screen.
将 usePhotoGallery.ts 更新为如下所示:
🌐 Update usePhotoGallery.ts to look like the following:
// CHANGE: Update import
import { ref, watch, onMounted } from 'vue';
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 const usePhotoGallery = () => {
const photos = ref<UserPhoto[]>([]);
const PHOTO_STORAGE = 'photos';
const addNewToGallery = async () => {
// Take a photo
const capturedPhoto = await Camera.getPhoto({
resultType: CameraResultType.Uri,
source: CameraSource.Camera,
quality: 100,
});
const fileName = Date.now() + '.jpeg';
const savedImageFile = await savePicture(capturedPhoto, fileName);
photos.value = [savedImageFile, ...photos.value];
};
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);
});
};
const cachePhotos = () => {
Preferences.set({
key: PHOTO_STORAGE,
value: JSON.stringify(photos.value),
});
};
const loadSaved = async () => {
const photoList = await Preferences.get({ key: PHOTO_STORAGE });
const photosInPreferences = photoList.value ? JSON.parse(photoList.value) : [];
for (const photo of photosInPreferences) {
const readFile = await Filesystem.readFile({
path: photo.filepath,
directory: Directory.Data,
});
photo.webviewPath = `data:image/jpeg;base64,${readFile.data}`;
}
photos.value = photosInPreferences;
};
// CHANGE: Add call to `onMounted()` with the `loadSaved()` method
onMounted(loadSaved);
watch(photos, cachePhotos);
return {
addNewToGallery,
photos,
};
};
export interface UserPhoto {
filepath: string;
webviewPath?: string;
}
如果在按照这些步骤操作后仍然看到损坏的图片链接或缺失的照片,你可能需要打开浏览器的开发者工具,并清除 localStorage 和 IndexedDB。
在 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!