构建离线友好的图片上传系统:利用PWA技术实现无缝上传体验

本文详细介绍了如何使用PWA技术(包括IndexedDB、Service Worker和Background Sync API)构建离线图片上传系统。该系统能在网络中断时自动存储图片,并在网络恢复后重新尝试上传,显著提升用户体验。

构建离线友好的图片上传系统

网络连接不稳定不意味着用户体验必须糟糕。借助PWA技术如IndexedDB、Service Worker和Background Sync API,你可以构建一个离线友好的图片上传系统,该系统能够排队上传并在网络恢复后自动重试——让你的用户即使离线也能无忧上传。

想象一下:你正在填写在线表单,系统要求上传文件。你点击输入框,从桌面选择文件,一切顺利。但突然网络中断,文件消失,你不得不重新上传。糟糕的网络连接可能让你花费大量时间才能成功上传文件。

用户体验的破坏源于需要不断检查网络稳定性并多次重试上传。虽然我们无法控制网络连接,但作为开发者,我们总能采取措施减轻这个问题带来的痛苦。

解决这个问题的方法之一是调整图片上传系统,使用户能够离线上传图片——消除对可靠网络连接的需求,然后在网络稳定时系统自动重试上传过程,无需用户干预。

本文将重点介绍如何使用PWA(渐进式Web应用)技术如IndexedDB、Service Worker和Background Sync API构建离线友好的图片上传系统。我们还将简要介绍提升该系统用户体验的技巧。

规划离线图片上传系统

以下是离线友好图片上传系统的流程图:

1
2
3
4
5
用户选择图片 → 图片存储在IndexedDB → 检查网络连接
    ↓                             ↓
网络可用 → 直接上传图片       网络不可用 → 等待网络恢复
    ↓                             ↓
上传成功 → 删除本地副本       网络恢复 → 后台同步处理待上传内容

如图所示,流程展开如下:

  1. 用户选择图片
  2. 图片本地存储在IndexedDB中
  3. 系统检查网络连接。如果网络可用,系统直接上传图片,避免不必要的本地存储使用。但如果网络不可用,图片将存储在IndexedDB中
  4. Service Worker检测网络何时恢复
  5. 连接恢复时,系统尝试重新上传图片
  6. 图片成功上传后,系统删除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应用程序对其用户的可靠性,特别是对于那些在互联网连接不可靠地区的用户。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计