关于Service Worker

934 阅读8分钟

背景

工作中因为一些原因静态资源的服务挂了,这样就会导致前端加载静态资源的时候报错,在研究的有没有方法进行容灾,发现service worker可以解决这个问题。

什么是service woker

本质上service woker也是一种web woker,所以它具有web woker的特性,那么所以先介绍一下web woker

web worker

我们知道JavaScript是一门单线程的语言,这意味着不能同时执行多个脚本。

比如我们浏览器页面中处理大量API、操作DOM、响应UI事件时,他们都不能同时进行,但是我们通过异步事件处理机制来处理他们,以达到“并发”的效果。

而web woker可以让Web应用程序可以在独立于主线程的后台线程中,运行一个脚本。所以web woker可以用来处理计算密集型任务,因为他独立于浏览器主线程,所以它不会阻塞UI交互等。

需要注意的是,你不能直接在 worker 线程中操纵 DOM 元素或使用widnow 对象中的某些方法和属性(大部分是可以使用)。

主线程和 worker 线程之间通信是通过,使用 postMessageonmessage 这两个方法来发送、接收消息和数据,注意收到数据只是原数据的副本,两个线程之间不能共享数据。

Service worker

Service worker 相比普通woker,除了拥有一般worker的能力,他还可以拦截、修改页面发起的资源请求,对于响应也可以进行缓存。

所以Service worker 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。

暂时无法在文档外展示此内容

出于安全考量,Service workers只能由HTTPS承载。

怎么用Service woker

浏览器兼容性可以查看:jakearchibald.github.io/isservicewo…

通过调用navigator.serviceWorker.register指定servier woker文件位置,完成安装:

if ('serviceWorker' in navigator) {

    window.addEventListener('load', function() {

      navigator.serviceWorker.register('/sw.js', {scope}).then(function(registration) {

        // Registration was successful

        console.log('ServiceWorker registration successful with scope: ', registration.scope);

      }, function(err) {

        // registration failed :(

        console.log('ServiceWorker registration failed: ', err);

      });

    });

  }

注意:如果域名为本地域名localhost,那么service worker的注册域名不会要求https,但是线上一定是需要https协议的。

scope

需要注意的是register的第二个可选参数还有传入scope。scope表示可以拦截请求的范围。

默认情况下,会有一个默认值:

例如文件的url是/js/sw.js,scope默认就是/js/。也就是说默认是和service_worker的文件路径匹配的。

navigator.serviceWorker.register("/js/sw.js").then(() => {

    console.log("Install succeeded with the default scope '/js/'.");

  });

例如文件的url是/js/sw.js,scope指定是/js/es/,那么sw只会拦截所有/js/es这个路由下的文件。

navigator.serviceWorker.register("/js/es/sw.js", { scope: "/js/es" }).then(() => {

    console.log("only apply to resources under /js/es/");

  });

如果文件的url是/js/sw.js,但是我们强行指定scope为./将会报错。如下

navigator.serviceWorker.register("/js/sw.js", { scope: "/" }).catch(() => {

    console.error("Install failed due to the path restriction violation.");

  });

但是如果请求sw.js的请求有 Service-Worker-Allowed 响应头,那么scope指定的范围只要不大于响应头value的值,那么也是能成功设置的

// 响应头 "Service-Worker-Allowed : /"

// 只要范围不大于"/",都是可以的。

navigator.serviceWorker.register("/js/sw.js", { scope: "/" }).then(() => {

    console.log("Install succeeded as the max allowed scope was overriden to '/'.");

  });

  

// 响应头 "Service-Worker-Allowed : /foo"

// 只要范围不大于“/foo", 都是可以的,但这个例子中的“/”大于了

navigator.serviceWorker.register("/foo/bar/sw.js", { scope: "/" }).catch(() => {

    console.error("Install failed as the scope is still out of the overriden maximum allowed scope.");

});

举个🌰,比如gip.bytedance.net这个域名下的除蓝鲸外的其他站点,想去使用蓝鲸的service worker的文件(gip.bytedance.net/bluewhale/sw.js)是不行的,注册将会报错,除非增加获取文件的响应有Service-Worker-Allowed 响应头。

service woker 生命周期

img

因为service worker的运行独立于浏览器,所以它有自己的生命周期。具体到代码层面,就是在service worker的代码里面,通过监听不同事件运行响应代码。

install

在浏览器调用navigator.serviceWorker.register注册之后,浏览器会拉取对应的service worker文件运行,那么会触发第一个事件install。在service woker代码里面:

self.addEventListener('install', function(event) {

     // waitUntil 意味着异步操作完成,service worker才会进入下一个状态。

     event.waitUntil(

        caches.open(CACHE_NAME)

            .then(function(cache) {

                console.log('[SW]: Opened cache');

                return cache.addAll(allAssets);

            })

    );

});

我们一般在install里面,可以缓存一些需要预缓存的文件。

如果在install里面有任何任务失败、报错,那么该步骤都会失败,service woker安装失败,将不会起作用,此时进入Error状态,否则进去active状态。

waiting

其实在install和在active之间还有一个waiting的中间状态,该状态发生在上一个service worker还在运行,当前service worker已经完成install阶段,只能进入waiting状态,只有上一个service workder进入terminated后,当前service worker才会进入active阶段。

img

activate

当前service worker在activate状态意味着,上一个service worker已经不会在运行了,所以一般可以在activate把已经不需要的、过期的缓存删掉(比如缓存策略或者缓存文件发生了变化)

self.addEventListener('activate', function(event) {

    event.waitUntil(

        caches.keys().then(deleteCaches)

    );

});

terminate

我们控制旧service worker进入terminated状态的条件有:

  1. 关闭浏览器。
  2. 手动在devTool里面点击跳过。
  3. 在新service worker的代码里面手动调用self.skipWaiting。

除了上面说的三种,浏览器可以在也可以在适当的时机,终止service worker的运行,如:

  • 没有要处理的事件
  • 检测异常操作,例如无限循环或者一些处理事件的任务超时。

更新 service Worker

我们在浏览器运行的代码,每次都会调用navigator.serviceWorker.register来注册service worker,但是上面时候决定是否需要更新service worker呢? 这里分为两种情况:

  • 如果没有注册的service worker,那么会请求下载,并安装激活。
  • 如果已经注册的service worker,那么发起请求,并根据内容和现有的service worker作对比(注意,这一过程是在后台进行的,network看板看不到该请求),如果代码内容没有差别,则啥也不做;如果有不同,则进入新的service worker的install阶段,之后进入waiting阶段。

Cache storage

在上面的service worker演示的代码中可以发现,常常使用caches相关的API,如caches.match、caches.open等,事实上缓存都是存在Cache storage 里面的。

什么是Cache storage ?

它是一种可以根据网络请求的来获取响应的一种储存机制,内部以请求和响应成对储存的,以request为key,response为value。

Cache

-------------

key | value

-------------

req | res

需要注意的是,service worker 和 cache storage 并一定非要****配套使用。CacheStorage 并不是 Service Worker 独有的API,而且service worker也不一定非要使用Cache Storage来储存缓存。

我们在 SW 中也可以使用其他存储机制,例如,IndexedDB,浏览器中的 SQL 数据等来储存网络请求和响应。

而且我们也在DOM线程里也可以使用 cache storage :

<script>

    if('caches' in window) {

        caches.open('cache_name').then((cache) => {

            // ...

        }).catch((err) => {

            // ...

        })

    }

</script>

实际上 cache storage也可以作为我们一个储存的工具,前提是我们需要手动构造请求和响应

const req = new Request('/image.jpg)

const res = new Response(new Blob([data]),{type:'image/jpeg'})

但是通常情况下,service worker和cache storage结合在一起使用会非常方便,所以他们往往放在一起使用。

Cache storage 相关API

下面的API指的是caches的API。

  • Open。传入缓存key,类型为string,作为缓存的命名空间。如果有缓存就返回对应cache,如果没有就创建一个。
 if ('caches' in window) {

        caches.open('cache_name').then((cache) => {

            l(cache)

        }).catch((err) => {

            l(err)

        })

    }
  • Check。 检查是否有这个缓存命名空间。如果有返回true,如果没有返回false
  • delete。删除这个缓存命名空间。删除成功返回true,失败返回false。
  • keys。 返回所有缓存的key。
  • match。传入request,在cache storage中找到第一个匹配的对象并返回,如果不存在返回undifine。
caches.match(request, options).then(function(response) {

  // Do something with the response

});

Cache 相关API

通过CacheStorage.open()会返回Cache对象

  • add。以Request对象或者url字符串为参数,并且自动发起网络请求,成功后,以请求为key,响应为value储存在cache storage里面。
caches.open('cache_name').then((cache) => {

    cache.add('/')

}).catch((err) => {

    l(err)

})
  • addAll。跟add类似,只不过能接受数组为参数,但是只要任何一个请求失败,都不会缓存。
  • put。更加灵活,接受request和response为参数,这意味着需要手动构造response对象或者手动发起请求。
fetch('/faq').then(function(response) {

    return caches.open('cache_name').then(function(cache) {

        return cache.put('/faq', response)

    })

})
  • delete。接受请求为参数,删除请求和响应。
  • keys。返回该缓存空间下所有缓存的request对象构成的数组。
  • match 、matchall。都是返回缓存的响应,但是后者会返回所有匹配到的。

容量

可以调用navigator.storage.estimate查看容量,但是从函数名estimate也能看得出来,这个方法返回的容量也仅供参考:

if ('storage' in navigator && 'estimate' in navigator.storage) {

    const {usage, quota} = await navigator.storage.estimate();

    const percentUsed = Math.round(usage / quota * 100);

    const usageInMib = Math.round(usage / (1024 * 1024));

    const quotaInMib = Math.round(quota / (1024 * 1024));



    const details = `${usageInMib} out of ${quotaInMib} MiB used (${percentUsed}%)`;

    console.log(details)

}
  • usage:表明当前域名用了多少字节,包括caches、service worker、和index db等。如果请求是不透明请求Opaque Request),会固定分配一个大小。在chrome下为15mb,所以我们应该尽量减少不透明请求的缓存。另外如果使用的不同设备使用的压缩算法不同,大小也会不一样。
  • quota:表明当前域名一共能用多少字节。在chrome的隐身模式下,将和常规的大小限制不同,大概是100mb。

跨域请求 && 不透明响应

当跨域请求是简单请求的时候,没有返回cors响应头的时候,这个时候的响应是不透明响应,获取不到响应体的内容和大小的,所以如果用cache storage 储存的话会默认占用15mb的大小。

所以如果我们的服务器支持cors响应,在浏览器需要配置crossorigin 为anonymous,否则浏览器会把这些请求处理为no-cors

<link crossorigin="anonymous" rel="stylesheet" href="https://example.com/path/to/style.css">

<img crossorigin="anonymous" src="https://example.com/path/to/image.png">

如果使用的是fetch,参考相关配置选项,把mode配置为cors。

事件驱动特性

除了active 和 install是可以监听的生命周期事件外,还可以监听fetch、push、sync、notificationclick、notificationclose等事件。(所有可以监听事件参考:w3c.github.io/ServiceWork…

其中fetch事件是我们最常用的,我们可以通过监听fetch事件,实现“缓存优先”策略的效果:

self.addEventListener('fetch', function(event) {

    event.respondWith(

      caches.match(event.request)

        .then(function(response) {

          if (response) {

            return response;

          }

          return fetch(event.request);

        }

      )

    );

  });

在service worker中,我们很多逻辑都是在事件的异步回调中进行的,这样天生异步的API,可以让浏览器调度随时调度worker的运行,随时可以停止或者重启service worker的运行。

使用Workbox

如果要我们从零自己基于事件来写service worker的代码,我们需要自己监听事件,逻辑处理会比较繁琐和分散。

而workbox是谷歌开源的库,它封装了service worker 和 cache storage 的相关的API,可以帮助我们专注于业务的需求,快速接入service worker,不用关心底层实现。

接入方式

主要有两种方式:

  1. npm包的形式引入service worker的代码里面,后续用webpack打包成一个文件。
  2. cdn的形式引入。

workbox当前最新的版本是v6,从v5开始workbox已经支持npm包的形式引入,推荐NPM包的方式接入,有ts类型提示,开发体验更佳。

cdn

在service worker中支持使用importScripts引用脚本,使用cdn的形式引入:

importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.2.0/workbox-sw.js');



workbox.precacheAndRoute(self.__WB_MANIFEST);

npm包

例如我们需要在install时,预缓存所有静态资源,用npm包的形式如下:

//service-worker.js

import { precacheAndRoute } from 'workbox-precaching';



// Use with precache injection

precacheAndRoute(self.__WB_MANIFEST); 

需要注意的上面的self.__WB_MANIFEST,全局一开始是没有这个变量的,需要我们手动注入,而这变量是有一定格式要求的。

如果我们使用在webpack里面配置workbox-webpack-plugin里的InjectManifest插件,他会根据我们每次打包出来的静态文件的不同,动态注入这个变量名

// Inside of webpack.config.js:

const {InjectManifest} = require('workbox-webpack-plugin');



module.exports = {

  // Other webpack config...

  plugins: [

    // Other plugins...

    new InjectManifest({

      swSrc: './src/sw.js',

    })

  ]

};

另外,workbox-webpack-plugin还提供GenerateSW插件,只是简单的提供precache的功能,通过简单的plugin配置来设置路由和策略,不需要我们手动写sw.js文件。

// Inside of webpack.config.js:

const {GenerateSW} = require('workbox-webpack-plugin');



module.exports = {

  // Other webpack config...

  plugins: [

    // Other plugins...

    new GenerateSW()

  ]

};

关于两个插件的选择:

  • 如果需要灵活就选择InjectManifest。它只是帮忙注入self.__WB_MANIFEST变量,也不用我们自己写service-worker.js文件的打包入口。
  • 如果只需要简单功能就选择GenerateSW。他通过一些简单配置就能控制预缓存、路由等简单功能。

模块简单介绍

workbox把内部的功能拆分为多个包,我们根据需求引用,下面简单介绍一下各个包的功能,详情可以查看具体链接:

  • workbox-precaching。提供precacheAndRoute方法,在install阶段预缓存一系列资源。
import {precacheAndRoute} from 'workbox-precaching';



precacheAndRoute([

  {url: '/index.html', revision: '383676' },

  {url: '/styles/app.0c9a31.css', revision: null},

  {url: '/scripts/app.0d5770.js', revision: null},

  // ... other entries ...

]);

// or webpack 注入变量

precacheAndRoute(self. __WB_MANIFEST)
  • workbox-routing。提供registerRoute、NavigationRoute等。把请求路由到各个处理函数中。
import {registerRoute} from 'workbox-routing';



// NavigationRoute 是专门处理navigation requests.的。

// 可以理解NavigationRoute 是用于缓存html文件的

registerRoute(

  new NavigationRoute(

    new NetworkFirst({

      cacheName: 'navigation-cache',

    }),

    {

      allowlist: [/\/bluewhale\/.*/],

    }

  )

);



registerRoute(

  new RegExp('/styles/.*\\.css'), //可以是函数、字符串、正则

  handlerCb // 可以是自定义处理函数、也可以是内workbox的处理策略。

);
  • workbox-strategies。提供一系列内置的缓存策略,下面一节会详细介绍。
  • workbox-expiration。提供ExpirationPlugin插件,用于缓存策略plugins配置,控制缓存过期。
import {registerRoute} from 'workbox-routing';

import {CacheFirst} from 'workbox-strategies';

import {ExpirationPlugin} from 'workbox-expiration';



registerRoute(

  ({request}) => request.destination === 'image',

  new CacheFirst({

    cacheName: 'image-cache',

    plugins: [

      new ExpirationPlugin({

        maxEntries: 20,

      }),

    ],

  })

);
  • workbox-cacheable-response。提供CacheableResponsePlugin插件,用于缓存策略plugins配置,可控制是否一个response应该被缓存。
import {registerRoute} from 'workbox-routing';

import {CacheFirst} from 'workbox-strategies';

import {CacheableResponsePlugin} from 'workbox-cacheable-response';



registerRoute(

  ({url}) => url.pathname.startsWith('/images/'),

  new CacheFirst({

    cacheName: 'image-cache',

    plugins: [

      new CacheableResponsePlugin({

        statuses: [0, 200],

      })

    ]

  })

)
import {

    pageCache,

    imageCache,

    staticResourceCache,

    googleFontsCache,

    offlineFallback,

  } from 'workbox-recipes';

  

  pageCache(); // 缓存html,使用networkfirst策略。

  

  googleFontsCache();

  

  staticResourceCache(); // 缓存css js等静态资源,使用stale-while-revalidate策略。

  

  imageCache();// 缓存image,使用cache-first策略。

  

  offlineFallback(); // 当没网的时候,返回offline.html。

缓存策略

缓存策略决定当service worker监听到一个fetch事件后,如何处理响应和缓存。

workbox-strategies提供了大多数通用的策略可供选择。

Network only

强制使用网络请求,service worker只是负责转发。

img

Cache Only

强制使用缓存,不会发起网络请求。

img

Stale-while-revalidate

优先使用缓存,后台网络请求更新缓存。如果没有缓存将会使用网络请求。

优先:放心使用。用户会更新缓存。

缺点:会占用用户带宽。

img

Cache first

返回缓存的响应。如果请求没有在缓存中,将会请求,然后把响应缓存起来。

img

Network first

网络请求优先。收到响应更新缓存。如果网络失败,返回缓存的响应。

img

precaching

precahing并不属于workbox-strategies,而是属于workbox-precaching。

应用启动后,后续请求的资源直接从缓存中读取。

调用precacheAndRoute 和 addRoute需要注意顺序。precacheAndRoute 使用的是cache-first策略,除非缓存的响应(因为某些意外错误),这种情况下会使用网络的响应来替代。

调用precacheAndRoute 和 addRoute的顺序很重要。我们通常在registerRoute之前调用precacheAndRoute,如果先调用registerRoute,那么他先命中的路由将会响应请求,而不是precacheAndRoute中使用的缓存优先的策略。

缓存清单格式precache manifest:

precacheAndRoute([  

    {url: '/index.html', revision: '383676' },  

    {url: '/styles/app.0c9a31.css', revision: null},  

    {url: '/scripts/app.0d5770.js', revision: null},  

    // ... other entries ...

]);

index.html 需要显式的传revision字段,他是通过HTML内容计算的哈希,而css和js资源 的revision为null,因为他们的文件名就包含了哈希。通过向precacheAndRoute传递revision属性,workbox可以知道文件啥时候发生了改变。

如何调试service worker

workbox调试设置

如果使用使用npm包的形式引入workbox,那么workbox会根据NODE_ENV来决定生产还是开发环境的打包,并且在开发环境会有对应的LOG输出。当接入webpack的时候,会根据webpack的mode配置来决定。

当使用cdn的方式接入,需要我们手动设置是否需要debug输出:

importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.2.0/workbox-sw.js');



// This needs to come before any other workbox.* methods.

workbox.setConfig({

  debug: true,

});

chrome调试

首先需要在devTools面板上勾选disable cache,否则可能显示资源从memory cache返回的。

img

chrome缓存优先级:

  1. Memory cache。 浏览器自身优化。
  2. Service worker 。 开发者自己控制。
  3. Disk cache。http 强缓存 或者 304响应的协商缓存。

ps:该文章认为:

  1. service worker 大于 memory cache,但经过我个人实验,事实并非如此。memory cache 是浏览器自身优化行为,优先级较高。
  2. 经过service worker,但是service worker是调用fetch方法实际上发起请求的,network会出现两条记录,一条表示浏览器的请求从service worker返回的结果,一条表示service worker发起请求的具体内容:

img

如果请求命中强缓存的:

img

在application下的service worker面板,可以快速勾选offiline、update on reload、bypass for network。

  • 当我们需要离线测试service worker在离线下的表现时,需要勾选offline。
  • 当我们不需要service worker的能力时,可以选择bypass for network。

img

在cache下的cache storage tab可以浏览所有已经缓存的资源。

PWA

讲到service worker 不得不介绍一下PWA,虽然PWA在国内的使用比较少,大部分都去做小程序了,但是在国外用的还是比较多。

什么是PWA

google提出PWA的时候,并没有给他一个准确的定义,PWA不是单指某一项技术,而是应用多项技术来改善用户体验的 Web App,其核心技术包括 :

  • Web App Manifest
  • Service Worker
  • Web Push

PWA的全称为 progressive web app(渐进式web应用),所谓渐进式一方面说的是开发者使用新特性代价很小,只需要做一点点更改,就可以让用户有接近原生APP的体验。

PWA 具有如下特性。

  • 渐进式 - 适用于所有浏览器,因为它是以渐进式增强作为宗旨开发的

  • 连接无关性 - 能够借助 Service Worker 在离线或者网络较差的情况下正常访问

  • 类原生应用 - 由于是在 App Shell 模型基础上开发,因此应具有 Native App 的交互,给用户 Native App 的体验

  • 持续更新 - 始终是最新的,无版本和更新问题

  • 安全 - 通过 HTTPS 协议提供服务,防止窥探,确保内容不被篡改

  • 可索引 - manifest 文件和 Service Worker 可以让搜索引擎索引到,从而将其识别为『应用』

  • 黏性 - 通过推送离线通知等,可以让用户回流

  • 可安装 - 用户可以添加常用的 Web App 到桌面,免去到应用商店下载的麻烦

  • 可链接 - 通过链接即可分享内容,无需下载安装

总的来说PWA,兼具了web 和native 的优点:

无法复制加载中的内容

核心技术

Web App Manifest

Web App Manifest | MDN (mozilla.org)

一般进入web页面,我们需要到浏览器输入网址或者打开书签,相比于原生应用可以直接点开手机主屏上的应用图标,这样使用链路太长,太麻烦了。

所以Web 应用清单(Web App Manifest)这项技术允许web应用也能在手机主屏上添加快捷方式,除此之外,还允许开发者隐藏浏览器多余的UI地址栏,导航栏等,让 PWA 具有和 Native App 一样的沉浸式体验。

具体来说,可以通过link标签引用manifest.json

<link rel="manifest" href="/manifest.json">

manifest.json这个文件里面配置相关信息,配置例子:

{

  "name": "HackerWeb",

  "short_name": "HackerWeb",

  "start_url": ".",

  "display": "standalone",

  "background_color": "#fff",

  "description": "A simply readable Hacker News app.",

  "icons": [{

    "src": "images/touch/homescreen48.png",

    "sizes": "48x48",

    "type": "image/png"

  }, {

    "src": "images/touch/homescreen72.png",

    "sizes": "72x72",

    "type": "image/png"

  }, {

    "src": "images/touch/homescreen96.png",

    "sizes": "96x96",

    "type": "image/png"

  }, {

    "src": "images/touch/homescreen144.png",

    "sizes": "144x144",

    "type": "image/png"

  }, {

    "src": "images/touch/homescreen168.png",

    "sizes": "168x168",

    "type": "image/png"

  }, {

    "src": "images/touch/homescreen192.png",

    "sizes": "192x192",

    "type": "image/png"

  }],

  "related_applications": [{

    "platform": "web"

  }, {

    "platform": "play",

    "url": "https://play.google.com/store/apps/details?id=cheeaun.hackerweb"

  }]

}

Service Worker

Service worker 是PWA中最关键的技术,他可以让web应用能够离线和缓存。前面介绍了很多,这里不详细展开。

离线通知

离线通知可以在用户没有打开 PWA 站点的情况下,也能接受服务器发送过来的通知并展现给用户。

其中,离线通知和展示通知,分别需要用到Web Push 和 Notification API。

浏览器在接收推送过来的离线消息时,会唤醒对应站点注册的 Service Worker,开发者可以在 Service Worker 文件中处理接收到的请求,显示通知。

通知——Notification

通过Notification API在页面可以直接发起通知,但是在发起通知之前需要先判断是否有权限:

if (Notification.permission === 'granted') {

  // 用户已授权,可展示通知

  // register()

} else if (Notification.permission === 'denied') {

  // 用户已禁止

} else {

  // 首次请求会要求用户选择配置,后续直接返回用户的配置。

  Notification.requestPermission().then(permission => {

  // 通过 permission 判断用户的选择结果

  if(permission === 'granted'){

    // register()

  }

  })

}

img

在拿到权限之后,在主线程可以通过实例化Notification对象来显示通知:

// main.js

const title = 'Notification Title'

const options = {

    body: 'body content \n body body content '

}

const notification = new Notification(title, options)

// notification.onclose = xxx

// notification.onclick = xxx

// ...

img

更多参数参考:Notifications API (w3c-html-ig-zh.github.io)

在service worker里面也可以发送通知,不过不是直接通过实例化Notification对象,而是通过showNotification API

// sw.js

const title = 'Notification Title'

const options = {

    body: 'body content \n body body content ',

    data: {url:'xxxx'}

}



self.registration.showNotification(title, options);



// 监听通知点击事件

self.addEventListener('notificationclick', function (e) {

  // 默认不会关闭通知

  e.notification.close()

  // 打开网页

  event.waitUntil (clients.openWindow(e.notification.data.url) )

})

// 监听通知关闭事件

self.addEventListener('notificationclose', function(event) {

  const dismissedNotification = event.notification

  // 可以打点分析

  event.waitUntil(notificationCloseAnalytics)

})

网络推送——web push

draft-ietf-webpush-protocol-12

下面是web puhs协议描述的基本流程:

   +-------+           +--------------+       +-------------+

    |  UA   |           | Push Service |       | Application |

    +-------+           +--------------+       |   Server    |

        |                      |               +-------------+

        |      Subscribe       |                      |

        |--------------------->|                      |

        |       Monitor        |                      |

        |<====================>|                      |

        |                      |                      |

        |          Distribute Push Resource           |

        |-------------------------------------------->|

        |                      |                      |

        :                      :                      :

        |                      |     Push Message     |

        |    Push Message      |<---------------------|

        |<---------------------|                      |

        |                      |                      |
1. 首先浏览器起订阅请求:
// 在service worker注册后调用

// 订阅推送并将订阅结果发送给后端

function subscribeAndDistribute (registration) {

  if (!window.PushManager) {

    return Promise.reject('系统不支持消息推送')

  }

  // 检查是否已经订阅过

  return registration.pushManager.getSubscription().then(function (subscription) {

    // 如果已经订阅过,就不重新订阅了

    if (subscription) {

      return

    }

    // 如果尚未订阅则发起推送订阅

    return registration.pushManager.subscribe({

      userVisibleOnly: true,

      applicationServerKey: base64ToUint8Array(VAPIDPublicKey)

    }).then(function (subscription) {

      // 订阅推送成功之后,将订阅信息传给后端服务器

      distributePushResource(subscription)

    })

  })

}

其中subcribe有两个参数:

  • userVisibleOnly,表明该推送是否需要显性地展示给用户,即推送时是否会有消息提醒,必传为true,否则会报错。
  • VAPIDPublicKey:web push使用的的自主应用服务标识(VAPID)协议,这是指的是公钥。可以使用npx web-push generate-vapid-keys来生成。这里使用了base64ToUint8Array是因为参数要。

Web Push 协议出于用户隐私考虑,在应用和推送服务器之间没有进行强身份验证,这为用户应用和推送服务都带来了一定的风险。 VAPID 规范允许应用服务器向推送服务器标识身份,推送服务器知道哪个应用服务器订阅了用户,并确保它也是向用户推送信息的服务器。使用 VAPID 服务过程很简单,通过几个步骤可以理解 VAPID 如何实现安全性:

  1. 应用服务器创建一对公钥/私钥,并将公钥提供给 Web App 客户端
  2. 当用户尝试订阅推送服务时,将公钥添加到 subscribe() 订阅方法中,公钥将被发送到推送服务保存。
  3. 应用服务器想要推送消息时,发送包含公钥和已经签名的 JSON Web 令牌到推送服务提供的 API,推送服务验证通过后,将信息推送至 Web App 客户端。
2.然后服务器把订阅信息存起来:

上面发给服务端的subscription的格式大概是这样:

{"endpoint":"https://wns2-sg2p.notify.windows.com/w/?token=BQYAAADKUMmLoyLvpM6uMBnodGcUlRj9FRepJceHi4V2Qu9vKwx3NvWB7yLuCxEcmpOCmClAIWp4z%2bXJ2mpK0H2x96Y9Mx4lPWNEojljTuOhBKElTzo6wAHXoc%2fC2SpUoBbOSDmku8AVqEamtHcUVsbnH9wn9s7RvbXUTiCwrp4es3SeYEag4vcTqm9wyCfOovREJV%2bPHsTlqzYgqUAsxuSM2U4ZSNKV1kkIArT4pzVWijAAyx582AhEX2iOt3zAV5VSw8yKlGM%2bqKEDxQorm1KGFqU6f1Pj03ij8G%2brxg2ObRfhEi9dzW%2b%2bbBrbLAhSvLtBUds%3d","expirationTime":null,"keys":{"p256dh":"BDadLo9qL3xowvwItyVi64hRKVBqtqtyYuWg9cDyALfuHBu4QPWd71Wu0VSBtV8ToHR9vzd8jE_UWos_PP-prJM","auth":"RtyUWfFS8BfZALqFb8S7vA"}}

其中endpoint标识了唯一的订阅地址,推送服务器才知道要把消息发给哪个设备。

服务端把需要把这个订阅信息存起来。

3. 服务端把消息发给推送服务器

需要推送消息的时候,再把订阅信息加上推送信息私钥加密发送给推送服务器。

所以VAPID协议的使用就是防止不让用户收到太多垃圾推送(必须是用户授权的,不是谁都能发推送)。

4. 推送服务器再把消息分发给浏览器

这里的推送服务指的就是谷歌的GCM或者FCM(GCM已被废弃),但是由于墙的关系,所以在国内很难接受到消息。

除此之外,浏览器也需要支持谷歌的接收服务,所以很多国产浏览器和safari都不太行。

这两点也是国内PWA不太行的原因。。。

5. Service worker接受消息,并通知。

self.addEventListener('push', function(event) {

    console.log('[Service Worker] Push Received.');

    console.log(`[Service Worker] Push had this data: "${event.data.text()}"`);



    const title = event.data.text();

    const options = {

      body: '快来看看蓝鲸',

      icon: 'images/icon.png',

      badge: 'images/badge.png',

      data: {

          url'https://gip.bytedance.net/bluewhale',

      }

    };



    event.waitUntil(self.registration.showNotification(title, options));

});



self.addEventListener('notificationclick', function(event) {

    console.log('[Service Worker] Notification click Received.');

    // 点击默认不会关闭,需要手动关闭

    event.notification.close();

    // 保证打开之前,service worker状态不会改变。

    event.waitUntil(

      clients.openWindow(event.data.url)

    );

});

有消息来的时候,浏览器启动对应站点的service worker服务,service worker里面有相应的事件监听函数。

所以service worker事件驱动的优势又一次体现了出来。

除此之外,本地也可以直接点击 service worker调试面板的push进行调试。

img

总结

虽然国内PWA用的不是很多,但是service worker强大的能力,相比大家已经感受到,有离线缓存、预缓存、网络代理等需求的可以尝试一下。

参考

mp.weixin.qq.com/s/3Ep5pJULv…

developers.google.com/web/tools/w…

segmentfault.com/a/119000003…

developers.google.com/web/fundame…

developers.google.com/web/updates…

segmentfault.com/a/119000001…

segmentfault.com/a/119000003…

zhuanlan.zhihu.com/p/44789005