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。
2. 优点/缺点
优点
- 拦截网络请求
- 缓存可用时返回缓存内容
- 对缓存内容进行管理
- 向客户端推送信息
- 后台数据同步
- 资源预取
缺点
- Web Worker的限制
3. 生命周期
Service Worker 的生命周期与 web 页面完全分离。它包括以下几个阶段:
- 下载
- 安装
- 激活
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。
- 用户导航至站点时,浏览器会尝试在后台重新下载定义 Service Worker 的脚本文件。 如果 Service Worker 文件与其当前所用文件存在字节差异,则将其视为新 Service Worker。
- 新 Service Worker 将会启动,且将会触发 install 事件。
- 旧 Service Worker 仍控制着当前页面,因此新 Service Worker 将进入 waiting 状态。
- 当网站上当前打开的页面关闭时,旧 Service Worker 将会被终止,新 Service Worker 将会取得控制权。
- 新 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. 浏览器兼容性
三、PWA
1. 什么是PWA ?
PWA(Progressive Web Apps,渐进式 Web 应用)运用现代的 Web API 以及传统的渐进式增强策略来创建跨平台 Web 应用程序。
2. 优点/缺点
优点
可被发现、易安装、可链接、独立于网络、渐进式、可重用、响应性和安全的。
- 渐进式 - 适用于所有浏览器,因为它是以渐进式增强作为宗旨开发的
- 连接无关性 - 能够借助 Service Worker 在离线或者网络较差的情况下正常访问
- 类原生应用 - 由于是在 App Shell 模型基础上开发,因此应具有 Native App 的交互,给用户 Native App 的体验
- 持续更新 - 始终是最新的,无版本和更新问题
- 安全 - 通过 HTTPS 协议提供服务,防止窥探,确保内容不被篡改
- 可索引 - manifest 文件和 Service Worker 可以让搜索引擎索引到,从而将其识别为『应用』
- 黏性 - 通过推送离线通知等,可以让用户回流
- 可安装 - 用户可以添加常用的 Web App 到桌面,免去到应用商店下载的麻烦
- 可链接 - 通过链接即可分享内容,无需下载安装
缺点
- 对系统功能的访问权限较低
- 没有审查标准
3. 核心技术
- Web App Manifest
Web App Manifest(Web 应用程序清单)概括地说是一个以 JSON 形式集中书写页面相关信息和配置的文件。
- start_url 可以设置启动网址
- icons 会帮我萌设置各个分辨率下页面的图标
- background_color 会设置背景颜色, Chrome 在网络应用启动后会立即使用此颜色,这一颜色将保留在屏幕上,直至网络应用首次呈现为止
- theme_color 会设置主题颜色
- display 设置启动样式
- Service Worker
- Notifications API 通知API
- Push API 推送API 推送 API 可以用来从服务端推送新的内容而无需客户端介入,它是由应用的 Service Worker 来实现的;通知功能则可以通过 Service Worker 来向用户展示一些新信息,或者至少提醒用户应用已经更新了某些功能。
4. App Shell
App Shell 是页面能够展现所需的最小资源集合,即支持用户界面所需的最小的 HTML、CSS 和 JavaScript 等静态资源集合。App Shell架构是构建PWA的一种方式,这种应用能可靠且即时地加载到您的用户屏幕上,与原生app相似。PWA 多数采用单页应用(Single Page Application)的方式编写,这样能减少页面跳转带来的开销,并且开发者可以在页面切换时增加过渡动画,避免出现加载时的白屏。那么在页面切换时页面上固定不动的内容就是 App Shell 的一部分。应用从显示内容上可以粗略的划分为内容部分和外壳部分。App Shell 就是外壳部分,即页面的基本结构。比如header,sidebar。
主要功能-都依赖于 Service Worker
1. 离线
- 让我们的Web App在无网(offline)情况下可以访问,甚至使用部分功能,而不是展示“无网络连接”的错误页。
- 在正常的网络情况下,也可以通过各种自发控制的缓存方式来节省部分请求带宽
- 在网络不好的时候,能使用缓存快速访问我们的应用,提升体验
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 发送消息给用户。
要完成消息推送并展示,需要经过下面几个步骤:
- web客户端 注册SW并向push service发起消息订阅请求,并将订阅信息(subscription)保存起来
- web server从 web客户端 处拿到subscription
- web server向subscription中的目的地(endpoint,其中包含了push service的地址)发送消息
- push service收到消息,转发给browser
- browser唤醒SW,将消息发给它
- SW收到消息,展示出来
- push service
和browser相关联的专门用来处理通知的服务,用来接受web server推送的消息。每个浏览器都有自己的push service
四、基于 Angular 的 PWA 消息通知
使用 web-push 完成订阅和推送。
第一步,客户端请求订阅用户:
一旦用户授权,浏览器就会生成一个PushScription,pushSubscription 包含公钥和 endpointURL,应用服务器推送时可以使用公钥对消息加密,endpointURL 是由推送服务器生成包含唯一标识符的 URL,推送服务器通过它判断将消息发送到哪个客户端。
第二步,应用服务器发送web push协议标准的api,触发推送服务器的消息推送:
应用服务器发送消息推送请求(目的是为了将更新推送到用户的浏览器),为了向推送服务器发出请求,需要查看先前获得的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事件并展示:
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);
});
- 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 的功能十分强大。有兴趣的同学可以自己试试离线和后台同步的功能。