pwa实践

PWA

Overview

Progressive Web App, 简称 PWA,是提升 Web App 的体验的一种新方法,能给用户原生应用的体验。

PWA本质上还是Web App,借助一些新技术也具备了Native App特性,兼顾两者优点

  • 渐进增强 : 能够让每一位用户使用,无论用户使用什么浏览器,因为它是始终以渐进增强为原则
  • 可安装 : 可以像原生APP在主屏幕上留有图标
  • 离线缓存 :通过Service Worker使得 Web App 也可以做到像 Native App 那样可以离线使用、消息推送的功能
  • 安全性 : 通过 HTTPS 来提供服务来防止网络窥探,保证内容不被篡改

PWA是一系列技术组成的集合,包括Manifest,Service Worker,Web Push等

涉及到的知识点:

  • ES6标准语法
  • Promise
  • fetch,全新的获取资源的API,它包括Request、Response、Header和Stream
  • WebWorker,JavaScript解决单线程的方案
  • Cache API(缓存API)

Service Worker

service worker

字面意思,“服务员”,就是服务于浏览器和服务器之间的,相当于充当服务端与浏览器、浏览器与 Web 应用程序之间的代理服务器

service worker是独立于当前页面的运行在浏览器后台进程的脚本。基于Web Worker事件驱动,带来速度提升网页加载体验,同时大量使用promise,

利用它,我们可以拦截页面请求,缓存文件,消息推送,静默更新,地理围栏等服务,还可以在客户端通过 indexedDB API 保存持久化信息

有以下几点特性:

  • 安全性,必须https 或者 本地localhost / 127.0.0.1这种
  • 独立于当前网页进程的后台进程,不会对浏览器主线程造成影响
  • 由于是独立线程,所以不可操作DOM和window,但可以通过postMessage与页面通信
  • 可以拦截作用域范围内的所有请求

浏览器支持

image

先决条件

  • 支持https
  • 支持Promise
  • 支持fetch API
  • 缓存机制依赖cache API

生命周期

  1. Registration 注册
  2. Installation 安装
  3. Activation 激活

image

注册

在主进程JS代码中注册SW

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// index.js
let version = 'v1';
// 判断浏览器是否支持Service Worker
if ('serviceWorker' in navigator) {
// Service Worker的register注册成功后会返回registration
// sw.js就是Service Worker的功能文件
navigator.serviceWorker.register('/sw.js').then(reg => {
if (localStorage.getItem('sw_version') !== version) {
reg.update().then(function () {
localStorage.setItem('sw_version', version);
console.log('service worker updated!');
});
}
console.log('service worker registed!');
}).catch(err => {
console.log('Opooos, something wrong happend!');
})
}

查看Service Worker是否启用

chrome

可以在chrome的Application中查看,或者执行下面命令查看

1
chrome://inspect/#service-workers
1
chrome://serviceworker-internals/
安装

install事件绑定在Service Worker文件中,当安装成功后,install事件就会被触发。

install中一般进行缓存,用到之前提到的Cahce API,它是一个Service Worker上的全局对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// sw.js
const cache_name = "v1.2";
const filesToCache = [
'/index.js',
'/index.html',
'/index.css',
'/'
];
// install
self.addEventListener('install', function (e) {
self.skipWaiting();
console.log('installed');
e.waitUntil(
caches.open(cache_name).then(cache => {
return cache.addAll(filesToCache);
})
);
});
  • 新增install的监听器,e.waitUntil()来确保,Service Worker不会在waitUntil()执行完成之前安装完成
  • 使用caches.open()创建一个cachev1的新缓存,返回一个缓存的promise对象,当它resolved时候,我们在then方法里面用caches.addAll来添加想要缓存的列表,列表是一个数组,里面的URL是相对于origin的。
激活

Service Worker安装完成后会进入激活状态,触发activate事件,这里可以进行一些预处理,旧版本更新,无用缓存处理等。

sw.js控制着页面资源和请求的缓存,如果 js 内容有更新,当访问网站页面时浏览器获取了新的文件,逐字节比对js 文件发现不同时它会认为有更新启动 更新算法,于是会安装新的文件并触发 install 事件。

但是此时已经处于激活状态的旧的 Service Worker 还在运行,新的 Service Worker 完成安装后会进入 waiting 状态。直到所有已打开的页面都关闭,旧的 Service Worker 自动停止,新的 Service Worker 才会在接下来重新打开的页面里生效。

可以在 install 事件中执行 self.skipWaiting() 方法跳过 waiting 状态,然后会直接进入 activate 阶段。接着在 activate 事件发生时,通过执行 self.clients.claim() 方法,更新所有客户端上的 Service Worker。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// cache update
self.addEventListener('activate', function (e) {
console.log('activate');
e.waitUntil(
caches.keys().then(function(cacheNames) {
console.log(cacheNames);
return Promise.all(
cacheNames.map(function(cacheName) {
// 如果当前版本和缓存版本不一致
if (cacheName !== cache_name) {
return caches.delete(cacheName);
}
})
);
})
);
})

当js 文件可能会因为浏览器缓存问题,当文件有了变化时,浏览器里还是旧的文件。这会导致更新得不到响应。

可以手动更新:

结合localStorage,进行reg.update()

1
2
3
4
5
6
7
8
9
10
11
navigator.serviceWorker.register('/sw.js').then(reg => {
if (localStorage.getItem('sw_version') !== version) {
reg.update().then(function () {
localStorage.setItem('sw_version', version);
console.log('service worker updated!');
});
}
console.log('service worker registed!');
}).catch(err => {
console.log('Opooos, something wrong happend!');
})
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
31
32
self.addEventListener('fetch', function (e) {
console.log('fetch');
e.respondWith(
caches.match(e.request)
.then(function (response) {
// 检测是否已经缓存过
if (response) {
return response;
}
var fetchRequest = e.request.clone();
return fetch(fetchRequest).then(
function (response) {
// 检测请求是否有效
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
var responseToCache = response.clone();
caches.open(CACHENAME)
.then(function (cache) {
cache.put(e.request, responseToCache);
});
return response;
}
);
})
);
});

Web Storage

cache API和 IndexedDB

针对于离线存储数据,可以有这两种方式

这两种API都是异步的存储,IndexedDB是基于事件的,而Cache API是基于Promise的

IndexedDB基本可以在所有浏览器环境使用

image

Cache API

image

对于PWA,我们可以缓存静态资源,从而使用 Cache API 编写的应用 Application Shell(JS/CSS/HTML 文件),并从 IndexedDB 填充离线页面数据

对于Web Storage(LocalStorage/SessionStorage)是同步的,不支持 Web worker线程,并且有大小和类型(仅限字符串)的限制。

添加到桌面

PWA 添加至桌面的功能实现依赖于 manifest.json。

为了实现 PWA 应用添加至桌面的功能,除了要求站点支持 HTTPS 之外,还需要准备 manifest.json 文件去配置应用的图标、名称等信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"name": "Hello PWA",
"short_name": "PWA",
"icon": {
"src": "./assets/logo_mars.png",
"sizes": "240x240",
"type": "image/png"
},
"lang": "en-US",
"start_url": "/index.html",
"display": "standalone",
"background_color": "white",
"theme_color": "white"
}

使用 link 标签将 manifest.json 部署到 PWA 站点 HTML 页面的头部

1
<link rel="manifest" href="./manifest.json">

参考注释:

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
31
32
33
34
35
name: {string} 应用名称,用于安装横幅、启动画面显示
short_name: {string} 应用短名称,用于主屏幕显示
icons: {Array.<ImageObject>} 应用图标列表
src: {string} 图标 url
type {string=} 图标的 mime 类型,非必填项,该字段可让浏览器快速忽略掉不支持的图标类型
sizes {string} 图标尺寸,格式为widthxheight,宽高数值以 css 的 px 为单位。如果需要填写多个尺寸,则使用空格进行间隔,如"48x48 96x96 128x128"
start_url: {string=} 应用启动地址
scope: {string} 作用域
// scope 应遵循如下规则:
//如果没有在 manifest 中设置 scope,则默认的作用域为 manifest.json 所在文件夹;
//scope 可以设置为 ../ 或者更高层级的路径来扩大PWA的作用域;
//start_url 必须在作用域范围内;
//如果 start_url 为相对地址,其根路径受 scope 所影响;
//如果 start_url 为绝对地址(以 / 开头),则该地址将永远以 / 作为根地址;
background_color: {Color} css色值 可以指定启动画面的背景颜色。
display: {string} 显示类型
//fullscreen 应用的显示界面将占满整个屏幕
//standalone 浏览器相关UI(如导航栏、工具栏等)将会被隐藏
//minimal-ui 显示形式与standalone类似,浏览器相关UI会最小化为一个按钮,不同浏览器在实现上略有不同
//browser 浏览器模式,与普通网页在浏览器中打开的显示一致
orientation: string 应用显示方向
//orientation属性的值有以下几种:
//landscape-primary
//landscape-secondary
//landscape
//portrait-primary
//portrait-secondary
//portrait
//natural
//any
theme_color: {Color} // css色值theme_color 属性可以指定 PWA 的主题颜色。可以通过该属性来控制浏览器 UI 的颜色。比如 PWA 启动画面上状态栏、内容页中状态栏、地址栏的颜色,会被 theme_color 所影响。
related_applications: Array.<AppInfo> 关联应用列表 可以引导用户下载原生应用
platform: {string} 应用平台
id: {string} 应用id

Push Notification

暂未实现

PWA 方向

PWA的核心技术包括:

  • Web App Manifest – 在主屏幕添加app图标,定义手机标题栏颜色之类
  • Service Worker – 缓存,离线开发,以及地理位置信息处理等
  • App Shell – 先显示APP的主结构,再填充主数据,更快显示更好体验
  • Push Notification – 消息推送,之前有写过“简单了解HTML5中的Web Notification桌面通知”

参考

What is PWA?

张鑫旭 – 借助Service Worker和cacheStorage缓存及离线开发