背景
工作中因为一些原因静态资源的服务挂了,这样就会导致前端加载静态资源的时候报错,在研究的有没有方法进行容灾,发现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 线程之间通信是通过,使用 postMessage
和onmessage
这两个方法来发送、接收消息和数据,注意收到数据只是原数据的副本,两个线程之间不能共享数据。
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 生命周期
因为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阶段。
activate
当前service worker在activate状态意味着,上一个service worker已经不会在运行了,所以一般可以在activate把已经不需要的、过期的缓存删掉(比如缓存策略或者缓存文件发生了变化)
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(deleteCaches)
);
});
terminate
我们控制旧service worker进入terminated状态的条件有:
- 关闭浏览器。
- 手动在devTool里面点击跳过。
- 在新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,不用关心底层实现。
接入方式
主要有两种方式:
- npm包的形式引入service worker的代码里面,后续用webpack打包成一个文件。
- 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],
})
]
})
)
- workbox-recipes。相当于内置了一系列通用的代码。
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只是负责转发。
Cache Only
强制使用缓存,不会发起网络请求。
Stale-while-revalidate
优先使用缓存,后台网络请求更新缓存。如果没有缓存将会使用网络请求。
优先:放心使用。用户会更新缓存。
缺点:会占用用户带宽。
Cache first
返回缓存的响应。如果请求没有在缓存中,将会请求,然后把响应缓存起来。
Network first
网络请求优先。收到响应更新缓存。如果网络失败,返回缓存的响应。
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返回的。
chrome缓存优先级:
- Memory cache。 浏览器自身优化。
- Service worker 。 开发者自己控制。
- Disk cache。http 强缓存 或者 304响应的协商缓存。
ps:该文章认为:
- service worker 大于 memory cache,但经过我个人实验,事实并非如此。memory cache 是浏览器自身优化行为,优先级较高。
- 经过service worker,但是service worker是调用fetch方法实际上发起请求的,network会出现两条记录,一条表示浏览器的请求从service worker返回的结果,一条表示service worker发起请求的具体内容:
如果请求命中强缓存的:
在application下的service worker面板,可以快速勾选offiline、update on reload、bypass for network。
- 当我们需要离线测试service worker在离线下的表现时,需要勾选offline。
- 当我们不需要service worker的能力时,可以选择bypass for network。
在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页面,我们需要到浏览器输入网址或者打开书签,相比于原生应用可以直接点开手机主屏上的应用图标,这样使用链路太长,太麻烦了。
所以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()
}
})
}
在拿到权限之后,在主线程可以通过实例化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
// ...
在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
下面是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 如何实现安全性:
- 应用服务器创建一对公钥/私钥,并将公钥提供给 Web App 客户端
- 当用户尝试订阅推送服务时,将公钥添加到 subscribe() 订阅方法中,公钥将被发送到推送服务保存。
- 应用服务器想要推送消息时,发送包含公钥和已经签名的 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进行调试。
总结
虽然国内PWA用的不是很多,但是service worker强大的能力,相比大家已经感受到,有离线缓存、预缓存、网络代理等需求的可以尝试一下。
参考
developers.google.com/web/tools/w…
developers.google.com/web/fundame…