使用 Service Worker 处理离线时的文件上传

2024-02-16

我们有一个 Web 应用程序(使用 AngularJS 构建),我们也逐渐添加 PWA“功能”(服务工作线程、可启动项、通知等)。我们的网络应用程序的功能之一是能够在离线状态下完成网络表单。目前,我们在离线时将数据存储在 IndexedDB 中,并鼓励用户在在线时将该数据推送到服务器(“此表单已保存到您的设备。现在您重新上线,您应该保存它到云端......”)。我们会在某个时候自动执行此操作,但目前没有必要。

我们正在向这些 Web 表单添加一项功能,用户可以将文件(图像、文档)附加到表单,也许可以在整个表单的多个位置附加文件(图像、文档)。

我的问题是 - 有没有办法让 Service Worker 处理文件上传?以某种方式(也许)在离线时存储要上传的文件的路径,并在连接恢复后推送该文件?这是否适用于移动设备,因为我们可以访问这些设备上的“路径”?任何帮助、建议或参考将不胜感激。


处理文件上传/删除以及几乎所有事情的一种方法是跟踪脱机请求期间所做的所有更改。我们可以创建一个sync对象内部有两个数组,一个用于需要上传的待处理文件,一个用于需要在我们重新上线时删除的已删除文件。

tl;dr

关键阶段


  1. Service Worker 安装


    • 除了静态数据外,我们还确保获取动态数据作为上传文件的主要列表(在示例中/uploads GET返回 JSON 数据和文件)。

  2. 服务人员获取


    • 处理服务人员fetch事件中,如果获取失败,那么我们必须处理文件列表请求、将文件上传到服务器的请求以及从服务器删除文件的请求。如果我们没有任何这些请求,那么我们会从默认缓存中返回匹配项。

      • Listing GET
        我们得到列表的缓存对象(在我们的例子中/uploads)和sync目的。我们concat默认列表文件pending文件,我们删除deleted文件,我们返回带有 JSON 结果的新响应对象,因为服务器会返回它。
      • 上传中PUT
        我们得到缓存的列表文件和sync pending缓存中的文件。如果该文件不存在,那么我们为该文件创建一个新的缓存条目,并使用 mime 类型和blob从创建新的请求Response对象,它将被保存到默认缓存中。
      • 正在删除DELETE
        我们检查缓存的上传,如果文件存在,我们从列表数组和缓存文件中删除该条目。如果文件处于待处理状态,我们只需从pending数组,否则如果它还没有在deleted数组,然后我们添加它。我们最后更新列表、文件和同步对象缓存。

  3. Syncing


    • 当。。。的时候online事件被触发,我们尝试与服务器同步。我们读到sync cache.

      • 如果有待处理的文件,那么我们获取每个文件Response来自缓存的对象,我们发送一个PUT fetch请求返回给服务器。
      • 如果有已删除的文件,那么我们会发送DELETE fetch向服务器请求每个文件。
      • 最后,我们重置sync缓存对象。

代码实现


(Please read the inline comments)

服务人员安装

const cacheName = 'pwasndbx';
const syncCacheName = 'pwasndbx-sync';
const pendingName = '__pending';
const syncName = '__sync';

const filesToCache = [
  '/',
  '/uploads',
  '/styles.css',
  '/main.js',
  '/utils.js',
  '/favicon.ico',
  '/manifest.json',
];

/* Start the service worker and cache all of the app's content */
self.addEventListener('install', function(e) {
  console.log('SW:install');

  e.waitUntil(Promise.all([
    caches.open(cacheName).then(async function(cache) {
      let cacheAdds = [];

      try {
        // Get all the files from the uploads listing
        const res = await fetch('/uploads');
        const { data = [] } = await res.json();
        const files = data.map(f => `/uploads/${f}`);

        // Cache all uploads files urls
        cacheAdds.push(cache.addAll(files));
      } catch(err) {
        console.warn('PWA:install:fetch(uploads):err', err);
      }

      // Also add our static files to the cache
      cacheAdds.push(cache.addAll(filesToCache));
      return Promise.all(cacheAdds);
    }),
    // Create the sync cache object
    caches.open(syncCacheName).then(cache => cache.put(syncName, jsonResponse({
      pending: [], // For storing the penging files that later will be synced
      deleted: []  // For storing the files that later will be deleted on sync
    }))),
  ])
  );
});

服务人员获取

self.addEventListener('fetch', function(event) {
  // Clone request so we can consume data later
  const request = event.request.clone();
  const { method, url, headers } = event.request;

  event.respondWith(
    fetch(event.request).catch(async function(err) {
      const { headers, method, url } = event.request;

      // A custom header that we set to indicate the requests come from our syncing method
      // so we won't try to fetch anything from cache, we need syncing to be done on the server
      const xSyncing = headers.get('X-Syncing');

      if(xSyncing && xSyncing.length) {
        return caches.match(event.request);
      }

      switch(method) {
        case 'GET':
          // Handle listing data for /uploads and return JSON response
          break;
        case 'PUT':
          // Handle upload to cache and return success response
          break;
        case 'DELETE':
          // Handle delete from cache and return success response
          break;
      }

      // If we meet no specific criteria, then lookup to the cache
      return caches.match(event.request);
    })
  );
});

function jsonResponse(data, status = 200) {
  return new Response(data && JSON.stringify(data), {
    status,
    headers: {'Content-Type': 'application/json'}
  });
}

服务人员获取Listing GET

if(url.match(/\/uploads\/?$/)) { // Failed to get the uploads listing
  // Get the uploads data from cache
  const uploadsRes = await caches.match(event.request);
  let { data: files = [] } = await uploadsRes.json();

  // Get the sync data from cache
  const syncRes = await caches.match(new Request(syncName), { cacheName: syncCacheName });
  const sync = await syncRes.json();

  // Return the files from uploads + pending files from sync - deleted files from sync
  const data = files.concat(sync.pending).filter(f => sync.deleted.indexOf(f) < 0);

  // Return a JSON response with the updated data
  return jsonResponse({
    success: true,
    data
  });
}

服务人员获取上传中PUT

// Get our custom headers
const filename = headers.get('X-Filename');
const mimetype = headers.get('X-Mimetype');

if(filename && mimetype) {
  // Get the uploads data from cache
  const uploadsRes = await caches.match('/uploads', { cacheName });
  let { data: files = [] } = await uploadsRes.json();

  // Get the sync data from cache
  const syncRes = await caches.match(new Request(syncName), { cacheName: syncCacheName });
  const sync = await syncRes.json();

  // If the file exists in the uploads or in the pendings, then return a 409 Conflict response
  if(files.indexOf(filename) >= 0 || sync.pending.indexOf(filename) >= 0) {
    return jsonResponse({ success: false }, 409);
  }

  caches.open(cacheName).then(async (cache) => {
    // Write the file to the cache using the response we cloned at the beggining
    const data = await request.blob();
    cache.put(`/uploads/${filename}`, new Response(data, {
      headers: { 'Content-Type': mimetype }
    }));

    // Write the updated files data to the uploads cache
    cache.put('/uploads', jsonResponse({ success: true, data: files }));
  });

  // Add the file to the sync pending data and update the sync cache object
  sync.pending.push(filename);
  caches.open(syncCacheName).then(cache => cache.put(new Request(syncName), jsonResponse(sync)));

  // Return a success response with fromSw set to tru so we know this response came from service worker
  return jsonResponse({ success: true, fromSw: true });
}

服务人员获取正在删除DELETE

// Get our custom headers
const filename = headers.get('X-Filename');

if(filename) {
  // Get the uploads data from cache
  const uploadsRes = await caches.match('/uploads', { cacheName });
  let { data: files = [] } = await uploadsRes.json();

  // Get the sync data from cache
  const syncRes = await caches.match(new Request(syncName), { cacheName: syncCacheName });
  const sync = await syncRes.json();

  // Check if the file is already pending or deleted
  const pendingIndex = sync.pending.indexOf(filename);
  const uploadsIndex = files.indexOf(filename);

  if(pendingIndex >= 0) {
    // If it's pending, then remove it from pending sync data
    sync.pending.splice(pendingIndex, 1);
  } else if(sync.deleted.indexOf(filename) < 0) {
    // If it's not in pending and not already in sync for deleting,
    // then add it for delete when we'll sync with the server
    sync.deleted.push(filename);
  }

  // Update the sync cache
  caches.open(syncCacheName).then(cache => cache.put(new Request(syncName), jsonResponse(sync)));

  // If the file is in the uplods data
  if(uploadsIndex >= 0) {
    // Updates the uploads data
    files.splice(uploadsIndex, 1);
    caches.open(cacheName).then(async (cache) => {
      // Remove the file from the cache
      cache.delete(`/uploads/${filename}`);
      // Update the uploads data cache
      cache.put('/uploads', jsonResponse({ success: true, data: files }));
    });
  }

  // Return a JSON success response
  return jsonResponse({ success: true });
}

Synching

// Get the sync data from cache
const syncRes = await caches.match(new Request(syncName), { cacheName: syncCacheName });
const sync = await syncRes.json();

// If the are pending files send them to the server
if(sync.pending && sync.pending.length) {
  sync.pending.forEach(async (file) => {
    const url = `/uploads/${file}`;
    const fileRes = await caches.match(url);
    const data = await fileRes.blob();

    fetch(url, {
      method: 'PUT',
      headers: {
        'X-Filename': file,
        'X-Syncing': 'syncing' // Tell SW fetch that we are synching so to ignore this fetch
      },
      body: data
    }).catch(err => console.log('sync:pending:PUT:err', file, err));
  });
}

// If the are deleted files send delete request to the server
if(sync.deleted && sync.deleted.length) {
  sync.deleted.forEach(async (file) => {
    const url = `/uploads/${file}`;

    fetch(url, {
      method: 'DELETE',
      headers: {
        'X-Filename': file,
        'X-Syncing': 'syncing' // Tell SW fetch that we are synching so to ignore this fetch
      }
    }).catch(err => console.log('sync:deleted:DELETE:err', file, err));
  });
}

// Update and reset the sync cache object
caches.open(syncCacheName).then(cache => cache.put(syncName, jsonResponse({
  pending: [],
  deleted: []
})));

示例 PWA


我创建了一个实现所有这些的 PWA 示例,您可以找到并测试它here https://pwasndbx.zikro.gr/。我已经使用 Chrome 和 Firefox 以及在移动设备上使用 Firefox Android 对其进行了测试。

您可以找到该应用程序的完整源代码(包括一个express server)在此 Github 存储库中:https://github.com/clytras/pwa-sandbox https://github.com/clytras/pwa-sandbox.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用 Service Worker 处理离线时的文件上传 的相关文章

  • 主干视图 DOM 元素已删除

    我一直在阅读有关 Backbone js 僵尸 或内存泄漏 问题的信息 基本上 当您不再需要该元素时 您必须从 DOM 中解除绑定并删除该元素 以确保所有事件也被删除 现在 我有一个包含几个容器的单页应用程序 div div div div
  • 如何重定向到 instagram://user?username={username}

    我的 html 页面上有这个链接 可以在特定用户上打开 Instagram 应用程序 a href Link to Instagram Profile a 我一直在寻找自动运行 url instagram user username USE
  • 使用 useReducers 调度函数发送多个操作?

    使用时是否可以通过调度函数发送多个动作useReducer挂钩反应 我尝试向它传递一组操作 但这会引发未处理的运行时异常 明确地说 通常会有一个初始状态对象和一个减速器 如下所示 const initialState message1 nu
  • 我想检查 $('#td1').text() === "x" 是否?

    我想检查innerHtml是否有X或O 所以我不能再次添加任何其他东西 但它不起作用 添加检查代码后它就停止了 我在这里尝试做一个简单的XO游戏来更熟悉javascript和jquery 我也不确定是否可以用 jQuery 做到这一点
  • 除了更改标题之外,如何在 Firefox 中强制另存为对话框?

    有没有办法在 ff 中强制打开 www example com example pdf 的另存为对话框 我无法更改标题 如果您可以将文件以 Base64 格式输出到客户端 则可以使用 data uri 进行下载 location href
  • 如何将 Google Charts 与 Vue.js 库一起使用?

    我正在尝试使用 Vue js 库使用 Google Charts 制作图表 但我不知道如何添加到 div 这是我尝试做的 这是如何使用普通 javascript 添加图表 这是文档的代码示例 https developers google
  • Jquery/Javascript 上传和下载文件,无需后端

    是否可以在没有后端服务器的情况下在 JavaScript 函数中下载和上传文件 我需要导出和导入由 JavaScript 函数生成的 XML 我想创建按钮 保存 xml 来保存文件 但我不知道是否可行 另一方面 我希望将 XML 文件直接上
  • MVC 在布局代码之前执行视图代码并破坏我的脚本顺序

    我正在尝试将所有 javascript 包含内容移至页面底部 我正在将 MVC 与 Razor 一起使用 我编写了一个辅助方法来注册脚本 它按注册顺序保留脚本 并排除重复的内容 Html RegisterScript scripts som
  • 如何在react-native中获取Text组件的onPress值

    我是一名新的 React Native 开发人员 我想使用 onPress 获取 Text 组件的值并将其传递给函数
  • 如何使输入字段和提交按钮变灰

    我想变灰这两件事 http doorsplit heroku com 歌曲输入字段和提交按钮 直到用户输入艺术家 有没有一种简单的方法可以通过 JQuery 来做到这一点 艺术家输入字段的id是 request artist 你可以这样做
  • Firefox 书签探索未超过 Javascript 的第一级

    我已经编写了一些代码来探索我的 Firefox 书签 但我只获得了第一级书签 即我没有获得文件夹中的链接 e g 搜索引擎 雅虎网站 谷歌网站 在此示例中 我只能访问 Search engines 和 google com 不能访问 yah
  • 提交表单并重定向页面

    我在 SO 上看到了很多与此相关的其他问题 但没有一个对我有用 我正在尝试提交POST表单 然后将用户重定向到另一个页面 但我无法同时实现这两种情况 我可以获取重定向或帖子 但不能同时获取两者 这是我现在所拥有的
  • Angular 2+ 安全性;保护服务器上的延迟加载模块

    我有一个 Angular 2 应用程序 用户可以在其中输入个人数据 该数据在应用程序的另一部分进行分析 该部分仅适用于具有特定权限的人员 问题是我们不想让未经授权的人知道how我们正在分析这些数据 因此 如果他们能够在应用程序中查看模板 那
  • 如何使用tampermonkey模拟react应用程序中的点击?

    我正在尝试使用 Tampermonkey 脚本模拟对 React 元素的点击 不幸的是 由于 React 有自己的影子 DOM 所以天真的方法使用document querySelector 不工作 我遇到了一些需要修改 React 组件本
  • HTML 离线应用程序缓存,列出下载的文件

    作为我正在构建的离线 Web 应用程序的加载屏幕的一部分 使用缓存清单 http developer apple com library safari documentation iPhone Conceptual SafariJSData
  • 如何在类似控制台的环境中运行 JavaScript?

    我正在尝试遵循这里的示例 http eloquentjavascript net chapter2 html http eloquentjavascript net chapter2 html and print blah 在浏览器中运行时
  • 为什么 jquery 没有检测到单选按钮未被选中的情况? [复制]

    这个问题在这里已经有答案了 可能的重复 JQuery radioButton change 在取消选择期间不会触发 https stackoverflow com questions 5176803 jquery radiobutton c
  • 如何获取浏览器视口中当前显示的内容

    如何获取当前正在显示长文档的哪一部分的指示 例如 如果我的 html 包含 1 000 行 1 2 3 9991000 并且用户位于显示第 500 行的中间附近 那么我想得到 500 n501 n502 或类似的内容 显然 大多数场景都会比
  • 在 React.js 中编辑丰富的数据结构

    我正在尝试为数据结构创建一个简单的基于网格的编辑器 但我在使用 React js 时遇到了一些概念问题 他们的文档对此没有太大帮助 所以我希望这里有人可以提供帮助 首先 将状态从外部组件传输到内部组件的正确方法是什么 是否有可能将内部组件中
  • fullCalendar 未显示正确的结束日期

    我正在看调试页面 http jsbin com wukofacaxu edit js outputFullCalendar 官方网站的 我想安排一个活动时间为 22 09 2015 至 30 09 2015 dd mm yyyy 但它只显示

随机推荐

  • 跟踪App Store下载源

    有没有办法追踪App Store的下载源 例如 如果我在应用程序的 Facebook 页面上发布链接 我可以知道有多少人通过该链接下载了我的应用程序吗 感谢您的回答 是的 您可以使用 Apple App Analytics 的活动链接来跟踪
  • 如何将 UILongPressGestureRecognizer 添加到 UITextField?

    我正在尝试添加UILongPressGestureRecognizer到页面上的 UITextField 之一 但长按 UiTextField 时不会调用选择器方法 我将它添加到 UItextField 但当我长按 TextField 但在
  • 如何在多个游戏对象上统一使用相同的脚本?

    我有一个脚本CoinFill这会形成一个径向进度条 当 FillAmount 1 时 我想将该特定图像重置为零 我希望能够将其用于多个GameObjects 问题是当第一次FillAmount 1 速度较快的便士 可以点击镍币 可能是50
  • std::atomic 的正确用法

    这是使用 std atomic 的正确方法吗 我有一个Logger在命名空间级别声明称为LOGGER class Logger public Logger Logger bool Init std wstring logFileLocati
  • Chrome 开发者工具:I CSS 覆盖未使用的字节

    我正在尝试从网页中删除未使用的 CSS 在 Chrome 开发者工具中 gt 单击垂直三个点 gt 更多工具 gt 覆盖范围 单击任意 CSS 我们看到一份报告 其中红色表示未使用的 CSS 绿色表示已使用的 CSS 有没有一种方法可以根据
  • 向 UIAppFonts 添加多种字体会相互覆盖

    我正在尝试通过以下方式向 iPhone 应用程序添加一些自定义字体UIAppFonts in Info plist 我可以通过代码访问这两个字体名称 即MyFont Bold and MyFont Medium 我的问题是最后一个字体UIA
  • 如何验证nestjs中的查询参数

    哟 我有 Nestjs 的商店应用程序 我需要验证 mongo id 这是通过查询传递的 问题是我也传递和搜索查询 我编写了验证所有值的管道 并排除了此搜索查询 Injectable export class ValidationObjec
  • Ada 通用平均函数

    我有一个函数可以对记录数组中的某个数值进行平均 该值可以是自然类型增量 也可以是枚举类型增量 我让它正确地总结了这些值 但我的问题是 如何将数组的长度转换为通用类型 以便它可以除以整数和增量类型数字 在记录数组上使用 长度 属性 这样做的优
  • Android 上的 Google 地图无法加载

    我的应用程序中有一个可用的 Google 地图 但我必须开始使用另一台计算机 所以我用 Android Studio 等在这台新电脑上下载了我的源代码 现在 我无法再让 Android 地图显示了 我在 Google Console 中从我
  • Hibernate 从多个表中计数

    我想编写一个 HQL 查询 从不同列的 4 个表中返回特定计数 我有4张桌子 Tab1 Tab2 Tab3 and Tab4 我想得到这样的东西 Tab1 Tab2 Tab3 Tab4 1 13 7 0 所有记录在Tab 有自己的id an
  • 如何在用 C 编写的类似 FORTH 的语言解释器中实现 LOOP

    我正在用 C 编写一种简单的基于堆栈的语言 并且想知道应该如何实现某种类型的循环结构和 或前瞻符号 由于此页面的代码有点长 超过 200 行 所以我将其放入GitHub 存储库 https github com tekknolagi Sta
  • 如何在 C# 中混合两个视频流?

    过去 我使用 Managed Direct X 和 DirectShow Net 将视频播放到简单网格上的纹理 然后 我使用相同的技术将多个视频源组合成单个纹理 并使用通过纹理公开的各种功能将这些流混合在一起 这是 2 年前的一个非常有趣的
  • Java 中 int 与 float 算术效率

    我正在编写一个应用程序 它使用 Dijkstra 算法来查找图中的最小路径 图中节点和边的权重为float数字 因此该算法对浮点数进行许多算术运算 如果我将所有重量转换为int是 Java中int算术运算比float算术运算更快吗 我尝试编
  • 用于添加应用程序的 Windows Azure Graph API

    我正在尝试使用 Windows Azure Active Directory WAAD 图形 API 将应用程序添加到我的 WAAD 租户 我已成功使用 API 创建用户 使用 API 添加应用程序时 我收到授权异常 Authorizati
  • 基于视图的 NSOutlineView 标题单元格字体问题

    我目前正在尝试在我的 Cocoa 应用程序中使用新的基于视图的 NSOutlineView 由于我没有使用绑定 因此我在控制器中实现了所有必需的委托和数据源方法 在界面生成器中 我添加了一个 NSOutlineView 其突出显示设置为来源
  • 如何使用 MATLAB 找到两个斑点(轮廓/闭合曲线)之间的最短路径?

    bwlabel可用于获取图像中断开连接的对象 L Ne bwlabel image 如何计算两条不连通的闭合曲线之间的最短路径 有实际的 非理论的 解决方案吗 建议1 尝试提取要连接的对象的周边像素的坐标 并将它们用作图形中的节点 然后使用
  • Xamarin iOS 构建的 DevOps CI 错误 在钥匙串中找不到有效的 iOS 代码签名密钥

    我已经为我的 Xamarin iOS 应用程序创建了 YAML 构建配置 并提供了证书文件 p12 和 mobileprovision 但是当管道运行时 它在应用程序的构建步骤中失败 证书安装通过 Note具有相同证书文件和密码的构建在应用
  • Keras 中的回调异常 - Tensorflow 2.0 - Python

    以下代码在与 Keras 打包的 MNIST 数据上运行顺序 Keras 模型 非常简单 在运行以下代码时 我遇到异常 该代码很容易重现 import tensorflow as tf class myCallback tf keras c
  • 有没有办法获取 Visual Studio 使用的构建命令行?

    我想从命令行进行构建 但我想从 Visual Studio 2012 获取确切的命令行语法 这样我就不必手动找出所有标志 导入和其他参数 有没有办法让 Visual Studio 显示这些信息 在 Visual Studio 中生成项目或解
  • 使用 Service Worker 处理离线时的文件上传

    我们有一个 Web 应用程序 使用 AngularJS 构建 我们也逐渐添加 PWA 功能 服务工作线程 可启动项 通知等 我们的网络应用程序的功能之一是能够在离线状态下完成网络表单 目前 我们在离线时将数据存储在 IndexedDB 中