【PWA学习】6. 使用 Service Worker 进行后台同步

news2025/1/19 14:18:51

引言

你一定遇到过类似这样的场景:

当用手机填写完一张信息表单点击"提交"时,恰好手机网络很差或没有网络,这时候只能盯着手机看着旋转的小圆圈。经过长时间等待后依然没有结果,这时候关闭浏览器,请求也被终止

上面的场景其实暴露了两个问题:

  • 普通的页面发起的请求会随着浏览器进程的结束或者 Tab 页面的关闭而终止
  • 无网环境下,没有一种机制能“维持”住该请求,以待有网情况下再进行请求

然而,Service Worker 的后台同步功能规避了这些缺陷

示例

下面就让我们先来了解下后台同步(Background Sync)的工作原理

工作原理

后台同步应该算是 Service Worker 相关功能(API)中比较易于理解与使用的一个

其大致的流程如下:

后台同步流程
  1. 你需要在 Service Worker 中监听 sync 事件
  2. 在浏览器中发起后台同步 sync(图中第一步)
  3. 之后会触发 Service Worker 的 sync 事件,在该监听的回调中进行操作,例如向后端发起请求(图中第二步)
  4. 可以在 Service Worker 中对服务端返回的数据进行处理

由于 Service Worker 在用户关闭该网站后仍可以运行,因此该流程名为“后台同步”实在是非常贴切

使用

client 触发 sync 事件

  • public/index.js
// background sync 基础版
navigator.serviceWorker.ready.then(function (registration) {
  var tag = 'sample_sync'

  document.getElementById('js-sync-btn').addEventListener('click', function () {
    registration.sync
      .register(tag)
      .then(function () {
        console.log('后台同步已触发', tag)
      })
      .catch(function (err) {
        console.log('后台同步触发失败', err)
      })
  })
})

由于后台同步功能需要在 Service Worker 注册完成后触发,因此较好的一个方式是在navigator.serviceWorker.ready之后绑定相关操作。例如上面的代码中,我们在 ready 后绑定了按钮的点击事件。当按钮被点击后,会使用registration.sync.register()方法来触发 Service Worker 的 sync 事件

registration.sync返回一个SyncManager对象,其上包含registergetTags两个方法

register()方法可以注册一个后台同步事件,其中接收的参数tag用于作为这个后台同步的唯一标识

当然,如果想要代码更健壮的话,我们还需要在调用前进行特性检测:

  • public/index.js
if ('serviceWorker' in navigator && 'SyncManager' in window) {
    // ……
}

在 Service Worker 中监听 sync 事件

当 client 触发了sync事件后,剩下的就交给 Service Worker。理论上此时就不需要 client(前端站点)参与了。例如另一个经典场景:用户离开时页面(unload)时在 client 端触发 sync 事件,剩下的操作交给 Service Worker,Service Worker 的操作可以在离开页面后正常进行

像添加 fetch 和 push 事件监听那样,我们可以为 Service Worker 添加 sync 事件的监听:

  • public/sw.js
self.addEventListener('sync', function (e) {
    // ……
});

在 sync 事件的 event 对象上可以取到 tag 值,该值就是我们在上一节注册 sync 时的唯一标识。通过这个 tag 就可以区分出不同的后台同步事件。例如,当该值为 'sample_sync' 时我们向后端发送一个请求:

  • public/sw.js
self.addEventListener('sync', function (e) {
    console.log(`service worker需要进行后台同步,tag: ${e.tag}`);
    var init = {
        method: 'GET'
    };
    if (e.tag === 'sample_sync') {
        var request = new Request(`sync?name=AlienZHOU`, init);
        e.waitUntil(
            fetch(request).then(function (response) {
                response.json().then(console.log.bind(console));
                return response;
            })
        );
    }
});

这里我通过e.tag来判断client触发的不同 sync 事件,并在监听到 tag 为 'sample_sync' 的 sync 事件后,构建了一个 request 对象,使用 fetch API 来进行后端请求。

需要特别注意的是,fetch 请求一定要放在e.waitUntil()内。因为我们要保证“后台同步”,将 Promise 对象放在e.waitUntil()内可以确保在用户离开我们的网站后,Service Worker 会持续在后台运行,等待该请求完成

完善后端服务

实际上,经过上面两小节,我们的大致工作已经完成。不过还缺少一个小环节:我们的 KOA 服务器上还没有 sync 路由和接口。添加一下,以保证 demo 可以正常运行:

router.get('/sync', async (ctx, next) => {
    console.log(`Hello ${ctx.request.query.name}, I have received your msg`);
    ctx.response.body = {
        status: 0
    };
});

Demo 效果展示

下面就来看一下这个 demo 的运行效果:

可以看到,在网络环境正常的情况下,点击“同步”按钮会立即触发 Service Worker 中的 sync 事件监听,并向服务端发送请求;而在断网情况下,点击“同步”按钮,控制台虽然显示注册了同步事件,但是并不会触发 Service Worker 的 sync 监听回调,指到恢复网络连接,才会在后台(Service Worker)中进行相关处理

下面再来看一下触发 sync 事件后,关闭网站的效果:

可以看到,即使在关闭网站后再重新连接网络,服务端依然可以收到来自客户端的请求(说明 Service Worker 在后台进行了相关处理)

在后台同步时获取所需的数据

其实上一节结束,我们就已经可以了解最基础的后台同步功能了。而这部分则会进一步探讨后台同步中的一个重要问题:如何在后台同步时获取并发送 client 中的数据?

例如在我们的上一个 Demo 中,用户的姓名 name 是硬编码在 Service Worker 中的,而实际上我们希望能在页面上提供一个输入框,将用户的输入内容在后台同步中进行发送。

实现的方式有两种:使用 postMessage 或使用 indexedDB

使用 postMessage

我们知道,在浏览器主线程与 Web Worker 线程之间可以通过 postMessage 来进行通信。因此,我们也可以使用这个方法来向 Service Worker “传输”数据

大致思路如下:

  1. client 触发 sync 事件
  2. 在 sync 注册完成后,使用 postMessage 和 Service Worker 通信
  3. 在 Service Worker 的 sync 事件回调中等待 message 事件的消息
  4. 收到 message 事件的消息后,将其中的信息提交到服务端
  • public/index.js
// 使用postMessage来传输sync数据
navigator.serviceWorker.ready.then(function (registration) {
    var tag = 'sample_sync_event';

    document.getElementById('js-sync-event-btn').addEventListener('click', function () {
        registration.sync.register(tag).then(function () {
            console.log('后台同步已触发', tag);

            // 使用postMessage进行数据通信
            var inputValue = document.querySelector('#js-search-input').value;
            var msg = JSON.stringify({type: 'bgsync', msg: {name: inputValue}});
            navigator.serviceWorker.controller.postMessage(msg);
        }).catch(function (err) {
            console.log('后台同步触发失败', err);
        });
    });
});

registration.sync.register完成后,调用navigator.serviceWorker.controller.postMessage来向 Service Worker Post 数据

为了提高代码的可维护性,我在sw.js中创建了一个SimpleEvent类,可以把它看做一个最简单的 EventBus,用来解耦 Service Worker 的 message 事件和 sync 事件

  • public/sw.js
class SimpleEvent {
    constructor() {
        this.listenrs = {};
    }

    once(tag, cb) {
        this.listenrs[tag] || (this.listenrs[tag] = []);
        this.listenrs[tag].push(cb);
    }

    trigger(tag, data) {
        this.listenrs[tag] = this.listenrs[tag] || [];
        let listenr;
        while (listenr = this.listenrs[tag].shift()) {
            listenr(data)
        }
    }
}

在 message 事件中监听 client 发来的消息,并通过 SimpleEvent 通知所有监听者

  • public/sw.js
const simpleEvent = new SimpleEvent();
self.addEventListener('message', function (e) {
    var data = JSON.parse(e.data);
    var type = data.type;
    var msg = data.msg;
    console.log(`service worker收到消息 type:${type};msg:${JSON.stringify(msg)}`);

    simpleEvent.trigger(type, msg);
});

在 sync 事件中,使用 SimpleEvent 监听 bgsync 来获取数据,然后再调用 fetch 方法。注意,由于e.waitUntil()需要接收 Promise 作为参数,因此需要对SimpleEvent.once进行 Promisfy

  • public/sw.js
self.addEventListener('sync', function (e) {
    if (e.tag === xxx) {
        // ……
    }

    // sample_sync_event同步事件,使用postMessage来进行数据通信
    else if (e.tag === 'sample_sync_event') {
        // 将SimpleEvent.once封装为Promise调用
        let msgPromise = new Promise(function (resolve, reject) {
            // 监听message事件中触发的事件通知
            simpleEvent.once('bgsync', function (data) {
                resolve(data);
            });
            // 五秒超时
            setTimeout(resolve, 5000);
        });

        e.waitUntil(
            msgPromise.then(function (data) {
                var name = data && data.name ? data.name : 'anonymous';
                var request = new Request(`sync?name=${name}`, init);
                return fetch(request)
            }).then(function (response) {
                response.json().then(console.log.bind(console));
                return response;
            })
        );
    }
});

进行后台同步时,使用 postMessage 来实现 client 向 Service Worker 的传输数据,是一个不错的方法

使用indexedDB

在 client 与 Servcie Worker 之间同步数据,还有一个可行的思路:client 先将数据存在某处,待 Servcie Worker 需要时再读取使用即可

为此需要找一个存数据的地方。你第一个想到的可能是 localStorage

然而,在最开始介绍 Service Worker 时提到过,为了保证性能,实现部分操作的非阻塞,在 Service Worker 中我们经常会碰到异步操作(因此大多数 API 都是 Promise 形式的)。那么像 localStorage 这样的同步 API 会变成异步化么?答案很简单:不会,并且 localStorage 在 Servcie Worker 中无法调用

不过我们还另一个强大的数据存储方式——indexedDB。它是可以在 Service Worker 中使用的

首先,需要一个方法用于连接数据库并创建相应的 store:

  • public/index.js
function openStore(storeName) {
    return new Promise(function (resolve, reject) {
        if (!('indexedDB' in window)) {
            reject('don\'t support indexedDB');
        }
        var request = indexedDB.open('PWA_DB', 1);
        request.onerror = function(e) {
            console.log('连接数据库失败');
            reject(e);
        }
        request.onsuccess = function(e) {
            console.log('连接数据库成功');
            resolve(e.target.result);
        }
        request.onupgradeneeded = function (e) {
            console.log('数据库版本升级');
            var db = e.srcElement.result;
            if (e.oldVersion === 0) {
                if (!db.objectStoreNames.contains(storeName)) {
                    var store = db.createObjectStore(storeName, {
                        keyPath: 'tag'
                    });
                    store.createIndex(storeName + 'Index', 'tag', {unique: false});
                    console.log('创建索引成功');
                }
            }
        }
    });
}

然后,在navigator.serviceWorker.ready中打开该数据库连接,并在点击按钮时,先将数据存入 indexedDB,再注册 sync:

  • public/index.js
navigator.serviceWorker.ready.then(function (registration) {
    return Promise.all([
        openStore(STORE_NAME),
        registration
    ]);
}).then(function (result) {
    var db = result[0];
    var registration = result[1];
    var tag = 'sample_sync_db';

    document.getElementById('js-sync-db-btn').addEventListener('click', function () {
        // 将数据存储进indexedDB
        var inputValue = document.querySelector('#js-search-input').value;
        var tx = db.transaction(STORE_NAME, 'readwrite');
        var store = tx.objectStore(STORE_NAME);
        var item = {
            tag: tag,
            name: inputValue
        };
        store.put(item);

        registration.sync.register(tag).then(function () {
            console.log('后台同步已触发', tag);
        }).catch(function (err) {
            console.log('后台同步触发失败', err);
        });
    });
});

同样的,在 Service Worker 中也需要相应的数据库连接方法:

  • public/sw.js
function openStore(storeName) {
    return new Promise(function (resolve, reject) {
        var request = indexedDB.open('PWA_DB', 1);
        request.onerror = function(e) {
            console.log('连接数据库失败');
            reject(e);
        }
        request.onsuccess = function(e) {
            console.log('连接数据库成功');
            resolve(e.target.result);
        }
    });
}

并且在 sync 事件的回调中,get 到 indexedDB 中对应的数据,最后再向后端发送请求:

  • public/index.js
self.addEventListener('sync', function (e) {
    if (e.tag === xxx) {
        // ……
    }
    else if (e.tag === yyy) {
        // ……
    }
    
    // sample_sync_db同步事件,使用indexedDB来获取需要同步的数据
    else if (e.tag === 'sample_sync_db') {
        // 将数据库查询封装为Promise类型的请求
        var dbQueryPromise = new Promise(function (resolve, reject) {
            var STORE_NAME = 'SyncData';
            // 连接indexedDB
            openStore(e.tag).then(function (db) {
                try {
                    // 创建事务进行数据库查询
                    var tx = db.transaction(STORE_NAME, 'readonly');
                    var store = tx.objectStore(STORE_NAME);
                    var dbRequest = store.get(e.tag);
                    dbRequest.onsuccess = function (e) {
                        resolve(e.target.result);
                    };
                    dbRequest.onerror = function (err) {
                        reject(err);
                    };
                }
                catch (err) {
                    reject(err);
                }
            });
        });

        e.waitUntil(
            // 通过数据库查询获取需要同步的数据
            dbQueryPromise.then(function (data) {
                console.log(data);
                var name = data && data.name ? data.name : 'anonymous';
                var request = new Request(`sync?name=${name}`, init);
                return fetch(request)
            }).then(function (response) {
                response.json().then(console.log.bind(console));
                return response;
            })
        );
    }
});

相比于 postMessage,使用 indexedDB 的方案要更复杂一点。它比较适用于一些需要数据持久化的场景

兼容性

依照惯例,我们还是来简单看一下文中相关功能的兼容性

先是 Background Sync:

Background Sync 兼容性

然后是 indexedDB:

indexedDB 兼容性

相较于 Background Sync 还是有着不错的兼容性的。而且在 safari(包括iOS safari)中也得到了支持

本章分支: sync

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/155339.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

红外传感器使用

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录前言一、红外传感器?二、使用步骤1.确保驱动已经安装2.安装GPIO工具3.安装GPIO的Python支持4.Python3代码5.测试结果总结前言 最近在做一个项目需要用到…

Linux命令学习

1、linux目录结构 linux目录结构是一个树状结构 当我们直接打开ubuntu的控制台,进入的是 home 目录下的创建的用户,这里是真正的 家 目录 或者在安装 ssh 服务器之后可以直接通过 windows 命令行 访问 ubuntu 的ssh服务器,进入的是 home 目录…

【规范】我们是怎么做MySQL数据库安全管理的?

一、背景说明 MySQL作为数据库管理系统,里面保存企业的重要业务数据,因此保证数据库的安全性非常重要,如何保证数据库的安全性呢?用户和用户权限管理是一个很重要的方面。 MySQL数据库具有非常高的安全性,为我们提供…

Vue 2 即将成为过去

自从 2020 年 9 月 18 日 Vue 3 正式发布以来,已经有两年多时间了,终于在 2022 年 2 月 7 日 Vue 作者发布了一则消息:Vue 3 将成为新的默认版本。与此同时,Vue 相关官方周边的核心库 latest 发布标签将指向其 Vue 3 的兼容版本。…

从0到1完成一个Vue后台管理项目(二十一、网上地图资源、树形控件及路由权限分析、路由守卫)

往期 从0到1完成一个Vue后台管理项目(一、创建项目) 从0到1完成一个Vue后台管理项目(二、使用element-ui) 从0到1完成一个Vue后台管理项目(三、使用SCSS/LESS,安装图标库) 从0到1完成一个Vu…

JAVA SE 详解类和对象

类和对象 面向对象的初步认知 什么是面向对象 Java是一门纯面向对象的语言(Object Oriented Program,简称OOP),在面向对象的世界里,一切皆为对象。 面 向对象是解决问题的一种思想,主要依靠对象之间的交互完成一件事情。用面向…

ArcGIS基础实验操作100例--实验79分区统计降雨量

本实验专栏参考自汤国安教授《地理信息系统基础实验操作100例》一书 实验平台:ArcGIS 10.6 实验数据:请访问实验1(传送门) 高级编辑篇--实验79 分区统计降雨量 目录 一、实验背景 二、实验数据 三、实验步骤 (1&am…

【VUE2-02】vue2的指令和vue2的缺点

文章目录一、vue条件渲染 v-if二、vue循环渲染 v-for三、vue的事件 v-on四、vue的双向绑定 v-model五、VUE2的缺点5.1 vue底层原理解决方案在上节 【VUE2-01】vue2的起步,中写hello world!例子的时候使用了一个指令 v-bind绑定元素属性一、vue条件渲染 v-if v-if控…

米筐量化终端是什么?

米筐量化终端大家应该也能想象到是应用的终端,是系统执行的终端环节,如果是用在量化方面,那它就是策略定制的终端,是方便投资者输入量化策略执行出来发最终优质目的,精确到细分股票的账户成交量,股价以及融…

java8 第七章-----多线程

7.1、线程基本知识 进程与线程: 进程(Process)是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构…

cobaltstrike的安装与基础使用

数据来源 本文仅用于信息安全学习,请遵守相关法律法规,严禁用于非法途径。若观众因此作出任何危害网络安全的行为,后果自负,与本人无关。 Cobalt Strike是什么? Cobalt Strike(简称CS)是一款团队作战渗…

Python学习笔记——列表

Python列表函数&方法函数:len(list):列表元素个数max(list):返回列表元素最大值min(list):返回列表元素最小值list(seq):将元组转换为列表方法list.append(obj):在列表末尾添加新的对象list.count(obj)…

字典树总结

字典树 一、概念 字典树 (Trie) 是一种用于实现字符串快速存储和检索的多叉树结构; Trie 的每个节点都拥有若干个字符指针,若在插入或检索字符串时扫描到一个字符 ccc ,就沿着当前节点的 ccc 字符指针,走向该指针指向的节&…

支持向量机(SVM)入门(六,解决遗留问题)

但也出现了如下结果,看来任务没完没了(svm深似海?),还得下功夫:前面我们的支持向量机到此便止步了,一晃两年,这几天有空,顺便研究了一下,问题有进展&#xff…

基于yolov5-v7.0开发构建汽车车损实例分割检测识别分析系统

在之前的文章中我们已经做了很多基于yolov5完成实例分割的项目,感兴趣的话可以自行移步阅读:《基于YOLOv5-v7.0的药片污染、缺损裂痕实例分割检测识别分析系统》《基于yolov5-v7.0开发构建裸土实例分割检测识别模型》《基于yolov5-v7.0开发实践实例分割模…

NSThead的进阶使用和简单探讨

概述 NSThread类是一个继承于NSObjct类的轻量级类。一个NSThread对象就代表一个线程。它需要管理线程的生命周期、同步、加锁等问题,因此会产生一定的性能开销。 使用NSThread类可以在特定的线程中被调用某个OC方法。当需要执行一个冗长的任务,并且不想…

文件minio进阶 分页查询

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录前言一、minio使用二、代码1.mino版本是最新的,那么pom中也要最新的2. 部分代码总结前言 就是现在通过minio管理文件,然后不需要其他信息,所以我也就没有用传统方式…

探究视频标注如何支持自动驾驶汽车

自动驾驶汽车技术有望使我们的道路更安全,同时提高运输和交付服务的效率。安全性和可靠性是自动驾驶汽车广泛采用所必需的关键因素。为了保证汽车在现实复杂的交通条件下导航能够有效运行,必须使用标注数据来训练基于计算机视觉的模型,这些数…

verilog学习笔记- 10)按键控制 LED 灯实验

目录 简介: 实验任务: 硬件设计: 程序设计: 下载验证 : 总结与反思: 简介: 按键开关是一种电子开关,属于电子元器件类。我们的开发板上有两种按键开关:第一种是本实…

NeRF制作数据集最后的踩坑

之前对数据集渲染的时候一直出现模糊的问题: 学长说可能是摄像机和poses没有对应。但是为什么会这样呢 后来我发现,确实在制作的过程中,某一步出了问题,导致下标的数字,跟理想的不一样: 原来的12&#xf…