PWA 的英文全称是 Progressive Web Apps,中文翻译过来就是渐进式 Web 应用。Google 在 2015 年开始推广这类无需下载的应用,运用现代的 Web API 以及传统的渐进式增强策略来创建跨平台 Web 应用程序,为网页提供 App 般使用体验的一系列方案。
PWA 实际上仍是网页,只不过它添加了App Manifest 和 Service Worker 来实现 PWA 的安装和离线等功能。
PWA 和小程序有什么区别
PWA 的特点是:无需安装、更轻量、不占用大量空间,只需要一款支持 PWA 应用的浏览器,就可以轻松添加 PWA 应用,具备了跨平台使用的特性。
而微信小程序这类应用,必须在安装微信的前提下才可以使用微信小程序,并且有些小程序强制用户关联微信账号后才能使用。与其相比,PWA 应用展示了更开放的一面。
优势
无需安装
快捷方式添加到桌面上,点击主屏幕图标可以实现启动动画以及隐藏地址栏
离线缓存功能,即使用户手机没有网络,依然可以使用一些离线功能
消息推送
内容可以通过搜索引擎发现
使用 URL 分享
在任何具有屏幕和浏览器的设备上可以正常使用
浏览器支持
PWA 所需的关键技术是 service worker 。 目前桌面和移动设备上的所有主流浏览器都支持 service worker。
其他的 Web App Manifest,Push,Notifications和 Add to Home Screen 功能也得到了广泛的支持。 目前,Safari 对 Web App Manifest 和 Add to Home Screen 的支持有限,并且不支持 Web 推送通知。 但是,其他主流浏览器支持所有这些功能。
准备
安装 http-server
通过 npm 安装
npm install --global http-server
通过 Homebrew 安装
brew install http-server
开始
准备一个 WEB 应用
一个应用包括 HTML 页面,CSS 样式,图片,JAVASCRIPT 脚本和字体。
添加 manifest 文件
manifest 是网页清单,位于 WEB 应用的根目录,通过 JSON 形式列举了网站的所有信息,允许应用能够添加到主屏幕,从设备主屏幕直接启动。
manifest.json 文件如下:
{
"name": "Progressive Web App",
"short_name": "PWA",
"description": "Progressive Web App Demo",
"icons": [
{
"src": "icons/icon-32.png",
"sizes": "32x32",
"type": "image/png"
},
// ...
{
"src": "icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "index.html",
"display": "fullscreen",
"theme_color": "#B12A34",
"background_color": "#B12A34"
}
一个 WEB 应用至少需要 name 和一个图标 (带有 src, size 和 type)。description, short_name, 和 start_url 最好要提供。
详细字段介绍:
name: 网站应用的全名。
short_name: 用户主屏幕上的应用名字。
description: 应用的描述。
icons: 主屏幕的图标。
start_url: 启动应用的网址。
display: 应用的显示方式;可以是全屏,独立,最小UI或者浏览器。
theme_color: 浏览器的地址栏等 UI 元素的颜色。
background_color: 背景色,用于安装程序时和显示启动画面时。
添加 Service Worker
Service Workers 实现了如何正确缓存网站资源并使其在用户设备离线时可用,还提供处理通知,在单独的线程上执行繁重的计算等。它运行在页面的 JavaScript 主线程独立的线程上,并且对 DOM 结构没有任何的访问权限。API 是非阻塞的,并且可以在不同的上下文之间发送和接收信息。
Service workers 可以控制网络请求,修改网络请求,返回缓存的自定义响应,或合成响应。
Service Workers 就像介于服务器和网页之间的拦截器,能够拦截进出的 HTTP 请求,从而完全控制你的网站。
特点
在页面中注册并安装成功后,运行于浏览器后台,不受页面刷新的影响,可以监听和拦截作用域范围内所有页面的 HTTP 请求。
网站必须使用 HTTPS。除了使用本地开发环境调试时(如域名使用 localhost)。
控制打开的作用域范围下所有的页面请求。
单独的作用域范围,单独的运行环境和执行线程。
不能操作页面 DOM,可以通过事件机制来处理。
事件驱动型服务线程。
注册 Service Worker
<script>
if (navigator.serviceWorker !== null) {
navigator.serviceWorker.register('sw.js')
.then(function(registration) {
console.log('Registered events at scope: ', registration.scope);
});
}
</script>
注册完成后,sw.js 文件会自动下载,然后安装,最后激活。
安装
在 install 的监听函数中, 我们可以初始化缓存以及添加离线应用时所需的文件。
创建缓存名字为 pwa-sample 的变量,将需要缓存的文件记录在数组上:
var cacheName = 'pwa-sample';
var appShellFiles = [
'index.html',
'app.js',
'style.css',
'favicon.ico',
'img/bg.png',
'icons/icon-32.png',
];
将要缓存的图片和上面的文件合并在一起:
var images = [];
for(var i=0; i<10; i++) {
images.push(`img/image${i}.jpg`);
}
var contentToCache = appShellFiles.concat(images);
监听 install 事件:
self.addEventListener('install', function(e) {
console.log('[Service Worker] Install');
e.waitUntil(
caches.open(cacheName).then(function(cache) {
console.log('[Service Worker] Caching all: app shell and content');
return cache.addAll(contentToCache);
})
);
});
waitUntil 里面的代码执行完毕之后才会开始安装,并返回一个 promise。caches
是一个特殊的 CacheStorage
对象,它能在Service Worker 指定的范围内提供数据存储的能力。
处理动态资源
self.addEventListener('fetch', function(e) {
e.respondWith(
caches.match(e.request).then(function(r) {
console.log('[Service Worker] Fetching resource: '+e.request.url);
return r || fetch(e.request).then(function(response) {
return caches.open(cacheName).then(function(cache) {
console.log('[Service Worker] Caching new resource: '+e.request.url);
cache.put(e.request, response.clone());
return response;
});
});
})
);
});
网络请求时可以监听 fetch 事件,在 caches 中去 match 事件的 request ,如果 response 不为空的话就返回 response ,否则返回 fetch 请求,在请求得到响应后缓存响应到 caches 中,当然在 fetch 事件中我们也可以手动生成 response 返回给页面,最后将 response 返回。
FetchEvent.respondWith
方法将会接管响应控制,它会作为服务器和应用之间的代理服务,允许我们对每一个请求手动处理成我们需要的数据并返回给页面。
也就是说首先会在缓存中查找资源是否被缓存,如果有,将会返回缓存的资源,如果不存在,会转而从网络中请求数据,然后将它缓存起来,这样下次有相同的请求发生时,我们就可以直接使用缓存。
更新资源
self.addEventListener('activate', function(e) {
e.waitUntil(
Promise.all(
caches.keys().then(cacheNames => {
return cacheNames.map(name => {
if (name !== cacheStorageKey) {
return caches.delete(name)
}
})
})
).then(() => {
return self.clients.claim()
})
)
})
缓存的资源会跟随着版本的更新过期,当我们把版本号更新,Service Worker 会根据缓存的字符串名称清除旧缓存,并将我们所有的文件(包括新的文件)添加到一个新的缓存中。
这个时候新的 Service Worker 会在后台被安装,而旧的 Service Worker 仍然会正确的运行,直到没有任何页面使用到它为止,这时候新的 Service Worker 将会被激活,然后接管所有的页面。
在新安装的 SW 中通过调用 self.clients.claim( ) 取得页面的控制权,这样之后打开页面都会使用版本更新的缓存,旧的 SW 脚本不在控制着页面之后会被停止。
Activate
Activate 用法跟 install 相同,通常用来删除已经不需要的文件或者做一些清理工作。
self.addEventListener('activate', function(e) {
e.waitUntil(
caches.keys().then(function(keyList) {
return Promise.all(keyList.map(function(key) {
if(cacheName.indexOf(key) === -1) {
return caches.delete(key);
}
}));
})
);
});
这样能够确保只有需要的文件会保留在缓存中,毕竟浏览器的缓存空间是有限的,手动清理掉不需要的缓存是一个不错的主意。
通知推送
推送 和 通知 是两个相互独立的功能,也可以配合使用。推送功能通过从服务端推送新的内容而不需要客户端发起请求,它是由应用的 Service Worker 来实现的。通知功能则可以通过 Service Worker 来向用户展示一些新的信息,或者提醒用户应用已经更新了某些功能。
通知
显示通知之前需要请求用户授权。
var button = document.getElementById("notifications");
button.addEventListener('click', function(e) {
Notification.requestPermission().then(function(result) {
if(result === 'granted') {
randomNotification();
}
});
});
授权的结果有三种:
- default:用户没有做出选择的时候,授权结果会返回defalut
- granted:用户已授权
- denied:用户拒绝授权
一旦用户选择授权,这个授权结果对通知 API 和推送 API 两者都有效。
创建通知
function randomNotification() {
var randomItem = Math.floor(Math.random()*10);
var title = `notification${randomItem}`;
var content = `Created by test${randomItem}.`;
var image = `img/img-${randomItem}.jpg`;
var options = {
body: content,
icon: image
}
var notification = new Notification(title, options);
setTimeout(randomNotification, 30000);
}
上述代码每隔三十秒会创建一个通知,直到用户手动关闭它为止。
推送
推送比通知更复杂,我们需要先从服务端订阅一个服务,之后服务端会推送数据到客户端应用。
用户订阅服务后才能接收到服务器推送的通知。
当用户订阅服务时,服务器会储存所有的接收到的信息以便在后续需要的时候能将信息推送出去。
navigator.serviceWorker.register('service-worker.js')
.then(function(registration) {
return registration.pushManager.getSubscription()
.then(async function(subscription) {
if (subscription) {
return;
}
return registration.pushManager.subscribe({ // 订阅新用户
userVisibleOnly: true, // 发送给用户的所有通知对他们都是可见的
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey) // 从服务端取得并转化的VAPID key
})
.then(function (subscription) {
// 订阅部分
})
});
});
在这部分代码里,注册完成之后,我们使用 registration 对象来发起订阅,然后使用subscription对象来结束这整个流程。
self.addEventListener('push', function(e) { /* ... */ });
为了能够接收到推送的消息,需要在 Service Worker 文件里面监听 push 事件,数据接收后通过通知的方式立刻展现给用户。
如何添加 PWA 应用
将 WEB 应用在浏览器打开,点击分享图标,从弹出框点击添加到主屏幕,这时你会发现在主屏幕出现了一个新的图标,从此便可以通过此快捷方式打开 PWA 应用。