🎊🎊🎊深入 ServiceWorker,消息推送,后台同步,一网打尽!

简介: 上一章讲到了ServiceWorker的基础使用,但是它的功能不仅仅只有这些,还有很多很多,比如消息推送,后台同步,甚至还有WebRTC,这一章我们来进阶ServiceWorker。

上一章讲到了ServiceWorker的基础使用,但是它的功能不仅仅只有这些,还有很多很多,比如消息推送,后台同步,甚至还有WebRTC,这一章我们来进阶ServiceWorker

前期准备

在开始之前,我们先做一下前期的准备,还是使用我上一篇的例子,但是我们需要删除service-worker.js里面的缓存代码,因为这一章用不上,新看到的小伙伴可以直接用我下面的代码也行:

  • index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>
    if ('serviceWorker' in navigator) {
    
    
        // 在调试阶段,如果发现 service worker 没有更新,可以先用下面的代码强制更新
        // navigator.serviceWorker.getRegistrations().then((registrations) => {
    
    
        //     for (let registration of registrations) {
    
    
        //         registration.unregister()
        //     }
        // });

        navigator.serviceWorker.register('/service-worker.js', {
    
    
            scope: '/'
        }).then((registration) => {
    
    
            // 先空着
        });
    }
</script>
</body>
</html>
  • service-worker.js里面暂时不写任何代码

消息推送

使用ServiceWorker是一定要了解消息推送的,因为它是ServiceWorker的一个重要功能,它可以让你的网站在用户不在的时候,也能够接收到消息,这对于一些即时通讯的网站来说,是非常重要的。

按照正常的消息推送流程,我们需要经历下面几个步骤:

  1. 注册ServiceWorker获得registration对象
  2. 通过registration对象获得PushManager对象
  3. 通过PushManager对象订阅消息推送,获得subscription对象
  4. subscription对象发送给服务器,由服务器保存
  5. 服务器通过subscription对象推送消息
  6. ServiceWorker通过监听push事件,获得推送的消息
  7. ServiceWorker通过showNotification方法,显示消息

在讲推送流程之前,我们先来体验一下消息推送的快感。

1. 体验消息推送

啥也不多说,直接上代码:

if ('serviceWorker' in navigator) {
   
   
    navigator.serviceWorker.register('/service-worker.js', {
   
   
        scope: '/'
    }).then((registration) => {
   
   
        // 直接调用showNotification方法,显示消息
        registration.showNotification('Hello World');
    });
}

image.png

上面的方法直接让我们在window上显示了一个消息,但是这个消息是没有任何交互的,我们可以通过Notification对象来实现交互:

// 省略注册 ServiceWorker 的代码

self.showNotification('Hello World!', {
   
   
    body: 'This is a notification!',
    icon: 'https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/703a0cee8b0b494eadd27adc61883956~tplv-k3u1fbpfcp-watermark.image?',
    actions: [{
   
   
        action: 'yes',
        title: 'Yes'
    }, {
   
   
        action: 'no',
        title: 'No'
    }]
});

image.png

这样我们就可以在消息上添加交互了,但是这个交互是没有任何效果的,我们需要通过监听notificationclick事件来实现交互:

// service-worker.js

// 监听 notificationclick 事件
self.addEventListener('notificationclick', (event) => {
   
   
    // 判断点击的是哪个按钮
    if (event.action === 'yes') {
   
   
        console.log('yes');
    } else if (event.action === 'no') {
   
   
        console.log('no');
    }
});

这个时候我们就可以在控制台看到点击的是哪个按钮了,接下来我们就来认识一下showNotification方法。

2. showNotification

showNotification方法是ServiceWorkerRegistration对象的一个方法,它可以用来显示消息,直接看函数签名:


interface ServiceWorkerRegistration extends EventTarget {
   
   
    /**
     * 显示消息
     * @param title 消息标题
     * @param options 消息配置
     *        options.body 消息内容
     *        options.icon 消息图标
     *        options.tag 消息标签
     *        options.data 消息数据
     *        options.actions 消息交互按钮
     * @returns Promise
     */
    showNotification(title: string, options?: NotificationOptions): Promise<Notification>;
}

上面的options参数并没有完全列出来,可以参考MDN

通过上述的实验代码,不知道大家有没有发现一个问题,就是showNotification方法调用出来的消息是直接显示在window上的,而不是显示在浏览器的通知栏上。

这是因为消息推送是浏览器行为,作用在window系统上,而不是作用在网页上。

通过这个案例,有没有产生一个想法,就是大家在写业务代码的时候,遇到消息通知都是自己手写一个弹窗消息,切换个网页就看不到了;

这个时候我们使用showNotification方法,直接推送到window上,虽然样式不能自定义,但是是不是逼格更高呢?

而我们的showNotification方法,其实是通过Notification对象来实现的,点到为止,继续学习ServiceWorker

消息推送流程

为什么要先体验消息推送的效果呢,再来讲消息推送的流程呢?

因为不想打击大家的热情,消息推送因为会被墙,所以在国内没有多少人使用,所以我们先体验一下,再来讲消息推送的流程。

现在我们直接上代码来查看整体流程是怎么样的:

  • index.html
if ('serviceWorker' in navigator) {
   
   
    // 1. 注册`ServiceWorker`获得`registration`对象
    // 2. 通过`registration`对象获得`PushManager`对象
    // 3. 通过`PushManager`对象订阅消息推送,获得`subscription`对象
    // 4. 将`subscription`对象发送给服务器,由服务器保存
    // 5. 服务器通过`subscription`对象推送消息
    // 6. `ServiceWorker`通过监听`push`事件,获得推送的消息
    // 7. `ServiceWorker`通过`showNotification`方法,显示消息

    // 1. 注册`ServiceWorker`获得`registration`对象
    navigator.serviceWorker.register('/service-worker.js', {
   
   
        scope: '/'
    }).then((registration) => {
   
   
        // 2. 通过`registration`对象获得`PushManager`对象
        const pushManager = registration.pushManager;

        // 3. 通过`PushManager`对象订阅消息推送,获得`subscription`对象
        pushManager.subscribe({
   
   
            userVisibleOnly: true,
            applicationServerKey: ''
        }).then((subscription) => {
   
   
            // 4. 将`subscription`对象发送给服务器,由服务器保存
            console.log(subscription);
        });

    });
}

前面的四步都是在客户端完成的,这里的关键在于第三步,通过PushManager对象订阅消息推送,获得subscription对象。

我们先来看一下subscribe方法的参数和简介:

interface PushSubscriptionOptions {
   
   
    userVisibleOnly?: boolean;
    applicationServerKey?: BufferSource;
}

interface PushManager {
   
   
    /**
     * 订阅消息推送
     * @param options
     *        options.userVisibleOnly: 是否只有用户可见
     *        options.applicationServerKey: 服务器公钥
     * @returns {Promise<PushSubscription>}
     */
    subscribe(options?: PushSubscriptionOptions): Promise<PushSubscription>;
}

这里的options在文档上显示是可选参数,但是在部分浏览器上是必填参数,所以我们在调用subscribe方法的时候,需要传入options参数。

参数介绍:

  • userVisibleOnly:布尔值,表示返回的推送订阅将只能被用于对用户可见的消息;在部分浏览器下,如果为false,则会抛出错误。
  • applicationServerKey:服务器公钥,用于加密推送消息,这个参数在部分浏览器下是必填参数。

这里的重点就是applicationServerKey,这个是由我们自己生成的,接下来我们来看一下如何生成。

生成服务器公钥

我们可以通过web-push这个库来生成服务器公钥,这个库是Google开发的,旨意是打通多端消息推送的通道,它提供了一些工具,可以帮助我们生成服务器公钥,以及发送推送消息。

web-push的库在这里:web-push

安装web-push会被墙,可以通过切换npm源来解决:

npm config set registry https://registry.npm.taobao.org

安装web-push

npm install web-push

安装完成后,我们就可以使用web-push来生成服务器公钥了:

const webpush = require('web-push');

// 生成服务器公钥
const vapidKeys = webpush.generateVAPIDKeys();

console.log(vapidKeys);

我们也可以全局安装web-push,然后通过命令行来生成服务器公钥:

npm install web-push -g

web-push generate-vapid-keys --json

image.png

上面生成的密钥对会有一个公钥和一个私钥,私钥放在服务器保存,公钥用于注册推送订阅:

pushManager.subscribe({
   
   
    userVisibleOnly: true,
    applicationServerKey: 'BHXrxJPYpQSwGMwcN-HprCaU_Po9POIUvqWFLFq9UUNHP5SNJKxk_Io59y8_twMTOuB5SbpbcPBwHFo2kBUj7vQ'
}).then((subscription) => {
   
   
    // 4. 将`subscription`对象发送给服务器,由服务器保存
    console.log(subscription);
});

上面注册成功后,会返回一个subscription对象,这个对象就是推送订阅,我们需要将这个对象发送给服务器,由服务器保存,用于后续推送消息。

有时候注册过了如果再重复注册会报错,可以通过pushManager.getSubscription()来获取已经注册的subscription对象。

pushManager.getSubscription().then((subscription) => {
   
   
    if (subscription) {
   
   
        // 这里 subscription 就是已经注册的 subscription 对象
        return;
    }

    pushManager.subscribe({
   
   
        userVisibleOnly: true,
        applicationServerKey: 'BHXrxJPYpQSwGMwcN-HprCaU_Po9POIUvqWFLFq9UUNHP5SNJKxk_Io59y8_twMTOuB5SbpbcPBwHFo2kBUj7vQ'
    }).then((subscription) => {
   
   
        console.log(subscription);
    });
});

发送推送消息

推送消息我们同样也是通过web-push来发送,它提供了一个sendNotification()方法,可以帮助我们发送推送消息。

这里我们通过node服务来写一个简单的推送消息的接口:

import express from 'express';
import webpush from 'web-push';

const vapidKeys = {
   
   
    "publicKey": "BHXrxJPYpQSwGMwcN-HprCaU_Po9POIUvqWFLFq9UUNHP5SNJKxk_Io59y8_twMTOuB5SbpbcPBwHFo2kBUj7vQ",
    "privateKey": "Yhd4XF08Efh8HNF_8RDJ9VL6pF-Gos-3KOmgyMEUSf8"
}

// 这个是需要在 google cloud platform 中创建的项目id
webpush.setGCMAPIKey('<Your GCM API Key Here>');

// 联系邮箱填自己的邮箱
webpush.setVapidDetails(
    'mailto:example@yourdomain.org',
    vapidKeys.publicKey,
    vapidKeys.privateKey
);

const app = express();

app.get('/push', (req, res) => {
   
   
    // 这里的 pushSubscription 就是上面注册成功后返回的 subscription 对象
    const pushSubscription = {
   
   
        "endpoint": "https://fcm.googleapis.com/fcm/send/cSAH1Q7Fa6s:APA91bEgYeKNXMSO1rcGAOPzt3L9fMhyjL-zSPV5JfiKwgqtbx_Q4de_8plEY_QViLnhfe6-0fUgdo7Z3Gqpml3zIBSfO6IISDYdF9kzL2h_dbZ_FE_YKbKOG70gMG_A74xwK1vsocCv",
        "keys": {
   
   
            "p256dh": "BAqZaMLZn_rtYeR7WsBLqBWG7uMiOGRyCx2uhOqm0ZaJwDdQac-ubAyRRdLXJVZDOrNe-B3mCTy3g0vHCkeyYyo",
            "auth": "fxDt8RtB92KHpQM7HetBUw"
        }
    };

    webpush.sendNotification(pushSubscription, 'Hello world')
        .then(result => {
   
   
            res.send(result);
        })
})

app.listen(1701, async () => {
   
   
    console.log('服务启动成功:http://localhost:1701');
})

这里我们需要申请一个GCMApiKey,这个是用于验证推送消息的身份,可以在FCM申请。

上面是web-push的使用,现在通过firebase注册的推送消息已经更换了架构,不在使用web-push,而是使用firebase,详情可以参考这里
firebase的使用在官网上有详细的文档,我这里只是演示订阅推送消息的过程,所以使用web-push来生成密钥对,然后通过web-push发送推送消息。

Service Worker监听推送消息

Service Worker监听推送消息,我们需要在Service Worker中注册push事件,然后在push事件中处理推送消息,也就是最开始提到的showNotification()方法。

// 注册 push 事件
self.addEventListener('push', function (event) {
   
   
    const data = event.data.json();
    self.registration.showNotification(data.title)
});

现在可以通过访问 http://localhost:1701/push 来发送推送消息了,然后在Service Worker中通过showNotification()方法来显示推送消息。

自此一整套的消息推送和接收的流程就走完了,但是为什么要订阅推送消息?为什么我自己的服务器还需要申请啥GCMApiKey

这就要说到消息推送的很多知识了,这里不宜过多的展开,有兴趣的可以自行了解。

消息推送的一些知识

我们这里需要了解的是订阅到服务器推送之间的过程倒是是什么样的,为什么我说推送消息会被墙,为什么我们推送消息会不成功?

这里我们直接看下面的流程图:

graph TD

A[订阅推送消息] --> B[FCM服务器]
B --> C1[验证不通过抛出异常]
B --> C[返回subscription对象]
C --> D[将subscription对象保存到服务器]
D --> E[服务器向FCM服务器发送推送消息]
E --> F[FCM服务器将消息直接推送给浏览器]
F --> G[浏览器接收到推送消息调用 push 回调]
F --> G1[浏览器不在线 推送消息会在浏览器上线时通过 notificationcallback 回调]
G --> H[执行 push 事件里面的回调函数]

可以看到服务和浏览器之间是没有直接的通信的,而是通过FCM服务器来进行通信的;

FCM服务器是谷歌的服务器,所以我们不管是订阅还是推送消息都需要通过谷歌的服务器,这就是为什么我们推送消息会被墙的原因;

离线推送

PC不像移动端,你的网页或者浏览器不会一直开着,所以你的网页或者浏览器不在线的时候,你的推送消息是不会被推送到你的浏览器的,这个时候你的推送消息就会被FCM服务器保存起来,等你的网页或者浏览器上线的时候,FCM服务器会通过notificationcallback回调来推送消息到你的网页或者浏览器。

self.addEventListener('notificationcallback', function (event) {
   
   
    const data = event.data.json();
    self.registration.showNotification(data.title)
});

注意:这个回调是在离线的时候有推送消息,然后在你的网页上线的时候才会触发,如果你的网页一直在线,那么这个回调是不会触发的。

ok,到这里我们已经了解了推送消息的一些知识,我们继续下面的环节。

消息同步

上面讲到了push事件的监听,现在就还剩下sync事件的监听了,sync事件是在Service Worker中注册的,当我们调用navigator.serviceWorker.ready方法后,就可以通过registration.sync.register()方法来注册sync事件了。

// 注册 sync 事件
navigator.serviceWorker.ready.then((registration) => {
   
   
    return registration.sync.register('sync');
});

这里我们注册了一个sync事件,这个事件的名字叫sync,这个名字是自己定义的,可以随便定义,但是需要注意的是,这个名字是全局唯一的,如果你注册了一个sync事件,那么下次再注册的时候,就不能再注册一个sync事件了,否则会报错。

上面注册好了之后,就需要在Service Worker中监听这个事件了:

// 监听 sync 事件
self.addEventListener('sync', (event) => {
   
   
    if (event.tag === 'sync') {
   
   
        event.waitUntil(
            // 同步数据
            syncData()
        );
    }
});

相对于push事件,sync事件的监听就简单多了,我们只需要判断一下事件的名字,然后执行对应的操作就可以了。

使用sync事件首选需要注册一个sync事件,通过registration.sync.register()方法来注册;

上面使用到navigator.serviceWorker.ready是用来确保Service Worker已经注册成功,它会返回一个不会失败的Promise对象,如果没有注册成功,那么就会一直处于pending状态。

Service Worker中监听sync事件,通过self.addEventListener('sync', callback)方法来监听,这个callback函数会接收一个event对象,通过event.tag来判断事件的名字,然后执行对应的操作。

sync的使用场景

看名字就知道,sync事件是用来同步数据的,那么我们什么时候需要同步数据呢?

sync的使用场景有很多,我下面列举几个:

  • 离线数据同步
  • 数据备份
  • 数据恢复
  • 数据同步

通常我们可以使用这个事件来通知Service Worker同步数据,或者更新缓存,继续拿我之前的百万数据的例子来说;

百万数据很大,在上一篇中我们将数据缓存下来了,但是如果数据发送了变化,那么我们就需要更新缓存,这个时候就可以使用sync事件来通知Service Worker更新缓存。

// main.js
// 请求后台查询数据是否有变化
fetch('/api/check').then((res) => {
   
   
    return res.json();
}).then((data) => {
   
   
    if (data.hasChange) {
   
   
        // 有变化,注册 sync 事件
        navigator.serviceWorker.ready.then((registration) => {
   
   
            return registration.sync.register('sync');
        });
    }
});

// service-worker.js
// 监听 sync 事件
self.addEventListener('sync', (event) => {
   
   
    if (event.tag === 'sync') {
   
   
        event.waitUntil(
            // 同步数据
            syncData()
        );
    }
});

// 同步数据
function syncData() {
   
   
    // 请求后台获取数据
    return fetch('/getData').then((res) => {
   
   
        return res.json();
    }).then((data) => {
   
   
        // 更新缓存
        caches.open('my-caches').then((cache) => {
   
   
            cache.put('/getData', new Response(JSON.stringify(data)));
        });
    });
}

上面的代码中,我们在main.js中请求后台查询数据是否有变化,如果有变化,那么就注册一个sync事件,然后在Service Worker中监听这个事件,当事件触发的时候,就会执行syncData方法,这个方法中会请求后台获取数据,然后更新缓存。

Service Worker更新

本文的最开始给了一段注释的代码,这段代码是用来更新Service Worker的,就是下面这段代码:

navigator.serviceWorker.getRegistrations().then((registrations) => {
   
   
    for (let registration of registrations) {
   
   
        registration.unregister()
    }
});

这段代码并不是用来更新Service Worker的,而是用来卸载Service Worker的,所以最开始我注释说明是开发阶段使用的,因为我们在开发阶段,经常会修改Service Worker的代码,如果不卸载Service Worker重装很有可能缓存没有清除,导致我们看到的效果和预期的不一样。

但是在生产环境中,我们肯定是不能轻易卸载Service Worker的,当然Service Worker也提供了更新的方法,我们可以通过Service Workerupdate方法来更新Service Worker

// 更新 Service Worker
navigator.serviceWorker.getRegistration().then((registration) => {
   
   
    registration.update();
});

上面的代码中,我们通过getRegistration方法获取Service Worker的注册对象,然后调用update方法来更新Service Worker

同样,是否需要更新Service Worker,也是需要我们自己来判断的,比如我们可以通过Service WorkerscriptURL属性来判断Service Worker的版本,如果版本不一致,那么就更新Service Worker

// 更新 Service Worker
navigator.serviceWorker.getRegistration().then((registration) => {
   
   
    if (registration.scriptURL !== '/service-worker.js?v=1.0.0') {
   
   
        registration.update();
    }
});

通常我们会将Service Worker的版本号放在scriptURL中,这样就可以通过scriptURL来判断Service Worker的版本了。

Service Worker缓存更新

上面说到了Service Worker的更新,我们的缓存也是需要更新的,比如我们的Service Worker更新了,那么我们的缓存也需要更新,这样才能保证我们正常的版本迭代。

缓存在上一章中,是使用了自定义的cacheName来进行缓存的,其实我们可以用版本号来进行缓存,和Service Worker保持一致,然后在更新的时候删除对应的缓存:

// 更新 Service Worker
navigator.serviceWorker.getRegistration().then((registration) => {
   
   
    if (registration.scriptURL !== '/service-worker.js?v=1.0.0') {
   
   
        registration.update();
        // 删除缓存
        caches.delete('v1.0.0');
    }
});

上面的代码中,我们在更新Service Worker的时候,也删除了缓存,这样就可以保证我们的缓存和Service Worker的版本一致了。

总结

本章几乎探索完了Service Worker的高频常用知识,以目前了解的Service Worker的知识来说,本章的内容已经足够满足大部分的需求了;

当然这只是一个起点,Service Worker的知识还有很多,而且并不是只围绕着Service Worker来讲,还有很多周边的知识;

例如本章讲到的showNotification方法,其实是Notification的API,push方法,其实是Push的API,还有很多;

本文讲的只是使用,并没有深入,光是使用这一篇文章就已经很长了,如果深入的话,那就更长了;

不多废话,记住这只是一个起点!!!

历史章节和预告

目录
相关文章
|
消息中间件 监控 NoSQL
一个支持消息推送,文件管理,在线用户监控的后台权限管理系统来了
一个支持消息推送,文件管理,在线用户监控的后台权限管理系统来了
128 0
|
开发工具 Android开发 开发者
Android P正式版即将到来:后台应用保活、消息推送的真正噩梦
1、前言 对于广大Android开发者来说,Android O(即Android 8.0)还没玩热,Andriod P(即Andriod 9.0)又要来了。
2527 0
|
iOS开发
IOS消息推送
IOS消息推送
137 0
|
JSON 数据格式 iOS开发
APNS IOS 消息推送JSON格式介绍
在开发向苹果Apns推送消息服务功能,我们需要根据Apns接受的数据格式进行推送。下面积累了我在进行apns推送时候总结的 apns服务接受的Json数据格式 示例 1: 以下负载包含哦一个简单的 aps 字典。
3475 0
|
Android开发 iOS开发
了解iOS消息推送一文就够:史上最全iOS Push技术详解
本文作者:陈裕发, 腾讯系统测试工程师,由腾讯WeTest整理发表。 1、引言 开发iOS系统中的Push推送,通常有以下3种情况: 1)在线Push:比如QQ、微信等IM界面处于前台时,聊天消息和指令都会通过IM自建的网络长连接通道推送过来,这种Pu...
3460 0
|
搜索推荐 iOS开发
iOS小技能:消息推送扩展的使用
iOS小技能:消息推送扩展的使用
561 0
iOS小技能:消息推送扩展的使用
|
PHP 数据安全/隐私保护 iOS开发
分分钟搞定IOS远程消息推送(二)
分分钟搞定IOS远程消息推送
415 0
分分钟搞定IOS远程消息推送(二)
|
存储 Android开发 数据安全/隐私保护
分分钟搞定IOS远程消息推送(一)
分分钟搞定IOS远程消息推送
256 0
分分钟搞定IOS远程消息推送(一)