构建离线友好的图片上传系统
网络连接不稳定不意味着用户体验必须糟糕。借助PWA技术如IndexedDB、Service Worker和Background Sync API,你可以构建一个离线友好的图片上传系统,该系统能够排队上传并在网络恢复后自动重试——让你的用户即使离线也能无忧上传。
想象一下:你正在填写在线表单,系统要求上传文件。你点击输入框,从桌面选择文件,一切顺利。但突然网络中断,文件消失,你不得不重新上传。糟糕的网络连接可能让你花费大量时间才能成功上传文件。
用户体验的破坏源于需要不断检查网络稳定性并多次重试上传。虽然我们无法控制网络连接,但作为开发者,我们总能采取措施减轻这个问题带来的痛苦。
解决这个问题的方法之一是调整图片上传系统,使用户能够离线上传图片——消除对可靠网络连接的需求,然后在网络稳定时系统自动重试上传过程,无需用户干预。
本文将重点介绍如何使用PWA(渐进式Web应用)技术如IndexedDB、Service Worker和Background Sync API构建离线友好的图片上传系统。我们还将简要介绍提升该系统用户体验的技巧。
规划离线图片上传系统
以下是离线友好图片上传系统的流程图:
1
2
3
4
5
|
用户选择图片 → 图片存储在IndexedDB → 检查网络连接
↓ ↓
网络可用 → 直接上传图片 网络不可用 → 等待网络恢复
↓ ↓
上传成功 → 删除本地副本 网络恢复 → 后台同步处理待上传内容
|
如图所示,流程展开如下:
- 用户选择图片
- 图片本地存储在IndexedDB中
- 系统检查网络连接。如果网络可用,系统直接上传图片,避免不必要的本地存储使用。但如果网络不可用,图片将存储在IndexedDB中
- Service Worker检测网络何时恢复
- 连接恢复时,系统尝试重新上传图片
- 图片成功上传后,系统删除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); // 错误时拒绝promise
};
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); // 用数据库实例解析promise
};
});
}
|
在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应用程序对其用户的可靠性,特别是对于那些在互联网连接不可靠地区的用户。