DTeam 技术日志

Doer、Delivery、Dream

谈谈 Service Worker 与 PWA

小纪同学 Posted at — May 19, 2021 阅读

Service Worker 是浏览器在后台独立于网页运行的脚本。PWA(Progressive Web App)是web应用程序,但在外观和感觉上与原生app类似。 在谈Service Worker 与 PWA 之前,先简单看看什么是Web Worker。

一、 Web Worker

1. 什么是 Web Worker ?

Web Worker 是浏览器内置的线程所以可以被用来执行非阻塞事件循环的 JavaScript 代码。 js是单线程,一次只能完成一件事,如果出现一个复杂的任务,线程就会被阻塞,严重影响用户体验, Web Worker 的作用就是允许主线程创建 worker 线程,与主线程同时进行。worker 线程只需负责复杂的计算,然后把结果返回给主线程就可以了。简单的理解就是,worker 线程执行复杂计算并且页面(主线程)ui很流畅,不会被阻塞。

2. 类型

1.Dedicated Workers 【专用 Worker】 是由主进程实例化并且只能与主线程进行通信。

2.Shared Workers 【共享 Worker】可以被运行在同源的所有进程访问。

3.Service workers【服务Worker】它可以控制它关联的网页,解释且修改导航,资源的请求,以及缓存资源以让你非常灵活地控制程序在某些情况下的行为。

3. 限制

1.同源限制

分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源,一般都放在项目下。

2. DOM限制

1) Web Workers 无法访问一些非常关键的 JavaScript 特性

2) DOM(它会造成线程不安全)

3) window 对象

4) document 对象

5) parent 对象

3. 文件限制

为了安全,worker线程无法读取本地文件,它所加载的脚本必须来自网络,且需要与主线程的脚本同源。

二、Service Worker

1. 什么是Service Worker ?

MDN 的介绍: Service workers 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。这个 API 旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用采取来适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。

image1

2. 优点/缺点

优点

缺点

3. 生命周期

Service Worker 的生命周期与 web 页面完全分离。它包括以下几个阶段:

image2

1.)下载

用户首次访问service worker控制的网站或页面时,service worker会立刻被下载。浏览器会下载包含 Service Worker 的 .js 文件。

2.)安装

需要在网页进行注册来安装,安装前需要检查是否支持 serviceWorker,如果支持,每次页面加载时就调用 register(),浏览器将会判断是否已注册。 register() 方法的一个重要细节是 Service Worker 文件的位置。在本例中,可以看到 Service Worker 文件位于域的根目录,这意味着 Service Worker 范围将是这个域下的。换句话说,这个 Service Worker 将为这个域中的所有内容接收 fetch 事件。如果我们在 /example/sw/sw.js 注册 Service Worker 文件,那么 Service Worker 只会看到以 /example/ 开头的页面的 fetch 事件(例如 /example/page1/、/example/page2/)。

if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    navigator.serviceWorker.register('/sw/sw.js').then(function(registration) {
      // 注册成功
      console.log('ServiceWorker registration successful with scope: ', registration.scope);
    }, function(err) {
      // 注册失败
      console.log('ServiceWorker registration failed: ', err);
    });
  });
}

注册成功后,install 事件会被触发,将会调用caches.open() 和我们想要的缓存名称, 之后调用 cache.addAll() 并传入文件数组。 这是一个promise 链( caches.open() 和 cache.addAll() )。 event.waitUntil() 方法接受一个promise,并使用它来知道安装需要多长时间,以及它是否成功。 如果成功缓存了所有文件,那么将安装 Service Worker。如果其中的一个文件下载失败,那么安装步骤将失败。如果缓存文件列表过长,将会增大失败的几率。

var CACHE_NAME = 'my-cache';
var urlsToCache = [
  '/',
  '/styles/main.css',
  '/script/main.js'
];

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});

3.)激活

接下来就是进入激活状态:Activate。 在这个状态可以更新 Service Worker。

  1. 用户导航至站点时,浏览器会尝试在后台重新下载定义 Service Worker 的脚本文件。 如果 Service Worker 文件与其当前所用文件存在字节差异,则将其视为新 Service Worker。
  2. 新 Service Worker 将会启动,且将会触发 install 事件。
  3. 旧 Service Worker 仍控制着当前页面,因此新 Service Worker 将进入 waiting 状态。
  4. 当网站上当前打开的页面关闭时,旧 Service Worker 将会被终止,新 Service Worker 将会取得控制权。
  5. 新 Service Worker 取得控制权后,将会触发其 activate 事件。
self.addEventListener('activate', function(event) {
var cacheAllowlist = ['pages-cache-v1', 'blog-posts-cache-v1'];
event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheAllowlist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

4. 缓存和返回请求

策略

在安装 Service Worker 且用户转至其他页面或刷新当前页面后,Service Worker 将开始接收 fetch 事件。下面是缓存优先的策略: 首先监听浏览器 fetch 事件,拦截原本的请求。 检查 cache 中是否存在将要请求的资源,有则返回缓存。 然后远程请求资源,将资源缓存后返回。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
     
        if (response) {
          return response;
        }
        var fetchRequest = event.request.clone();

        return fetch(fetchRequest).then(
          function(response) {
       
            if(!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }

            var responseToCache = response.clone();

            caches.open(CACHE_NAME)
              .then(function(cache) {
                cache.put(event.request, responseToCache);
              });

            return response;
          }
        );
      })
    );
});

5. 浏览器兼容性

image3

三、PWA

1. 什么是PWA ?

PWA(Progressive Web Apps,渐进式 Web 应用)运用现代的 Web API 以及传统的渐进式增强策略来创建跨平台 Web 应用程序。

2. 优点/缺点

优点

可被发现、易安装、可链接、独立于网络、渐进式、可重用、响应性和安全的。

缺点

3. 核心技术

  • start_url 可以设置启动网址
  • icons 会帮我萌设置各个分辨率下页面的图标
  • background_color 会设置背景颜色, Chrome 在网络应用启动后会立即使用此颜色,这一颜色将保留在屏幕上,直至网络应用首次呈现为止
  • theme_color 会设置主题颜色
  • display 设置启动样式

4. App Shell

App Shell 是页面能够展现所需的最小资源集合,即支持用户界面所需的最小的 HTML、CSS 和 JavaScript 等静态资源集合。App Shell架构是构建PWA的一种方式,这种应用能可靠且即时地加载到您的用户屏幕上,与原生app相似。PWA 多数采用单页应用(Single Page Application)的方式编写,这样能减少页面跳转带来的开销,并且开发者可以在页面切换时增加过渡动画,避免出现加载时的白屏。那么在页面切换时页面上固定不动的内容就是 App Shell 的一部分。应用从显示内容上可以粗略的划分为内容部分和外壳部分。App Shell 就是外壳部分,即页面的基本结构。比如header,sidebar。

image5

主要功能-都依赖于 Service Worker

1. 离线

2. 后台同步

后台同步可以让你在关闭网站后,进行一些被中断的请求或操作。 需要在 Service Worker 中监听 sync 事件,在浏览器中发起后台同步 sync 会触发 Service Worker 的 sync 事件。

// index.js
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);
        });
    });
});

// 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;
            })
        );
    }
});

3. 消息推送/通知

Push API 和 Notification API 是不同但互补的功能,Push API 是用于订阅并推送消息给 Service Worker,而 Notification API 用于从 Service Worker 发送消息给用户。

要完成消息推送并展示,需要经过下面几个步骤:

  1. web客户端 注册SW并向push service发起消息订阅请求,并将订阅信息(subscription)保存起来
  2. web server从 web客户端 处拿到subscription
  3. web server向subscription中的目的地(endpoint,其中包含了push service的地址)发送消息
  4. push service收到消息,转发给browser
  5. browser唤醒SW,将消息发给它
  6. SW收到消息,展示出来

和browser相关联的专门用来处理通知的服务,用来接受web server推送的消息。每个浏览器都有自己的push service

image6

四、基于 Angular 的 PWA 消息通知

使用 web-push 完成订阅和推送。

第一步,客户端请求订阅用户: image7

一旦用户授权,浏览器就会生成一个PushScription,pushSubscription 包含公钥和 endpointURL,应用服务器推送时可以使用公钥对消息加密,endpointURL 是由推送服务器生成包含唯一标识符的 URL,推送服务器通过它判断将消息发送到哪个客户端。

第二步,应用服务器发送web push协议标准的api,触发推送服务器的消息推送: image8

应用服务器发送消息推送请求(目的是为了将更新推送到用户的浏览器),为了向推送服务器发出请求,需要查看先前获得的PushScription,取出其中的endpoint,即为推送服务器配置给该用户的访问点。

一个PushScription对象如下:

{
  "endpoint": "https://random-push-service.com/some-kind-of-unique-id-1234/v2/",
  "keys": {
    "p256dh" :
"BNcRdreALRFXTkOOUHK1EtK2wtaz5Ry4YfYCA_0QTpQtUbVlUls0VJXg7A8u-Ts1XbjhazAkj7I99e8QcYP7DkM=",
    "auth"   : "tBHItJI5svbpez7KI4CCXg=="
  }
}

其中的endpoint就是web server发送消息的目的地

可以看到chrome的push service服务运行在https://fcm.googleapis.com,每个浏览器都不一样,firefox的是https://updates.push.services.mozilla.com, edge的是https://sg2p.notify.windows.com.

第三步,浏览器端接收消息推送,触发push事件并展示:

image9

push service收到消息后,会通知浏览器(如果浏览器当前关闭了,下一次打开时会收到通知),浏览器唤醒相应的SW,具体就是给SW发送”push”事件,SW处理push事件,并弹个小框将消息展示出来。

主要代码

首先询问用户是否开启订阅:requestSubscription,如果是,得到一个PushScription对象。在调用subscribe生成PushScription时,浏览器会向它指定的中转服务器发送请求来生成endpoint和其余部分。获取到PushScription对象后,将其发往应用服务器,进行存储。

 subscribeToNotifications() {

        this.swPush.requestSubscription({
            serverPublicKey: this.VAPID_PUBLIC_KEY
        })
        .then(sub => {
            this.sub = sub;
            console.log("Notification Subscription: ", sub);
            this.newsletterService.addPushSubscriber(sub).subscribe(
                () => console.log('Sent push subscription object to server.'),
                err =>  console.log('Could not send subscription object to server, reason: ', err)
            );
        })
        .catch(err => console.error("Could not subscribe to notifications", err));

    }

应用服务器发送消息,push service 收到消息后,会通知浏览器(如果浏览器当前关闭了,下一次打开时会收到通知),浏览器唤醒相应的SW,具体就是给SW发送”push”事件,SW处理push事件,并弹个小框将消息展示出来。 还可以设置action或处理用户点击。

 const notificationPayload = {
        "notification": {
            "title": "Angular News",
            "body": "Newsletter Available!",
            "icon": "assets/main-page-logo-small-hat.png",
            "vibrate": [100, 50, 100],
            "data": {
                "dateOfArrival": Date.now(),
                "primaryKey": 1
            },
            "actions": [{
                "action": "explore",
                "title": "Go to the site"
            }]
        }
    };


    Promise.all(USER_SUBSCRIPTIONS.map(sub => webpush.sendNotification(
        sub, JSON.stringify(notificationPayload) )))
        .then(() => res.status(200).json({message: 'Newsletter sent successfully.'}))
        .catch(err => {
            console.error("Error sending notification, reason: ", err);
            res.sendStatus(500);
        });

image10

  • VAPID

VAPID是为了区分出合法的推送者。简单来说就是我们为 web server 生成一个密匙对,包含公匙和私匙,并加上一个email地址,以便发生问题时push service可以联系推送者。在使用之前加上vapidKeys即可。 使用web-push generate-vapid-keys --json获取vapidKeys。

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

const vapidKeys = {
    "publicKey":"BPCvPoAV6Msm4Y_uWb6H-8SAwKMN2JpuhkYIEKEqfVPSzH4krH7_-M14HGcnG7mWC153aUDMw74LRHVKcYCDujI",
    "privateKey":"I6gFHumIGU_wTr8SvfLYsiX4u8bplzBlQJJM88kOrYw"
};
webpush.setVapidDetails(
    'mailto:example@yourdomain.org',
    vapidKeys.publicKey,
    vapidKeys.privateKey
);

总结

Service Worker 与 PWA 的功能十分强大。有兴趣的同学可以自己试试离线和后台同步的功能。

觉得有帮助的话,不妨考虑购买付费文章来支持我们 🙂 :

付费文章

友情链接


相关文章