构建离线友好的图片上传系统:利用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. 图片存储在本地IndexedDB中:系统检查网络连接。如果网络可用,系统直接上传图片,避免不必要的本地存储使用。如果网络不可用,图片将存储在IndexedDB中。
  3. Service Worker检测网络恢复:图片存储在IndexedDB中后,系统等待检测网络连接恢复以继续下一步。
  4. 背景同步处理待上传内容:连接恢复时,系统将尝试重新上传图片。
  5. 文件成功上传:图片上传后,系统将删除存储在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应用程序对其用户的可靠性,特别是在互联网连接不可靠的地区。

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