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

打开 photo.service.ts 并首先在 PhotoService 类中定义一个新属性,该属性将作为存储的键。

🌐 Open photo.service.ts and begin by defining a new property in the PhotoService class that will act as the key for the store.

export class PhotoService {
public photos: UserPhoto[] = [];

// CHANGE: Add a key for photo storage
private PHOTO_STORAGE: string = 'photos';

// ...existing code...
}

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

🌐 Next, at the end of the addNewToGallery() method, add a call to Preferences.set() 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 { Injectable } from '@angular/core';
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';

@Injectable({
providedIn: 'root',
})
export class PhotoService {
// ...existing code...

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

const savedImageFile = await this.savePicture(capturedPhoto);

this.photos.unshift(savedImageFile);

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

// ...existing code...
}

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

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

🌐 With the photo array data saved, create a new public method in the PhotoService class 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 class PhotoService {
// ...existing code...

// CHANGE: Add the method to load the photo data
public async loadSaved() {
// Retrieve cached photo array data
const { value: photoList } = await Preferences.get({ key: this.PHOTO_STORAGE });
this.photos = (photoList ? JSON.parse(photoList) : []) as UserPhoto[];
}
}

在移动端(接下来会讲到!),我们可以直接将图片标签的源——<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 class PhotoService {
// ...existing code...

// CHANGE: Update the `loadSaved()` method
public async loadSaved() {
// Retrieve cached photo array data
const { value: photoList } = await Preferences.get({ key: this.PHOTO_STORAGE });
this.photos = (photoList ? JSON.parse(photoList) : []) as UserPhoto[];

// CHANGE: Display the photo by reading into base64 format
for (let photo of this.photos) {
// Read each saved photo's data from the Filesystem
const readFile = await Filesystem.readFile({
path: photo.filepath,
directory: Directory.Data,
});

// Web platform only: Load the photo as base64 data
photo.webviewPath = `data:image/jpeg;base64,${readFile.data}`;
}
}
}

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

import { Injectable } from '@angular/core';
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
import type { Photo } from '@capacitor/camera';
import { Filesystem, Directory } from '@capacitor/filesystem';
import { Preferences } from '@capacitor/preferences';

@Injectable({
providedIn: 'root',
})
export class PhotoService {
public photos: UserPhoto[] = [];

private PHOTO_STORAGE: string = 'photos';

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

// Save the picture and add it to photo collection
const savedImageFile = await this.savePicture(capturedPhoto);

this.photos.unshift(savedImageFile);

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

private async savePicture(photo: Photo) {
// 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 this.convertBlobToBase64(blob)) as string;

// Write the file to the data directory
const fileName = Date.now() + '.jpeg';
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,
};
}

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

public async loadSaved() {
// Retrieve cached photo array data
const { value: photoList } = await Preferences.get({ key: this.PHOTO_STORAGE });
this.photos = (photoList ? JSON.parse(photoList) : []) as UserPhoto[];

for (let photo of this.photos) {
// Read each saved photo's data from the Filesystem
const readFile = await Filesystem.readFile({
path: photo.filepath,
directory: Directory.Data,
});

// Web platform only: Load the photo as base64 data
photo.webviewPath = `data:image/jpeg;base64,${readFile.data}`;
}
}
}

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

我们的 PhotoService 现在可以加载保存的图片,但我们需要更新 tab2.page.ts 来使新代码生效。我们将在 ngOnInit 生命周期方法中调用 loadSaved(),这样当用户首次导航到照片图库时,所有照片都会被加载并显示在屏幕上。

🌐 Our PhotoService can now load the saved images, but we'll need to update tab2.page.ts to put that new code to work. We'll call loadSaved() within the ngOnInit lifecycle method so that when the user first navigates to the Photo Gallery, all photos are loaded and displayed on the screen.

tab2.page.ts 更新为如下所示:

🌐 Update tab2.page.ts to look like the following:

import { Component } from '@angular/core';
import { PhotoService } from '../services/photo.service';

@Component({
selector: 'app-tab2',
templateUrl: 'tab2.page.html',
styleUrls: ['tab2.page.scss'],
standalone: false,
})
export class Tab2Page {
constructor(public photoService: PhotoService) {}

// CHANGE: Add call to `loadSaved()` when navigating to the Photos tab
async ngOnInit() {
await this.photoService.loadSaved();
}

addPhotoToGallery() {
this.photoService.addNewToGallery();
}
}
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!