PWA是什么

人们追求了极限这么多年,怎么能达到像原生一样的应用机制呢?最大的区别莫不是在于是不是动态加载资源,大家都知道APP很多资源安装包都包括了,所以Web很难超越。突然,一群牛B的谷歌工程师相处了一个解决方案,然后他们就在2016年Google I/O大会上提出了一个Next Web Generation的概念,也就是所谓的PWA,全称Progressice Web Apps(渐进式的网页应用),其实这不是一个单独的技术,而是一些技术的合计。它可以提供更好的缓存机制,可以提供更多的原生硬功功能,下面咱们一起走进PWA的时间去转转。

PWA中主要技术点

PWA整体主要用到以下技术内容:

  • Service Workers:主要用来控制缓存内容。
  • Fetch API:一种比XMLHttpRequest更底层的API,意在统一浏览器的各种fetch,,使他们表现的更为一致。Fetch API中还定义了Response和Request对象接口,借此我们可以更方便的操作HTTP请求和相应。
  • App Manifest:可以支持PC与M端的桌面安装图标,以及首页内容配置。
  • Push Notification:消息推送机制,包含Notification和Push API两部分组成,前者用于向用户展示通知,后者用于订阅推送消息。
  • Background-sync:可以存储断网时浏览器的请求,当下一次连上网时会发送请求。

PWA特点介绍

PWA是一种可以在网页中呈现出让人惊讶的效果的方法,PWA具备以下几个特色:

  1. Reliable(可靠性):系统始终会为用户呈现出来页面,即使网络环境很差甚至无网络。
  2. Fast(快速):能够提供快速的用户相应,让用户拥有顺畅的操作体验。
  3. Engaging(引人入胜的):可以让web在设备上看起来像一个应用程序,提供沉浸式的用户体验。

拥有了以上几点优势,让PWA也能够在在用户的屏幕上赢得一席之地。
但因为其缓存行,也会给不法分子提供更好的攻击途径,所以PWA在使用时,会有一些限制来保证用户网站能达到流程的用户体验的同时还保证用户的安全。
注意事项:

  1. PWA中Service-worker只能在https的域名中才可以注册,并对网站进行缓存,但谷歌为了提供开发环境,PWA也支持localhost或127.0.0.1本地域名注册。
  2. Services-workers在缓存时也只能缓存对应的https得请求文件。

ServiceWorker

使用PWA缓存,最终要的是需要在页面中使用注册一个ServiceWorkers,并且此功能是单独的一个线程,区别于当前页面线程,所以在ServiceWorkers中是无法操作任何dom元素的。首先我们先了解一下ServiceWorkers的生命周期:
当页面首次安装ServiceWorkers时,会经历一下步骤:

  1. 首先会在浏览器中注册一个进程,并安装注册的ServiceWorkers文件。
  2. 等待安装完成,在等待完成过程中,fetch,push等事件不会触发。
  3. 安装完成之后进入Activate状态,此时会可以使用ServiceWorkers中的完整功能。

谷歌官方提供了动态示意图来更好的了解首次安装的过程,如下图所示:


来看看ServiceWorker兼容性:
ServiceWorker兼容性
因为是比较新的技术,所以现在只能兼容到比较主流的浏览器,所以在注册时我们需要进行向下兼容。
PWA的功能导致会出现很多安全隐患,所以只能在localhost或者https的环境下才可被注册,所以接下来的demo,我采用一个vue的单页面应用来介绍,demo可以直接从我的github上下载来直接运行查看。
我们先准备一个html

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta content="telephone=no" name="format-detection"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport" />
<title>SP1A</title>
</head>
<body>
<div id="app"></div>
</body>
</html>

我们先来注册一下ServiceWorker:

1
2
3
4
5
6
7
8
9
10
11
12
13
 <script type="text/javascript">
window.addEventListener('load', function () {
if('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js?'+new Date().getTime(),{scope: '/'})
.then(reg => {
console.log('Server worker registered!');
})
.catch(err => {
console.log('Server worker registered Fail!', err)
})
}
});
</script>

我们采用了在body尾注册ServiceWorder,在此位置加载不会阻止Dom渲染以及css加载的渲染,注册过程中我们已经做了向下兼容,当系统支持serviceWorder时才会注册,否则直接跳过。
具体来看我们注册了一个叫“sw.js”的文件,此文件为ServiceWorker的主文件。

sw.js文件

1
2
3
4
5
6
7
8
//监听
var filesToCache = [
'/app.js'
]
console.log(self);
self.addEventListener('install', function(e) {

})

ServiceWorker本身,并非window本身,来看下打印出来的self具体为何物:
Server-Worker中的self对象内容
ServiceWorker在浏览器注册时,会开启单独线程,并行与浏览器的进程,ServiceWorker中self指向的是ServiceWorker的对象,所以在ServiceWorker中是无法操作DOM对象,只能操作ServiceWorker的接口API,ServiceWorker已经注册成功。
未使用waitUntil控制的SerivceWorker

在ServiceWorker进入activate(激活)状态前,我们可以提前缓存一些已知文件,来达到更好的效果,这时候我们可以使用waitUntil函数。

waitUntil

当ServiceWorker注册成功之后,并且在进行拦截之前,我们想提前缓存一部分已知的确定文件,此时,我们可以使用waitUntil函数,它可以使ServiceWorker无法直接进入到生命中期中的activite(激活)状态。

1
2
3
4
5
6
7
8
9
10
11
var cacheName="testcache";
var filesToCache = [
'/app.js'
]
self.addEventListener('install', function(e) {
e.waitUntil(
caches.open(cacheName).then(function(cache) {
return cache.addAll(filesToCache);
})
)
})

我们成功的在ServiceWorker第一次激活前创建了一个名为“testcache”的缓存数据,并且缓存了app.js.
第一次加载时已经成功缓存了app.js

激活之后请求的文件我们还需要继续缓存,接下来我们借助Fetch来进行后续的文件缓存工作。
来看看Fetch兼容性,基本也是最新浏览器的兼容程度,不用担心ServiceWorker注册后Fetch不兼容问题:
fetch兼容性

fetch功能

Fetch提供了一个获取资源的接口,任何使用过XMLHttpRequest的人都能轻松上手,但新的API提供了更强大和灵活的功能集,此处我们就不过多介绍Fetch功能,我们主要来看Fetch如何与ServiceWorker结合工作。
Fetch可以拦截任何形式的请求,所以我们可以在拦截到请求的时候进行判断,如果缓存中存在,可以不需要去请求服务器,如果不存在则去请求服务器,并进行缓存,以备下次使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
self.addEventListener('fetch', function(e) {
e.respondWith(
//首先我们把拦截到的请求与cache中的资源做对比
caches.match(e.request)
.then(function(response){
//如果存在则直接返回缓存
if(response) {
return response;
}
//如果不存在,我们会通过fetch来请求新的资源。
var fetchRequest = e.request.clone();
return fetch(fetchRequest)
.then(
response => {
//请求接口报错时直接返回,不进行缓存
if(!response || response.status != 200) {
console.log('%c因为错误不缓存:','color: green',e.request.url);
return response;
}
var responseToCache = response.clone();
caches.open(cacheName)
.then(function(cache) {
cache.put(e.request, responseToCache);
})
return response;
}
)
})
)
})

Fetch函数会拦截所有注册页面中的请求,并且针对请求来判断是否在缓存中,如果存在则可以直接返回缓存内容,无需请求,如果不存在从服务器请求。
值得注意的一点是:response与request都是单次对象,如果使用了就会消失,所以我们在使用之前要进行clone,才能更好的进行下面的工作。

更新Service

上面说了如何进行简单的PWA配置,接下来我们来简单聊聊更重要的环节,如何进行更新。
我们先看看,第二次打开页面进行注册时,整个Service Workerde运行过程:


我们需要在waitUntil中添加一个跳过等待的方法,否则新的sw会永远处于等待状态,如下图。
fetch兼容性
针对此情况,我们在install周期时进行如下调整:

1
2
3
4
5
6
7
8
9
10
11
self.addEventListener('install', function(e) {
console.log('Service Worker installed');
e.waitUntil(
caches.open(cacheName).then(function(cache) {
cache.addAll(filesToCache);
//跳过等待
self.skipWaiting();
return ;
})
)
})

接下来,新注册的ServiceWorker使用的缓存名字换成了新的名字,激活之后从列表中查找,如果没有回删除掉其他的重新创建新的,我们需要把self的作用域提升到新的ServiceWorker中,使用self.clients.claim()即可做到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
self.addEventListener('activate', function(e) {
console.log('Service Worker Activate');
e.waitUntil(
Promise.all([
self.clients.claim(),
caches.keys().then(function(cacheList) {
return Promise.all(
cacheList.map(function(cn) {
if(cn !== cacheName) {
return caches.delete(cn);
}
})
)
})
])
)
})

好了,我们初步了解了一下如何通过Service Worker建立缓存机制,并且也了解了更新缓存的简单方法。如对PWA感兴趣,可以持续关注我,后续会针对ServiceWorker进行扩展的讲解。

参考资料: