基础介绍
在开始介绍 PWA 的具体应用前,我们可以先介绍下 PWA 是什么。 PWA 是一系列的技术能力,可以用户增强 Web 的功能,可以让传统的 Web 网站对移动端用户更加友好。最终期望传统的 Web 网站在移动端可以达到接近 APP 的能力
PWA 另一个特点是来自于其名字,Progressive web apps,从名字上可以看到相关的技术都是渐进式增强的,因此技术人员可以根据需要增强,而且在不支持新技术的情况下应该可以回到最初的能力。
具体的特点
- 可安装性,类似原生 APP,可以被安装在主屏幕上
- 可靠性,可以在离线或网络很差时使用
- 可连接,有新内容可以通知给用户
- 安全性,与应用间的连接应该是安全的
文档 还列举了一些其他的特点,但是这些是 PWA 最突出的特点,后面的介绍也主要涉及 PWA 的这些特点
PWA 与 Github Page
Github Page 是 Github 提供的一个博客托管途径,技术人员可以在不需要自己的服务器和域名即可搭建一个博客服务,而且定制化程度还比较高,个人博客就是长期托管在 Github Page 上。这边的介绍就是以 Github Page 为例进行介绍,如何将 Web 网站改造为 PWA。
由于 Github Page 本身就是采用 https 的,因此安全性已经得到的满足,因此需要关注的特点主要在于可安装性,可靠性,可连接的特点上,对应的技术分别是:
Manifest
,此技术是 A2HS 的实现,可以将网站安装至主屏幕Service Worker 与 Cache
,这两种技术可以实现对网站的响应进行缓存,从而实现离线访问Web Push 和 Notification
,这两种技术可以实现将消息推送并通知给用户,从而实现可连接性
下面就分别对涉及到技术进行梳理:
Manifest
Manifest 是就是一个 json 格式的配置文件,用于浏览器识别并正确展示,主要包含的字段包括下面:
{
"name": "Bryan Blog", // 名字
"short_name": "Bryan Blog", // 缩略名,空间不够时展示缩略名
"icons": [ // 应用的图标
{
"src": "icons/128.png",
"sizes": "128x128",
"type": "image/png"
},
],
"background_color": "#fff", // 背景色
"start_url": "/", // 启动时加载资源你的路径
"display": "standalone", // 展示模式,fullscreen 是全屏,standalone 是独立应用模式,影响最终 App 展示效果
}
在 html 里面正确连接此配置文件即可
<link rel="manifest" href="path/to/manifest.json">
按照这个配置之后浏览器应该就可以正确识别,并可以转换为 App 了
当然浏览器可能还会对你有额外的要求,比如 Chrome 还要求网站是 https,并且要求支持离线缓存,但是理论上安装只和 Manifest 相关。
由于本人使用 ios 系统,按照 ios 的要求,需要在 html 中增加如下配置文件
<link rel="apple-touch-icon" href="path/to/icon.png">
通过额外的参数增加图标的图片,最终即可在 iphone 中正确地将应用添加至主屏幕上,最终展示的效果如下所示:
通过前面介绍可以看到 Manifest 本身并不对 Web 网站进行复杂的改造,只是单纯地支持了将应用的快捷方式添加到桌面,让访问更加的便利
离线缓存
为了支持离线缓存需要使用 Service Worker 与 Cache 机制,下面分别进行介绍:
Service Worker
Service Worker 是一种独立于浏览器网页的脚本,可以在单独的线程中执行任务,利用 Service Worker 执行任务可以避免阻塞主线程。
另外 Service Worker 还具有一个巨大的优势,就是 Service Worker 可以截获网络请求,因此利用 Service Worker 很容易实现网络代理,也可以比较方便地实现离线缓存的功能
Service Worker 的使用也十分简单,一般情况下,将 Service Worker 相关的功能实现在独立的文件中,一般叫做 sw.js
,然后将相关的功能通过 navigator.serviceWorker.register()
注册上,功能就生效了。
-
Service Worker 注册
为了保证渐进式地增强,注册时需要判断是否支持 Service Worker,如果支持则注册,相关的代码如下所示:
if(navigator.serviceWorker){ // 判断浏览器是否支持 navigator.serviceWorker .register('/sw.js') .then((registration) => handleRegistration(registration)) .catch((error) => {console.log('ServiceWorker registration failed: ', error)} }
-
功能实现
Service Worker 是事件驱动的,可以根据需要在特定的事件上注册需要的功能,为了实现网络请求的拦截,可以监听
fetch
事件即可,顺便说一句,Service Worker 中都是基于 Promise 进行任务处理的。为了实现离线缓存,我们实现的功能如下所示:self.addEventListener('fetch', event => { // 根据 event.request 获取到网络请求信息,构造返回数据 })
我们可以根据自己的需要选择合适的策略去构造返回数据,可以选择优先从缓存中获取数据,也可以选择优先从网络请求中获取。
Cache
Cache API 是一个特别适合用于缓存网络请求的技术,存储的键是网络请求 Request,存储的值是网络请求返回值 Response。
在使用时可以通过caches.match(request)
匹配是否存在对应缓存,在需要更新缓存时通过caches.put()
增加缓存
离线缓存实现
在实现离线缓存时,主要在 fetch
事件的处理方法中,根据网络请求以及实际的业务需要选择合适的资源缓存策略,一般情况下存在下面的策略:
- Cache Only 仅缓存,适合用于处理不变而且预加载的静态资源
- Network Only 仅网络,适合用于处理实时性极高的网络资源,没有缓存的价值
- Cache First 优先缓存,适合实时性要求不太高的网络资源,使用缓存可以避免重复请求
- Network First 优先网络,适合实时性相对高,但是也能接受缓存数据的请求
- Stale While Revalidate 优先缓存,每次都更新缓存,网络请求返回比较快,同时也能保证实时性,适合范围较广
在实现离线缓存中,还有一个值得关注的优化机制是预缓存,对于服务中的静态资源,可以使用预缓存大大加快请求响应速度,在 Service Worker 中一般在 install
时执行预缓存,为了保证资源的更新,因此还需要执行缓存资源的清理,一般会在 activate
时执行资源的清理。
预缓存一般会调用 Cache 的 addAll()
去加载多个资源,代码如下所示:
self.addEventListener('install', e => {
e.waitUntil(
caches.open(CACHE).then(cache => {
return cache.addAll(PRECACHE_LIST) // PRECACHE_LIST 是预加载的资源列表
.then(self.skipWaiting())
.catch(err => console.log(err))
})
)
});
通知机制
通知机制的实现主要涉及到两个独立功能,分别是消息的推送与消息的通知。
消息推送
对于消息推送机制存在 公共的标准 ,基本的流程如下所示:
+-------+ +--------------+ +-------------+
| UA | | Push Service | | Application |
+-------+ +--------------+ | Server |
| | +-------------+
| Subscribe | |
|--------------------->| |
| Monitor | |
|<====================>| |
| | |
| Distribute Push Resource |
|-------------------------------------------->|
| | |
: : :
| | Push Message |
| Push Message |<---------------------|
|<---------------------| |
| | |
涉及到三个部分:
- UA,用户代理,即当前用户的浏览器
- Push Service,推送服务器,需要支持推送的服务的支持,Google 提供了 Push Service 的实现 FCM ,可用于帮助实现消息推送,可惜国内被墙无法使用
- Application Server,应用服务器,一般是业务本身的服务器,在需要时可以发起消息推送
基础的流程是:
- 用户在 Service Worker 中调用 pushManager 的
subscribe()
方法发起订阅,Push Service 接收到订阅后,返回资源信息 Distribute Push Resource, 此信息唯一标识一个客户端的订阅,可以用于向此客户端推送消息; - 用户将资源信息 Distribute Push Resource 发送给应用服务器,应用服务器保存后在需要的时候可以将信息发送给 Push Service,此时需要带上对应的资源信息,Push Service 会根据资源信息将消息推送给特定的用户;
通过上面的流程可以看到,整个流程并不复杂,但是由于 FCM 被墙,从用户到 FCM 的订阅无法完成,因此目前这一套流程比较难应用在实际的业务中
消息通知
消息通知是指在消息已经推送给用户浏览器的情况下,将消息通知给用户的过程
消息的通知使用的是 Notification,主要涉及到权限的申请与实际的消息通知
-
权限申请
权限申请就是调用
Notification.requestPermission()
申请下通知权限,代码如下所示:Notification.requestPermission( function(status) { // 值为 "granted" 时就是用于允许发送通知了 });
-
实际消息通知
实际发送通知消息通过
new Notification(title, {body: xxx})
即可发出一条通知消息
由于 Github page 是个人博客,没有复杂的通知需求,暂时没有加入个人 Github page 中,有需要可以自行加入通知机制
总结
基础的 PWA 功能与相关技术梳理完成,可以看到,主要的功能由于兼容性或其他的一些原因使用都比较缺失,虽然目前实用价值有限,但是不妨碍 PWA 给出了一些增强 Web 网站,对移动端用户更友好的建议,PC 上的 Web 服务面向移动端用户的增强与优化不会是一蹴而就,可以采纳渐进的策略,逐步增强,在不影响现有用户的情况下,给支持新特性的用户提供更优的体验。