《PWA 开发实战》
前言
- 渐进式 Web 应用(progressive Web App,PWA)
第 1 章 渐进式 Web 应用介绍
PWA 的优势
无连接状态下的可用性
加载速度快
推送通知
主屏幕快捷方式
媲美原生
service worker
可以通过注册来控制站点的一个或多个页面。
可以监听并响应在其控制之下的所有页面的事件。可以拦截和修改事件
可以在用户离线的情况喜爱工作,检测离线状态或服务响应慢的情况,使用缓存内容替代
关闭标签也依然运行,并可以和服务器通信,可以接受并显示推送通知,确保每个操作都传递到服务器。
第 2 章 你的第一个 service worker
- serviceWorker 注册
navigator.serviceWorker
.register('/serviceWorker.js')
.then(function (registration) {})
.catch(function (err) {});
生命周期
修改 service worker 文件后,不会立即生效,旧版本还会依旧处于激活中,新版本处于等待状态
开启 chrome Service Workers 的 Update on reload 强制更新
可拦截请求并进行变更
渐进增强
渐进增强能为用户提供尽可能多的功能体验
可以让网站兼容所有用户
为了保护用户和防止中间人攻击,只有使用 HTTPS 的页面才能使用 service worker
开发中可以通过 localhost 来绕过安全连接的限制,但部署后必须使用 HTTPS 才能正常工作
service worker 作用域
为了安全问题,service worker 只能控制他的目录下的请求
通过 scope 可以限制作用域,但是只能向下
第 3 章 CacheStorage API
CacheStorage 是一种全新的缓存层
open、add、addAll、match
不能取代 HTTP 缓存
第 4 章 service worker 生命周期和缓存管理
生命周期
installing
使用 register 时,代码会被下载、解析并进入安装状态,如果安装成功,就会进入 installed 状态。发生错误则进入 redundant 状态。
监听 install 事件,并调用 waitUntil 可延时 install 事件,promise 失败后会进入 redundant 状态。
installed/waiting
安装成功进入 installed 状态
一般会马上进入 activating 状态,除非另一个激活的 service worker 依旧在控制,这时会维持 waiting 状态
activating
在激活并接管应用前,会触发 activate 事件。
和 installing 一样可以使用 waitUntil 扩展
activated
- service worker 只能在页面开始加载之前控制页面
redundant
- 不会有任何作用
service worker 独立于窗口或标签
第一次加载时安装 service worker,然而此时页面已经开始执行,service worker 无法控制,只有等安装完成后的打开的页面才可被控制。
service worker 生命周期和 waitUntil
service worker 不是一直运行,否则浏览器性能会受到严重影响
service worker 在控制域中事件触发时,会被唤醒,然后处理并再次终止
异步处理可使用 waitUntil 扩展执行,防止提前终止
更新 service worker
新的 service worker 注册和安装后不会替换现有的 service worker,而是保持 waiting
直到 service worker 作用域下的每个标签页或窗口关闭,旧的 service worker 才会进入废弃状态,新的 service worker 才会激活
设计是为了防止多版本 service worker 同时运行导致不兼容问题
管理缓存
如果在 install 时安装缓存,只会在 install 触发时安装
任何对 service worker 文件的修改,都会触发 service worker 的更新,此时会触发 install 并重新安装缓存
每个 service worker 都有单独的缓存
缓存管理和清除
浏览器对缓存存在存储限制
cache.keys 获取所有缓存名称
cache.delete 删除对应缓存
install 时安装缓存,activate 时删除旧缓存
重用已缓存的响应
- 通过 caches.match 匹配存在的缓存,并复制到新缓存中
service worker 响应头
service worker 缓存时间不能过长,否则会导致更新不及时
错误的 service worker 逻辑不及时更新会导致一些难以修复的问题
默认、最大过期时间为 24 小时
第 5 章 拥抱离线优先
什么是离线优先
离线和低连接是不可避免的,不该视为灾难
拥抱离线优先应在用户离线是保证大部分功能的可用
常用缓存模式
仅缓存
- 适用静态资源,更新后可通过修改文件名或手动更新缓存更新
缓存有限、网络回退
- 缓存找不到时会尝试从网络请求中返回
仅网络
网络优先、缓存回退
- 适用过时内容依然一定程度有用的情况
先缓存、后网络
- 先返回缓存中的数据、再去按照需要更新缓存,并更新页面
通用回退
- 缓存找不到、网络不可用时,返回一个替代的默认版本
混合与匹配:创造新模式
按需缓存
第一次请求后,将内容保存到缓存
response 如果不止一次用到 需要进行 clone
缓存优先,网络作为回退方案,并频繁更新缓存
- 先返回缓存,然后后台更新缓存
网络优先,缓存作为回退方案,并频繁更新缓存
- 每次网络请求都替换缓存为最新
规划缓存策略
使用缓存优先,网络作为回退方案,并频繁更新缓存模式返回 index.html 文件
使用缓存优先,网络作为回退方案模式返回首页需要展示的所有静态文件
从网络中返回谷歌地图的 JavaScript 文件。如果请求失败,返回一个替代的脚本
使用网络优先,缓存作为回退方案,并频繁更新缓存模式,返回 events.json 文件
使用按需缓存模式返回事件的图片文件,如果网络不可用并且图片没有缓存,则回退到默认的通用图片
数据分析的请求直接通过,不作处理
缓存预测
- 猜测用户接下来会用到的内容来缓存
App shell 架构
鼓励尽可能轻量的向用户呈现一个 shell,随着其变得可用再填充内容和附加功能
优先显示屏幕结构和内容,而非可以推迟处理的结构
第 6 章 使用 IndexedDB 在本地存储数据
什么是 IndexedDB
IndexedDB 是浏览器中的事务型对象存储数据库
事务型
- 操作会按照事务来分组,一个事务中,要么所有操作都成功、要么都失败
对象存储数据库
- 不用与关系型存储数据列、表格,对象型数据库可以存储多个对象
索引数据库
- 可以在对象存储中添加索引,并用来检索需要的对象
基于浏览器的
- 完全基于浏览器运行
其它内容
可以创建多个数据库
每个数据库可以包含多个对象存储
每个对象存储通常只包含一种数据类型
对象存储包含键值队
值几乎可以是任何 JavaScript 中的数据内容
键用来引用对象存储中的某个值,可以是简单的数字标或特定路径
遵循同源策略
数据库是版本控制的。创建或修改结构时可通过新版本号打开数据库连接,会触发 upgrade needed 事件,在事件中处理新旧版本的迁移更改
大部分 IndexedDB 操作都是异步的
数据库使用基本模式
打开数据
启动事务,用来读取或写入对象存储
打开对象存储
在对象存储中执行操作
完成事务
使用 IndexedDB
打开数据库连接
open 在没有的时候会创建对应数据库
监听 onsuccess 获取 idb 对象
数据库版本
- open 第二个参数为版本,版本变化时会触发 upgradeneed 事件
修改对象存储
- db.createObjectStore 创建对象存储
添加数据
db.transaction 创建事务
往事务中添加数据
transaction 的第一个参数为事务作用域,作用域相同的不同事务会串行等待运行,不同则可以并行运行
读取数据
- db.transaction 开启事务,然后通过 get 读取
版本管理
多版本迁移的兼容
通过判断之前数据库版本,做对应操作
或者通过判断当前数据库情况,执行对应操作
使用游标
事务中使用 openCursor 打开游标
游标只是一个指针,不包含结果
游标每次变动都会触发 onsuccess
创建索引
通过 autoIncrement 创建自动索引,这种称为 out-of-line key 外键,键和值的存储是分离的
使用自身属性做键叫做 inline key 内键
使用索引
- 打开事务 打开制定索引 打开游标
限制游标的范围
打开游标的缩略写法
```js
exchangeIndex.openCursor("CAD");
exchangeIndex.openCursor(IDBKeyRange.only("CAD"));
```还支持 lowerBound()、upperBound() 和 bound()
lowerBound 为指定键以后的键
upperBound 为指定键以前的键
lowerBound,upperBound 第二个参数为是否排除传入键
bound 为组合,四个参数为 lowerBound、upperBound、忽略 lowerBound、忽略 upperBound
设置游标方向
打开游标时第二个参数传入 prev 使用相反的方向
exchangeStore.openCursor(null, 'prev');
更新对象存储中的对象
通过 put 更新对象(外键)
内键需要检索对应对象,然后通过 update 或 put 更新值,此时不需要指定主键,因为传递原始对象中包含了键名
删除对象
可直接调用 delete 传入主键
内键需要检索然后在游标上调用 delete
删除所有对象
- 调用 clear
处理冒泡 IDB 错误
- IDB 错误事件会冒泡
SQL 忍者的 IndexedDB
SQL 和 IDB 的对比
游标
打开游标和运行 SELECT * FROM table; 类似。
不同的是游标只是指向对象,没有返回对象
IDBKeyRange
- IDBKeyRange 相当于 WHERE
索引
IDB 的索引更简化
IDB 只允许使用已经索引的属性来限制游标
游标方向
类似于 SQL 的 ORDER BY x DESC
不过只能根据对象存储的键或索引的键来排序
Promise 式的数据库
IndexedDB 管理
存储限制
超出限制可能数据被浏览器自动清理
IndexedDB 生态
PouchDB
- 优先 IndexDB、WebSQL 作为回退
localForage
- 使用 IndexDB、WebSQL,localStorage 作为回退
Dexie.js
- 提升 IndexedDB 开发体验
IndexedDB Promised
- 使用 Promised 改进 IndexedDB 体验
第 7 章 使用后台同步保证离线功能
后台同步如何工作
- 在 service worker 中注册 sync,并处理
SyncManager
访问 SyncManager
service worker 中使用 self.registration 获取
页面中通过 navigator.serviceWorker.ready 获取
注册事件
- 调用 sync.register
sync 事件
发送时机
注册完成后
用户从离线变成在线时
每隔几分钟,有尚未完成的注册时
事件通过 promise 响应,promise 完成后注册会被删除,promise 失败会保留并等待下次重试
事件标签
- 标签时唯一的,如果使用已有标签会被忽略
获取已注册的事件列表
- 使用 getTags 获取所有的已注册标签列表
最后的机会
- 多次失败时会尝试最后一次调用,可通过 事件的 lastChance 判断,最后一次失败如何处理
传递数据给 sync 事件
页面执行的大多数操作依赖于数据完成, 而 sync 事件能接收到的只有名称
几种方案
IDB 中维护操作队列
- 完成一个删除一个,全部完成再完成回调
IDB 中维护请求队列
将请求存储到 IDB 中
重新发送请求
传递数据给 sync 事件标签
- 把参数拼接到事件名称中
给应用添加后台同步
第 8 章 使用 postMessage 在 service worker 和页面之间通信
几种通信类型
窗口向 service worker 发消息
service worker 向作用域所有窗口发消息
service worker 向特定窗口发消息
通过 service worker 在窗口间发消息
窗口向 service worker 发消息
通过 navigator.serviceWorker.controller 获取操作页面的 serviceWorker
event source 中存在一些窗口信息
使用场景
- 通过用户访问的页面,service worker 提前判断缓存
注意判断 serviceWorker 是否存在
service worker 向作用域所有窗口发消息
通过 self.clients.matchAll 获取控制作用域下所有的窗口对象
client 存在 id
matchAll 传入参数 includeUncontrolled 可包含未受控的客户端
service worker 向特定窗口发消息
通过 get 特定 client id 获取特定的 client 并发送消息
matchAll 和 post message 的 source 中都可以拿到 client id
使用 MessageChannel 保持通信渠道打开
- 可将 MessageChannel 的另一个端口传给 service worker,就可以打开一条通信渠道
窗口间的通信
从 sync 事件向页面传递消息
第 9 章 可安装的 Web 应用:占领主屏先机
可安装的 Web 应用
实现步骤
注册 service worker
创建 Web 应用清单文件
在 Web 应用中,添加这个清单的链接
浏览器如何决定何时显示应用安装横条
符合标准才会考虑,标准如下
网站提供 HTTPS 服务
网站注册了 service worker
网站拥有一份 Web 应用清单,至少包含了必填字段
用户对应用有足够的兴趣,取决于浏览器判断条件
剖析 Web 应用清单
name 或 short_name
- name 为全名,如果位置不够则显示 short_name
start_url
- 点击图标时打开的 URL
icon
包含一个或多个对象的数组,描述可以使用的图标
要触发安装,至少包含一个图标,尺寸至少为 144*144
display
控制启动时的显示模式
browser 浏览器中打开
standalone 不显示浏览器栏
fullscreen 不显示浏览器栏和设备栏
description
- 应用描述
orientation
指定屏幕方向
landscape
portrait
auto
theme_color
主题色可以让浏览器和设备调整 UI 颜色来匹配网站
也可通过 meta 标签设置
meta 标签会覆盖清单设置
background_color
设备启动和加载时的背景色
页面样式会覆盖该颜色
不设置会为白色
scope
设置应用的作用域,如果超过作用域的链接会弹出到浏览器打开
某些浏览器会设置安卓系统的 Intent Filter。指向作用域的页面会启动应用来打开
dir
显示 name、short_name、description 文本方向
ltr 从左到右
rtl 从右到左
auto 使用浏览器语言设置
lang
- 指定 name、short_name、description 的文本语言
prefer_related_applications
- 设置为 true 时,会将当前平台的原生应用列举在安装横幅中
related_applications
设置安装的原生应用信息
"related_applications": [ {
"platform": "play", "url": "https://play.google.com/store/apps/details?id=com.goth.app", "id": "com.goth.app"
}, { "platform": "itunes", "url": "https://itunes.apple.com/app/gotham-imperial/id1234"
}],
"prefer_related_applications": true
各端兼容性
安装的应用图个平台图标一致变化
https://pwabook.com/appicons 可在此处查看详细列表
第 10 章 推送通知
推送通知的生命周期
推送通知包含两个概念
使用 push api 发送消息
使用 notification API 显示消息
Notification API
获取权限后显示通知
Notification.requestPermission().then(function (permission) {
if (permission === 'granted') {
new Notification('Shiny');
}
});
Push API
需要经过统一的中心服务器,由浏览器决定
步骤
页面使用 Push API 调用 subscribe
中心服务器接收到消息会返回新订阅的详情,返回给页面
将详情发送到服务器保存
服务器发消息给消息服务区
消息服务器转发到 service worker
权限同 Notification
Push + Notification
页面向用户请求显示通知的权限,用户授权;
页面和中央消息服务器通信,要求服务器为这个用户创建一个新的订阅;
消息服务器返回新的订阅详情对象作为响应;
页面将订阅详情发送给服务器;
服务器将订阅详情储存起来,以供将来使用;
时间流逝,季节变化,需要发送新的通知;
服务器使用订阅详情,通过消息服务器将消息发送给用户;
消息服务器将消息转发给用户的浏览器;
service worker 的 push 事件监听器收到消息;
service worker 显示通知,其中包含了消息内容。
用户权限按照同源策略保存
页面的 Notification 在移动端无法正常工作,需要使用 service worker 调用
Notification 的参数
navigator.serviceWorker.ready.then(function (registration) {
registration.showNotification('Quick Poll', {
body: 'Are progressive web apps awesome?',
icon: '/img/reservation-gih.jpg',
badge: '/img/icon-hotel.png',
tag: 'awesome-notification',
actions: [
{ action: 'confirm1', title: 'Yes', icon: '/img/icon-confirm.png' },
{ action: 'confirm2', title: 'Hell Yes', icon: '/img/icon-cal.png' }
],
vibrate: [500, 110, 500, 110, 450, 110, 200, 110, 170, 40, 450, 110, 200, 110, 170, 40, 500]
});
});- body 通知显示的正文文本
- icon 通知中显示的图标地址
- badge 用来代表发送通知的应用的图片 URL,或者是代表应用发送的通知类别
- actions 可传入操作对象数组,让用户可以快捷操作
- vibrate 设置震动模式
- tag 通知的唯一标识,同样的标识新消息会替换旧消息
- renotify 设置为 true,从而在更新消息时提醒用户
- data 可以用来附加任何想要伴随通知发送的数据
- dir 文本方向
- lang 主要语言
- noscreen 用来指定设备的屏幕是否会被这个通知打开。兼容较差
- silent 是否为静默消息。兼容较差
- sound 通知的铃声。兼容较差
- 订阅消息过程
- 创建应用时,生成一个公钥和一个私钥;
- 私钥是保密的,保存在服务器中;
- 公钥会包含在脚本中,并在创建订阅时被发送到消息服务器;
- 消息服务器将公钥连同其他订阅详情一起存储起来;
- 当服务器要发送消息时,使用私钥进行签名,随后发送到消息服务器;
- 消息服务器使用公钥验证消息是否用正确的私钥签名,如果是,则将消息发送给用户。
- 生成 VAPID 公钥和私钥
- 使用 web-push 生产公私钥
- 生成 GCM 密钥
- 仅仅使用 VAPID 密钥不足以向所有的浏览器发送推送消息。
- 早期的 Chrome 使用 GCM,当时还没有达成协议
- 在 firebase 中查看密钥
- 将 gcm_sender_id 添加到 manifest
- 创建新订阅
- subscribe 参数 userVisibleOnly,代表所有消息用户可见,静默消息可能损害用户隐私
- 推送消息
- 监听推送事件
- 监听 notificationclick 处理通知点击
第 11 章 渐进式 Web 应用的用户体验
信任
- 用户对 web 信任感较弱
渐进式 Web 应用中的常见消息
缓存完成
某页面已缓存
操作失败,并会在恢复连接时重试
启用通知
选择通知用词
发送通知需要考虑时机和消息
使用自己的通知授权界面
可以多次尝试
可以复用界面
更好的用户体验
设计
从设计反映出条件、环境的变化
设计适配环境和媒介
设计向用户注入信心
控制安装提示
- 无法控制安装,但是可以拦截安装事件并延时或取消
使用 RAIL 测量性能并实现高性能
响应(Response)
- 当用户执行任何操作时,例如点击屏幕上的任何元素,我们希望能在 0.1 秒内做出响应。
动画(Animation)
- 要让动画在人眼中看起来流畅,它每秒至少需要更新 60 次。
空闲(Idle)
- 将非必要的工作推迟到空闲时间。
加载(Load)
- 当用户执行一个操作,例如在网站上请求页面时,你的目标是在一秒之内显示操作的结果
RAIL 的指导原则是:
在 100 毫秒或者更短时间内,显示对用户操作的某种响应;
确保每 16 秒(或者更短)绘制一次屏幕动画;
在页面空闲时执行工作,每次不超过 50 毫秒;
在 1000 毫秒内加载并显示用户请求的内容。
第 12 章 渐进式 Web 应用的未来
使用 Payment Request API 接受支付请求
使用 Credential Management API 进行用户管理
WebGL 实时图像处理
未来的语音识别 API
使用 WebVR 在浏览器中实现虚拟现实
轻松共享应用
流畅的媒体播放 UI