背景
Web 应用的现状
- 网络资源下载带来的网络延迟
- Web 应用依赖于浏览器作为入口
- 没有很好的离线使用方案
- 没有好的消息通知方案
- ….
针对以上问题,结局方案出现了PWA
PWA简介
PWA全称Progressive Web App,即渐进式WEB应用。 一个 PWA 应用首先是一个网页, 可以通过 Web 技术编写出一个网页应用. 随后添加上 App Manifest 和 Service Worker 来实现 PWA 的安装和离线等功能
PWA主要特点
- 可靠-即使在不稳定的网络环境下,也能瞬间加载并展现
- 体验-快速响应,并且有平滑的动画响应用户的操作
- 粘性-像设备上的原生应用,具有沉浸式的用户体验,用户可以添加到桌面
PWA特点的实现
- 可靠-离线缓存- Service Worker
- 体验-web 存储- App Shell 模型
- 粘性-吸引留住用户(添加到主屏幕和网络推送通知)- manifest.json
Service Worker
- 可靠-离线缓存- Service Worker
- 体验-web 存储- App Shell 模型
- 粘性-吸引留住用户(添加到主屏幕和网络推送通知)- manifest.json
Service Worker
Service Worker 是浏览器在后台独立于网页运行的脚本,它打开了通向不需要网页或用户交互的功能的大门。 Service Worker从英文翻译过来就是一个服务工人,服务于前端页面的后台线程,基于Web Worker实现。有着独立的js运行环境,分担、协助前端页面完成前端开发者分配的需要在后台悄悄执行的任务。
客户端访问,通过Service Worker 服务,判断请求内容从哪里取的,如果缓存中存在,直接 取缓存,否则就走网络Service Worker功能
- 推送通知 — 激活沉睡的用户,推送即时消息、公告通知,激发更新等。如web资讯客户端、web即时通讯工具、h5游戏等运营产品。
- 离线缓存 — 可编程拦截代理请求和返回,缓存文件,缓存的文件可以被网页进程取到(包括网络离线状态),将H5应用中不变化的资源或者很少变化的资源长久的存储在用户端,提升加载速度、降低流量消耗、降低服务器压力
- 事件同步 — 确保web端产生的任务即使在用户关闭了web页面也可以顺利完成。如web邮件客户端、web即时通讯工具等。
- 定时同步 — 周期性的触发Service Worker脚本中的定时同步事件,可借助它提前刷新缓存内容。如web资讯客户端
Service Worker特性
- Service Worker 是一种可编程网络代理,让您能够控制页面所发送网络请求的处理方式
- 它是一种 JavaScript Worker,无法直接访问 DOM
- 必须在 HTTPS 环境下才能工作
- Service Worker 广泛地利用了 promise,异步实现
- Service Worker 在不用时会被中止,并在下次有需要时重启, Service Worker线程中不能保存需要持久化的信息
浏览器支持情况
看上图可以看出来,目前浏览器的支持情况还是很可观的,基本上市场占比很大的浏览器目前都是支持的,可喜可贺。
- Service Worker 是一种可编程网络代理,让您能够控制页面所发送网络请求的处理方式
- 它是一种 JavaScript Worker,无法直接访问 DOM
- 必须在 HTTPS 环境下才能工作
- Service Worker 广泛地利用了 promise,异步实现
- Service Worker 在不用时会被中止,并在下次有需要时重启, Service Worker线程中不能保存需要持久化的信息
浏览器支持情况
查看当前页面是否有Service Worker
我们打开浏览器的控制台,查看Service Workers ,会给我们展示出所有已经支持Service Workers 的网站Service Worker 生命周期
主要包含六种状态 解析成功(parsed),正在安装(installing),安装成功(installed),正在激活(activating),激活成功(activated),废弃(redundant)。
- 解析成功(Parsed):首次注册 Service Worker 时,浏览器解决脚本并获得入口点,如果解析成功,就可以访问到 Service Worker 注册对象(registration object)
- 正在安装(Installing):解析完成后,浏览器会试着安装,进入下一状态,“installing”,在 installing 状态中,Service Worker 脚本中的 install 事件被执行
- 安装成功/等待中(Installed/Waiting):如果安装成功,Service Worker * 进入安装成功(installed)(也称为等待中[waiting])状态
- 正在激活(Activating):处于 waiting 状态的 Service Worker,在以下之一的情况下,会被触发activating 状态:
- 当前已无激活状态的 workerService
- Worker 脚本中的 self.skipWaiting() 方法被调用
- 用户已关闭 Service Worker 作用域下的所有页面,从而释放了此前处于激活态的 worker
- 超出指定时间,从而释放此前处于激活态的 worker
- 激活成功(Activated):就可以应对事件性事件 —— fetch 和 message
- 废弃(Redundant):
- installing 事件失败
- activating 事件失败
- 新的 Service Worker 替换其成为激活态 worker 我们看一下在代码中是如何体现的
// main.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/sw.js')
.then(function (registration) {
// 注册成功
console.log('ServiceWorker registration successful with scope: ', registration.scope);
if (registration.installing) {
// Service Worker is Installing
console.log('Service Worker is Installing')
} else if(registration.waiting) {
// 这是更新新版本或自动更新缓存的绝佳时机
/*
* 当前没有激活的 worker
* 如果在 Service Worker 的脚本中 self.skipWaiting() 被调用
* 如果用户访问其他页面并释放了之前激活的 worker
* 在一个特定的时间过去后,之前一个激活的 worker 被释放
*/
console.log('Service Worker is Waiting')
} else if(registration.active) {
// 激活成功, Service Worker 是一个可以完全控制网页的激活 worker
console.log('Service Worker is active')
}
})
.catch(function (err) {
// 注册失败:(
console.log('ServiceWorker registration failed: ', err);
});
});
}
window.onload = function() {
document.body.append('PWA!')
}
Service Worker 事件
- Install:Service Worker 注册并安装完成后,对站点离线访问最关键的资源 URL 列表,它们通常也是关键请求链包含的文件,并将其缓存进 caches 中
- Activate:安装完成并激活后,一个管理老旧缓存的好地方, 在一般 PWA 中,我们可以结合版本号和缓存名,及时删除过期缓存
- Fetch:页面受控后所有请求会被 Service Worker “劫持”,根据资源类型动态返回缓存数据或请求新数据
在代码中的具体实现
// sw.js
/*
sw.js 控制着页面资源和请求的缓存
*/
// 监听 service worker 的 install 事件,
// 对站点离线访问最关键的资源 URL 列表,它们通常也是关键请求链包含的文件,并将其缓存进 caches 中
self.addEventListener('install', function (e) {
// 如果监听到了 service worker 已经安装成功的话,就会调用 event.waitUntil 回调函数
e.waitUntil(
caches.open('v1').then(cache => {
// 通过 cache 缓存对象的 addAll 方法添加 precache 缓存
return cache.addAll([
'/main.js',
'/index.html',
'/'
]);
}).then(function() {
// 跳过waiting,直接进入active
console.log('Skip waiting!')
return self.skipWaiting()
})
);
});
/*
事件回调是一个管理老旧缓存的好地方
在一般 PWA 中,我们可以结合版本号和缓存名,及时删除过期缓存
*/
self.addEventListener('activate', function(e) {
const cacheStorageKey = 'v1'
e.waitUntil(
Promise.all(
caches.keys().then(cacheNames => {
return cacheNames.map(name => {
if (name !== cacheStorageKey) {
return caches.delete(name)
}
})
})
).then(() => {
console.log('Clients claims.')
// 通过clients.claim方法,更新客户端上的server worker
return self.clients.claim()
})
)
})
/*
对不同资源类型应用不同的请求/缓存策略
看看这些请求所请求的文件在我们的缓存里有没有,有的话就直接从缓存里拿,不用下载了。
这也是PWA最重要的功能之一
*/
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('v1')
.then(function (cache) {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});
// 推送消息
self.addEventListener('message', function(event) {
// Do stuff with postMessages received from document
console.log("SW Received Message: " + event.data);
});