构建离线友好的图片上传系统
网络连接不稳定不一定会导致糟糕的用户体验。通过使用PWA技术,如IndexedDB、Service Worker和Background Sync API,您可以构建一个离线友好的图片上传系统,该系统可以排队上传并自动重试,让用户即使离线也能无忧上传。
问题背景
想象一下,您正在填写一个在线表单,系统要求您上传文件。您点击输入框,从桌面选择文件,一切顺利。但突然网络断开,文件消失,您不得不重新上传。糟糕的网络连接可能导致您花费大量时间才能成功上传文件。
用户体验的破坏源于需要不断检查网络稳定性并多次重试上传。虽然我们可能无法对网络连接做太多改进,但作为开发者,我们可以采取措施减轻这个问题带来的痛苦。
解决方案概述
我们可以通过调整图片上传系统来解决这个问题,使用户能够离线上传图片,消除对可靠网络连接的需求,并在网络稳定时自动重试上传过程,无需用户干预。
本文将重点介绍如何使用PWA(渐进式Web应用)技术(如IndexedDB、Service Worker和Background Sync API)构建离线友好的图片上传系统。我们还将简要介绍如何改善该系统的用户体验。
系统规划
以下是离线友好图片上传系统的流程图:
- 用户选择图片:系统允许用户选择图片。
- 图片存储在本地IndexedDB中:系统检查网络连接。如果网络可用,系统直接上传图片,避免不必要的本地存储使用。如果网络不可用,图片将存储在IndexedDB中。
- Service Worker检测网络恢复:图片存储在IndexedDB中后,系统等待检测网络连接恢复以继续下一步。
- 背景同步处理待上传内容:连接恢复时,系统将尝试重新上传图片。
- 文件成功上传:图片上传后,系统将删除存储在IndexedDB中的本地副本。
系统实现
选择图片
系统实现的第一步是允许用户选择图片。有多种方式可以实现这一点:
- 使用简单的
<input type="file">
元素;
- 拖放界面。
建议同时使用这两种方式。有些用户喜欢使用拖放界面,而其他用户则认为上传图片的唯一方式是通过<input type="file">
元素。提供两种选项有助于改善用户体验。您还可以考虑使用Clipboard API允许用户直接在浏览器中粘贴图片。
注册Service Worker
该解决方案的核心是Service Worker。我们的Service Worker将负责从IndexedDB存储中检索图片,在网络连接恢复时上传,并在图片上传后清除IndexedDB存储。
要使用Service Worker,首先需要注册一个:
1
2
3
4
5
|
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then(reg => console.log('Service Worker registered', reg))
.catch(err => console.error('Service Worker registration failed', err));
}
|
检查网络连接
请记住,我们试图解决的问题是由不可靠的网络连接引起的。如果这个问题不存在,就没有必要解决任何问题。因此,一旦选择了图片,我们需要在注册同步事件并将图片存储在IndexedDB之前检查用户是否有可靠的互联网连接。
1
2
3
4
5
6
7
8
|
function uploadImage() {
if (navigator.onLine) {
// 上传图片
} else {
// 注册同步事件
// 将图片存储在IndexedDB中
}
}
|
注意:这里仅使用navigator.onLine
属性来演示系统的工作原理。navigator.onLine
属性不可靠,建议您提出自定义解决方案来检查用户是否连接到互联网。一种方法是通过向您创建的服务器端点发送ping请求。
注册同步事件
网络测试失败后,下一步是注册同步事件。同步事件需要在由于网络连接不良而无法上传图片时注册。
1
2
3
4
5
6
7
|
async function registerSyncEvent() {
if ('SyncManager' in window) {
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('uploadImages');
console.log('Background Sync registered');
}
}
|
注册同步事件后,您需要在Service Worker中监听它。
1
2
3
4
5
|
self.addEventListener('sync', (event) => {
if (event.tag === 'uploadImages') {
event.waitUntil(sendImages());
}
});
|
sendImages
函数将是一个异步过程,从IndexedDB检索图片并上传到服务器。它的样子如下:
1
2
3
4
5
6
7
|
async function sendImages() {
try {
// 等待图片检索和上传
} catch (error) {
// 抛出错误
}
}
|
打开数据库
为了本地存储图片,我们需要首先打开一个IndexedDB存储。从下面的代码中可以看到,我们创建了一个全局变量来存储数据库实例。这样做的原因是,随后当我们想从IndexedDB检索图片时,不需要再次编写打开数据库的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
let database; // 全局变量存储数据库实例
function openDatabase() {
return new Promise((resolve, reject) => {
if (database) return resolve(database); // 返回现有数据库实例
const request = indexedDB.open("myDatabase", 1);
request.onerror = (event) => {
console.error("Database error:", event.target.error);
reject(event.target.error); // 错误时拒绝承诺
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 如果不存在,创建“images”对象存储
if (!db.objectStoreNames.contains("images")) {
db.createObjectStore("images", { keyPath: "id" });
}
console.log("Database setup complete.");
};
request.onsuccess = (event) => {
database = event.target.result; // 全局存储数据库实例
resolve(database); // 用数据库实例解析承诺
};
});
}
|
在IndexedDB中存储图片
打开IndexedDB存储后,我们现在可以存储图片。
您可能想知道为什么没有使用更简单的解决方案,如localStorage。原因是IndexedDB异步运行,不会阻塞主JavaScript线程,而localStorage同步运行,如果使用可能会阻塞JavaScript主线程。
以下是如何在IndexedDB中存储图片:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
async function storeImages(file) {
// 打开IndexedDB数据库
const db = await openDatabase();
// 创建具有读写权限的事务
const transaction = db.transaction("images", "readwrite");
// 访问“images”对象存储
const store = transaction.objectStore("images");
// 定义要存储的图片记录
const imageRecord = {
id: IMAGE_ID, // 唯一ID
image: file // 存储图片文件(Blob)
};
// 将图片记录添加到存储中
const addRequest = store.add(imageRecord);
// 处理成功添加
addRequest.onsuccess = () => console.log("Image added successfully!");
// 处理插入过程中的错误
addRequest.onerror = (e) => console.error("Error storing image:", e.target.error);
}
|
图片存储和背景同步设置好后,系统准备在网络连接恢复时上传图片。
检索和上传图片
网络连接恢复后,同步事件将触发,Service Worker将从IndexedDB检索图片并上传。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
async function retrieveAndUploadImage(IMAGE_ID) {
try {
const db = await openDatabase(); // 确保数据库打开
const transaction = db.transaction("images", "readonly");
const store = transaction.objectStore("images");
const request = store.get(IMAGE_ID);
request.onsuccess = function (event) {
const image = event.target.result;
if (image) {
// 在这里上传图片到服务器
} else {
console.log("No image found with ID:", IMAGE_ID);
}
};
request.onerror = () => {
console.error("Error retrieving image.");
};
} catch (error) {
console.error("Failed to open database:", error);
}
}
|
删除IndexedDB数据库
图片上传后,IndexedDB存储不再需要。因此,应将其及其内容删除以释放存储空间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
function deleteDatabase() {
// 检查是否有打开的数据库连接
if (database) {
database.close(); // 关闭数据库连接
console.log("Database connection closed.");
}
// 请求删除名为“myDatabase”的数据库
const deleteRequest = indexedDB.deleteDatabase("myDatabase");
// 处理数据库成功删除
deleteRequest.onsuccess = function () {
console.log("Database deleted successfully!");
};
// 处理删除过程中的错误
deleteRequest.onerror = function (event) {
console.error("Error deleting database:", event.target.error);
};
// 处理删除被阻止的情况(例如,仍有打开的连接)
deleteRequest.onblocked = function () {
console.warn("Database deletion blocked. Close open connections and try again.");
};
}
|
至此,整个过程完成!
注意事项和限制
虽然我们通过支持离线上传做了很多工作来帮助改善体验,但系统并非没有限制。我认为特别指出这些限制是值得的,因为值得知道这个解决方案可能在哪些方面无法满足您的需求。
没有可靠的互联网连接检测
JavaScript没有提供万无一失的方法来检测在线状态。因此,您需要提出自定义解决方案来检测在线状态。
仅限Chromium解决方案
Background Sync API目前仅限于基于Chromium的浏览器。因此,该解决方案仅由Chromium浏览器支持。这意味着如果大多数用户使用非Chromium浏览器,您将需要更强大的解决方案。
IndexedDB存储策略
浏览器对IndexedDB施加存储限制和驱逐策略。例如,在Safari中,如果用户不与网站交互,存储在IndexedDB中的数据有七天的生命周期。如果您为支持Safari的背景同步API提出替代方案,应牢记这一点。
增强用户体验
由于整个过程在后台发生,我们需要一种方式来通知用户图片何时存储、等待上传或已成功上传。为此实现某些UI元素确实会增强用户体验。这些UI元素可能包括toast通知、上传状态指示器(如旋转器显示活动进程)、进度条(显示状态进度)、网络状态指示器,或提供重试和取消选项的按钮。
总结
糟糕的互联网连接可能会破坏Web应用程序的用户体验。然而,通过利用PWA技术(如IndexedDB、Service Worker和Background Sync API),开发者可以帮助提高Web应用程序对其用户的可靠性,特别是在互联网连接不可靠的地区。