1. 先说说PWA?
PWA = progressive web app = 渐进增强式web应用 = 谷歌也想分app市场的一杯羹搞出的东西 = 用js写本地app
由于是谷歌想搞点东西,所以支持pwa的主要还是安卓系统和谷歌浏览器。
单纯依赖网页的网站存在一些问题:
手机桌面入口不够便捷,想要进入一个页面必须要记住它的url或者加入书签
没网络就没响应,不具备离线能力
不像APP一样能进行消息推送
基于以上几个网页应用存在的问题,pwa横空出世,其核心就是想要脱离浏览器入口环境成为可以离线运行的app,实现native app可以做到的事情。
那么说回来,pwa想要实现离线运行,就必须要缓存能力与操控请求和响应的权利,拥有安全高效的底层功能。
PWA的核心技术是ServiceWorker:
ServiceWorker给前端开发者开放了内核大量的底层能力,比如,它给前端提供了细粒度操作请求缓存的底层原语,等同于给前端开放了操作HTTP Cache级别缓存的能力,与Fetch API结合,让前端具备了完全操控请求,响应,缓存的能力,这点对于pwa的实现至关重要。因此serviceWorker是pwa的核心。
实际应用中,为了兼容安卓与ios,serviceWorker的应用最广泛的还不在pwa上面,而是在网站的优化方面。
ServiceWorker能在网页优化中干些啥?
Service Worker 是 Chrome 团队提出和力推的一个 WEB API,用于给 web 应用提供高级的可持续的后台处理能力。
Service Worker 最主要的特点是:在页面中注册并安装成功后,运行于浏览器后台,不受页面刷新的影响,可以监听和截拦作用域范围内所有页面的 HTTP 请求。
基于 Service Worker API 的特性,结合 Fetch API、Cache API、Push API、postMessage API 和 Notification API,可以在基于浏览器的 web 应用中实现如离线缓存、消息推送、静默更新等 native 应用常见的功能,以给 web 应用提供更好更丰富的使用体验。
特点
网站必须使用 HTTPS。除了使用本地开发环境调试时(如域名使用 localhost)
运行于浏览器后台,不受页面刷新的影响,可以控制打开的作用域范围下所有的页面请求
单独的作用域范围,单独的运行环境和执行线程,可监听和拦截注册Scope下的所有请求和响应
不能操作页面 DOM。但可以通过事件机制来处理
有独立的与html无关的生命周期
生命周期
参考 Service Worker 的生命周期,使用 Service Worker 大概需要如下几个过程。
install -> installed -> actvating -> Active -> Activated -> Redundant
收到事件SW线程要启动,也意味着事件处理完成,SW线程是需要关闭的。SW有独立的GlobalScope,独立的Isolate,独立的JS运行环境,SW线程的资源消耗是非常大的,事件驱动是减少SW线程资源消耗的一种有效的方式,这就是SW被设计成事件驱动的原因。
生命周期包含两部分,一部分是脚本,一部分是线程。
SW脚本的状态是存储在数据库的,打开页面时,会先从数据库中读取当前页面activated状态的SW脚本,然后再派发Fetch事件去启动SW线程。SW要控制页面,脚本是activated状态,线程是running状态,两者缺一不可,而这两者的生命周期都与页面文档无关。这就是SW文档无关生命周期的内在涵义。
sw脚本(sw.js) => 脚本状态存储数据库LevelDB开启(activated) => 派发fetch事件启动线程 =>
sw线程 => 事件驱动(running)=> sw所控制的页面
SW脚本激活之后会存储相关信息到LevelDB数据库,再次访问页面时,可以直接从注册数据库里读取信息,然后派发Fetch事件去启动SW线程,SW线程启动完成之后,所有的Fetch请求都会触发fetch事件,前端可以监听fetch事件,按照各种策略去获取资源。
拦截注册Scope下所有的请求和响应
Scope内的页面,所有的请求都会经过SW,由内部对象负责处理。内部对象会检查这些资源是否在SW缓存,如果在SW缓存,就会创建读取缓存的任务,直接从SW缓存读取;如果不在SW缓存,就会创建写入缓存的任务,继续走到网络流程,并将结果写入SW缓存。如果请求不受SW控制,会直接进入网络请求,走正常请求的流程。
具有可靠的能力
web前端服务常被认为是不够可靠(Reliable)的,网络会断,用户停留时间无法掌控,没有本地缓存机制。现在有了sw了,这些问题都可以解决了。读不不读取缓存,更不更新scope,都在web前端的掌控中。
可以精细的控制每个资源的缓存,让资源更可靠;
可以用push来预加载,让资源减轻即时的网络依赖;
可以使用sync触发后台更新;
可以使用fetch实现后台的上传下载
sw代码实现
注册
在网站页面上注册实现 Service Worker 功能逻辑的脚本。例如注册 /sw/sw.js 文件,参考代码:
f('serviceWorker'innavigator) {
navigator.serviceWorker.register('/sw/sw.js', {scope: '/'})
.then(registration => console.log('ServiceWorker 注册成功!作用域为: ', registration.scope))
.catch(err => console.log('ServiceWorker 注册失败: ', err));
}
以上代码的作用就是告诉浏览器,你要读我的sw.js文件哦,里面有配置哟!
Service Worker 的注册路径决定了其 scope 默认作用范围。示例中 sw.js 是在 /sw/ 路径下,这使得该 Service Worker 默认只会收到 /sw/ 路径下的 fetch 事件。如果存放在网站的根路径下,则将会收到该网站的所有 fetch 事件。
如果希望改变它的作用域,可在第二个参数设置 scope 范围。示例中将其改为了根目录,即对整个站点生效。
另外应意识到这一点:Service Worker 没有页面作用域的概念,作用域范围内的所有页面请求都会被当前激活的 Service Worker 所监控。
sw.js
const CACHE_NAME = "lzwme_cache_v1.0.0"; // 用于标注创建的缓存,也可以根据它来建立版本规范
// 列举要默认缓存的静态资源,一般用于离线使用
const urlsToCache = [ '/offline.html', '/offline.png' ];
// self 为当前 scope 内的上下文
self.addEventListener('install', event => {
// event.waitUtil 用于在安装成功之前执行一些预装逻辑, 但是建议只做一些轻量级和非常重要资源的缓存,减少安装失败的概率
// 安装成功后 ServiceWorker 状态会从 installing 变为 installed
event.waitUntil (
caches.open(CACHE_NAME).then(cache => { // 使用 cache API 打开指定的 cache 文件
console.log(cache); // 添加要缓存的资源列表
return cache.addAll(urlsToCache);
})
);
});
sw.js 内监听了 install 事件。当 sw.js 被安装时会触发 install 事件,监听该事件可执行安装时要做的事情。示例中是缓存用于离线时使用的静态资源,这也是最常见的行为。
需要注意的是,只有 urlsToCache 中的文件全部安装成功,Service Worker 才会认为安装完成。否则会认为安装失败,安装失败则进入 redundant (废弃)状态。所以这里应当尽量少地缓存资源(一般为离线时需要但联网时不会访问到的内容),以提升成功率。
安装成功后,即进入等待(waiting)或激活(active)状态。在激活状态可通过监听各种事件,实现更为复杂的逻辑需求。具体参见后文事件处理部分。
更新
如果 sw.js 文件的内容有改动,当访问网站页面时浏览器获取了新的文件,它会认为有更新,于是会安装新的文件并触发 install 事件。但是此时已经处于激活状态的旧的 Service Worker 还在运行,新的 Service Worker 完成安装后会进入 waiting 状态。直到所有已打开的页面都关闭,旧的 Service Worker 自动停止,新的 Service Worker 才会在接下来打开的页面里生效。
如果希望在有了新版本时,所有的页面都得到及时更新怎么办呢?
可以在 install 事件中执行 skipWaiting 方法跳过 waiting 状态,然后会直接进入 activate 阶段。接着在 activate 事件发生时,通过执行 clients.claim 方法,更新所有客户端上的 Service Worker。
// 安装阶段跳过等待,直接进入 active
self.addEventListener('install', function(event) {
event.waitUntil(self.skipWaiting());
});
self.addEventListener('activate', event => event.waitUntil(
Promise.all([
// 更新客户端
clients.claim(),
// 清理旧版本
caches.keys().then(cacheList => Promise.all(
cacheList.map(cacheName => {
if(cacheName !== CACHE_NAME) {
caches.delete(cacheName);
}
})
))
])
))
另外要注意一点,sw.js 文件可能会因为浏览器缓存问题,当文件有了变化时,浏览器里还是旧的文件。这会导致更新得不到响应。如遇到该问题,可尝试这么做:在 webserver 上添加对该文件的过滤规则,不缓存或设置较短的有效期。注意,不要想着不同版本使用不同的文件名称,这会带来混乱的问题。
参考文章: