
2016年 Google 在 I/O 大会上提出一个 Next Web Generation 的概念 —— PWA 横空出世,2017年始开始流行,目前虽未完全成熟,但越来越火爆,可以预见未来前景广阔。你还在等什么?

一、Hello PWA
PWA(Progressive Web App)[1], 渐进式 WEB 应用,是提升 Web App 的体验的一种方法,给用户原生应用的体验。PWA 可以通过 Service Worker, Manifest 等新技术让站点具备离线可用、添加到桌面、实时消息提醒等功能,从功能和体验上无限接近原生 App。
1.1. 背景
自1990年万维网之父-蒂莫西·约翰·“蒂姆”·伯纳·李爵士,创建了 HTTP、HTML 和 WorldWideWeb (全世界第一个网页浏览器)以来,Web 技术和影响力在以惊人的速度增长。HTML5,CSS3,Webpack,React,VUE,Babel,SPA 等技术的成熟与发展仿佛让 Web 进入了百家争鸣的春秋时期,Web应用能做的事情越来越多,大家对web的希望也越来越高。
但随着移动时代的到来,web 应用因为不能离线访问,没有快捷入口和页面频繁卡顿等开始失宠。除了原生应用因离线能力,瞬时加载和可靠性强等优点爆炸性崛起外,Hybrid ,React Native 等 APP 开发模式似乎也有点如日中天的“赶脚”。作为一名 Web 前端开发工程师已经瑟瑟发抖,你呢? 莫慌,老大哥 Google 的工程师们早就“抖”完了,并在2015年提出2016年推出 PWA ,号称 PWA 将成为 Web 颠覆者的契机。

从上图我们可以看出除了原生功能体验、渲染性能,支持设备底层访问,网络要求等四个方面外, Web App 对比 Native、Hybrid、React Native,还是占据一定优势的。更让人兴奋的是 PWA 一定程度上解决了 Web App 的“劣势”(图中黄色背景部分),让 Web APP 在保留原有优势的基础上渐进式接近原生 App。
1.2. 主要特点&优势

-
可靠 - 即使在不稳定的网络环境下,也能瞬间加载并展现
-
快速 - 快速响应,并且有平滑的动画响应用户的操作
-
粘性 - 像设备上的原生应用,具有沉浸式的用户体验,用户可以添加到桌面
以上列举的是PWA的三个最主要的特点,要想了解所有特点,可到 PWA官网[2] 查看。
1.3. 主要技术
PWA并不是描述一个技术,而是一个技术的合集,包含以下几个主要技术:
1. Service Worker(详见本文第二节);
2. App Manifest(详见本文第三节);
3. Push API:允许 Web 应用拥有接收服务器并推送消息的能力( Web App 内部的消息推送)。目前已得到安卓和 PC 上新版本主流浏览器的支持,ios平台不兼容;
4、Notifications API:允许 Web 应用向用户显示系统通知。目前已兼容大部分 PC 主流浏览器,尚不兼容移动端浏览器。
5、Background Sync:可延迟发送用户行为,直到用户网络连接稳定。目前几乎不兼容移动端浏览器,PC 上 Firefox、 Chrome、 Safari、 Edge 等浏览器均已兼容。可解决两个常见问题: - 普通的页面发起的请求会随着浏览器进程的结束/或者 Tab 页面的关闭而终止; - 无网环境下,没有一种机制能“维持”住该请求,以待有网情况下再进行请求。
二、Service Worker
2.1. 什么是 Service Worker?
将你的网络请求想象成飞机起飞,Service Worker 是路由请求的空中交通管制员。它可以通过网络加载,或甚至通过缓存加载。

空中交通管制员可以延迟甚至改变飞机的降落的机场,Service Worker 的行为方式也是如此,它可以重定向你的请求,甚至彻底停止。如上图所示,Service Worker 在浏览器和后端服务之间起到了“管制员”的作用,它可以让你全权控制网站发起的每一个请求,这为许多不同的使用场景开辟了可能性,离线访问只是其中一种。
2.2. 功能特性

关于 Service Worker 的功能特性,以下几点看似无聊,其实很重要,不妨开发过程遇到问题再回头看看。
-
要求 HTTPS 环境,开发过程中,一般浏览器也允许 host 为 localhost 或者 127.0.0.1;
-
运行在它自己的全局脚本上下文中;
-
不绑定到具体的网页,无法修改网页中的元素,因为它无法访问 DOM;
-
一旦被 install,就永远存在,除非被手动 unregister;
-
异步实现,内部大都是通过 Promise 实现,依赖 HTML5 fetch API[3];
-
Service Worker 的缓存机制是依赖 Cache API[4] 实现的;
2.3. 生命周期

Service Worker 包含以下几个生命周期:
-
正在安装(installing):发生在 Service Worker 注册之后,表示开始安装,触发 install 事件回调指定一些静态资源进行离线缓存,
install事件回调中有两个方法: -
event.waitUntil():传入一个 Promise 为参数,等到该 Promise 为 resolve 状态为止。 -
self.skipWaiting():执行该方法表示强制当前处在 waiting 状态的 Service Worker 进入 activate 状态。 -
已安装(installed):安装完成,等待其他的 Service Worker 线程被关闭。
-
正在激活(activating):处于 activating 状态期间,Service Worker 脚本中的
activate事件被执行。我们通常在activate事件中,清理 cache 中的文件。activate回调中有以下两个方法: -
event.waitUntil():传入一个 Promise 为参数,等到该 Promise 为 resolve 状态为止。 -
self.clients.claim():在 activate 事件回调中执行该方法表示取得页面的控制权, 这样之后打开页面都会使用版本更新的缓存。旧的 Service Worker 脚本不再控制着页面,之后会被停止。 -
已激活(activated):在这个状态会处理
activate事件回调 (提供了更新缓存策略的机会)。并可以处理功能性的事件fetch(请求)、sync(后台同步)、push(推送)等。 -
废弃状态(Redundant):这个状态表示一个 Service Worker 的生命周期结束。进入废弃 ( redundant ) 状态的原因可能为这几种:
-
安装 ( install ) 失败
-
激活 ( activating ) 失败
-
新版本的 Service Worker 替换了它并成为激活状态
2.4. 主要事件
Service Worker 是基于事件的,安装、激活、缓存、通信等操作都是需要在特定事件下操作,包含以下几个主要事件。
2.5. 使用 Service Worker 缓存
上面一小节,我们了解了常用的几个事件,本节让我们一起利用这些事件缓存资源。(看代码的时候注意注释哦)
(1) 注册
首先要注册 Service Worker, 我们需要注册 Service Worker 来启动安装。 sw.js 文件推荐在 HTML 当中引入:
<script>
if ('serviceWorker' in navigator) {
//如果浏览器支持 Service Worker API,在页面 onload 的时候注册位于 /sw.js 的 Service Worker。
//启动一个线程很耗时,建议放到onload事件中
window.addEventListener('load', function () {
navigator.serviceWorker.register('/sw.js', {scope: '/'})//scope 指定网域目录上所有事项的 fetch 事件
.then(function (registration) { // 注册成功
console.log('ServiceWorker registration successful with scope: ', registration.scope);
})
.catch(function (err) { // 注册失败
console.log('ServiceWorker registration failed: ', err);
});
});
}
</script>
(2) 通过 `install` 事件做静态缓存
接着我们往 sw.js 文件中添加逻辑,我们先尝试用 install 事件来做静态缓存。
为了帮助大家更好的理解下文的代码,请先熟悉上文生命周期中的几个方法和以下几个 Service Worker 的全局变量:
-
self: Service Worker 作用域;
-
caches: 缓存;
-
clients: Service Worker 接管的页面;
var cacheName = 'helloPwa1'; //缓存的名称
self.addEventListener('install', event => { //安装
//确保 Service Worker 不会在 waitUntil() 里代码执行完毕之前安装完成。
event.waitUntil(
//用我们指定的缓存名称来打开缓存
caches.open(cacheName)
//把 JavaScript 和 图片文件添加到缓存中
.then(cache => cache.addAll([
'./js/script.js',
'./images/hello.png',
'./images/logo.webp',
]))
);
});
/*敲黑板*/
//如果任何文件下载失败了,那么安装过程也会随之失败。如果文件列表很长会增加缓存失败的几率导致 Servicer Worker 无法安装。
(3) 通过 `fetch` 事件使用缓存和处理动态缓存
Service Worker 会拦截浏览器所有请求,并查询当前 cache,如果存在 cache 则直接返回,若不存在,则通过 fetch 方法向服务端发起请求,并返回请求结果给浏览器。
//监听fetch事件来拦截请求
self.addEventListener('fetch', function(event) {
event.respondWith(
//当前请求是否匹配缓存中存在的任何内容
caches.match(event.request)
.then(function(response) {
if (response) {
//如果匹配的话,就此返回缓存并不再继续执行
return response;
}
//克隆了请求。请求是一个流,只能消耗一次。(很重要一步)
var requestToCache = event.request.clone();
//尝试按预期一样发起原始的 HTTP 请求
return fetch(requestToCache).then(
function(response) {
//如果由于任何原因请求失败或者服务器响应了错误代码,则立即返回错误信息
if (!response || response.status !== 200) {
return response;
}
//再一次克隆响应,因为我们需要将其添加到缓存中,而且它还将用于最终返回响应
var responseToCache = response.clone();
//打开名称为 “helloWorld” 的缓存
caches.open(cacheName)
.then(function(cache) {
// 将响应添加到缓存中
cache.put(requestToCache, responseToCache);
});
return response;
}
);
})
);
});
(4) 通过 `active` 事件更新缓存
当我们将资源缓存后,除非注销 sw.js 或手动清除缓存,否则新的静态资源无法缓存。这个时候在我们可以在 activate 事件中检查 cacheName 是否变化,如果变化则表示有了新的缓存资源,则将原有缓存删除。所以在 sw.js中加入以下代码后,当需要更新缓存时,我们仅仅需要修改 cacheName 就可以了。
self.addEventListener('activate', function (e) {
var cachePromise = caches.keys().then(function (keys) {
return Promise.all(keys.map(function (key) {
if (key !== cacheName) {
return caches.delete(key);
}
}));
})
e.waitUntil(cachePromise);
return self.clients.claim();
});
除了通过 active 事件更新缓存,我们还可以在注册 Service Worker 的时候借助 Registration.update() 更新。
var version = '1.0.1';//每次更新改这个版本号即可
navigator.serviceWorker.register('/sw.js').then(function (registration) {
if (localStorage.getItem('sw_version') !== version) {
registration.update().then(function () {
localStorage.setItem('sw_version', version)
});
}
});
2.6. 兼容性

2.7. 小结
咳咳咳,关于 Service Worker,本文先聊这么多,后续会有专文跟大家讨论,请持续我们的微信公众号或jdc博客 http://jdc.jd.com/。
三、Manifest
3.1. Manifest是什么?
manifest 的目的是将Web应用程序安装到设备的主屏幕,为用户提供更快的访问和更丰富的体验。
3.2. 安装 Web App 到主屏幕条件
-
站点支持 HTTPS 访问;
-
站点部署
manifest .json; -
站点注册 Service Worker;
3.3. manifest.json
PWA 添加至桌面的功能实现依赖于 manifest.json 文件,一个基本的 manifest.json 文件应包含如下信息:
{
"name": "莎士比亚", //web app 的名称
"short_name": "莎士比亚", //简称,没有足够空间的时候显示
"description": "人工智能撰稿与设计平台", //简介
"icons": [{ //图标
"src": "./icon8.png",
"sizes": "150x150",
"type": "image/png"
}, {
"src": "./icon1.png",
"sizes": "250x250",
"type": "image/png"
}],
"background_color": "#fff", //背景颜色
"theme_color": "#000", //主题色
"start_url": "../index.html", //Web App 启动时的html文件,地址路径相对于mainfest.json文件
"display": "standalone", //显示类型: 包含 fullscreen,standalone,minimal-ui,browser
"orientation": "portrait" //显示方向,包含横屏,竖屏,自适应等
}
3.4. 引用manifest.json
在 manifest.json 配置的 start_url 对应的 html 文件的 head 标签中按如下方式引用即可:
<link rel="manifest" href="./manifest.json">
3.5. 使用 manifest 实现 Web APP 的启动页

通过配置 manifest.json 的下列属性,可以很容易的实现上图的 Web App 启动页:
-
设置图像和标题:标题则直接取自 name。浏览器会从 icons 中选择最接近 128dp 的图片作为启动画面图像。
-
设置启动背景颜色:支持
#ffffff,#fff,white,rgb (255 , 255, 255 )格式,其他不支持,如 rgba 。 -
设置启动显示类型:仅当显示类型 display 设置为 standalone 或 fullscreen 时,PWA 启动的时候才会显示启动画面。
3.6. 小结
看到这里,你已经可以动手去把你的 Web APP 添加到桌面啦,由于兼容性问题,建议在 Android 或 PC 上使用 Chrome 浏览器体验。(PC 上需要配置谷歌浏览器,打开“chrome://flags/”中允许Desktop PWAs即可)
写在最后的话
本文旨在和大家一起学习 PWA 的入门知识,分别介绍了 PWA 的现状,特点以及其核心技术 Service Worker 和 Manifest。后续还会有文章和大家一起深入学习 PWA。欢迎各位提出问题和建议,共同成长和进步。让热爱 Web 的我们一起书写 Web 的未来!
扩展阅读
[1]https://developers.google.com/web/progressive-web-apps/
[2]https://developers.google.cn/web/progressive-web-apps/checklist
[3]https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API
[4]https://developer.mozilla.org/zh-CN/docs/Web/API/Cache