PWA 从入门到实践开发

了解 PWA

PWA(Progressive Web App)渐进式 Web 应用程序

Webpack、Rollup 等打包工具
Babel、PostCss 等转译工具
Typescript 等可转编译为 JavaScript 的编程语言
React 、Angular、Vue等现代 web 前端框架
同构 JavaScript 应用

Web 应用体验依然不佳

  • 网页资源下载带来的网络延迟
  • Web 应用依赖于浏览器作为入口
  • 没有好的离线使用方案
  • 没有好的消息通知方案

PWA(Progressive Web App)

  • 显著提高应用加载速度
  • Web 应用可以在离线环境下使用
  • Web 应用能像原生应用一样被添加到主屏
  • Web 应用能在未被激活时发起推送通知
  • Web 应用与操作系统集成能力进一步提高

PWA 和原生 App 的差异

‘’ 安装方式 访问入口 通知服务 开发迭代
PWA 在线、自动安装 图标、URL 通知服务 Web,一次开发,多平台可用,无须发版
普通 NavtiveApp 应用商店 图标、相关调起 可通知服务 分平台,需发版

PWA 的支持情况

  1. Chrome、Oprea、Firefox 都已经实现了 PWA 所需的所有关键技术,Edge所有特性都已经处于【正在开发中】的状态。
  2. Safari,尤其是在 iOS 上,四个关键 API 都未得到支持,而且由于平台限制,第三方浏览器也无法在 iOS 上支持。
  3. 庆幸的是 Service Worker 与 Web App Manifest 纷纷列入了【正在考虑】的API 中,相信在不久的将来在 iOS 上也能体验 PWA。

PWA 特性

  • 渐进增强
  • 响应式用户界面
  • 不依赖网络连接
  • 类原生应用
  • 持续更新
  • 安全
  • 可发现
  • 再次访问
  • 可安装
  • 可连接性

PWA关键技术

  • Web APP Manifest
  • Service Worker
  • Cache Storage API
  • Push Notification

Web APP Manifest

Web APP Manifest,即通过一个清单文件向浏览器暴露 web 应用的元数据,包括名字、icon的 URL 等,以备浏览器使用,比如在添加至主屏或推送通知时暴露给操作系统,从而增强 web 应用与操作系统的集成能力。

  • 可以添加到桌面,有唯一的图标和名称
  • 有启动时界面,避免生硬的过渡
  • 隐藏浏览器相关UI,比如地址栏等

使用 Web APP Manifest

  • 在项目目录下创建一个 manifest.json 文件
  • 在 index.html 中引入 manifest.json 文件
  • manifest.json 文件中提供常见的配置
  • 需要在 https 协议 或者 http://localhost 下访问项目
1
<link rel="manifest" href="manifest.json">

通过 http-server 开启本地服务,访问 index.html 文件

常用属性

  • name:用于指定应用的名称,安装横幅提示的名称,和启动画面中的文字。
  • short_name:应用的短名称,用于主屏幕显示
  • icons:用于指定可在各种环境中用作应用程序图标的图像对象数组,144×144
  • scope:定义了 web 应用的浏览作用域,比如作用域外的 URL 就会打开浏览器而不会在当前 PWA 里继续浏览。
  • start_url:定义了一个 PWA 的入口页面。
  • orientation:锁定屏幕旋转
  • theme_color/background_color:主题色与背景色,用于配置一些可定制的操作系统 UI 以提高用户体验,比如 Android 的状态栏、任务栏等。
  • display:用于指定 web app 的显示模式

Service Worker

  • 一旦被 install,就永远存在,除非被手动 unregister
  • 用到的时候可以直接唤醒,不用的时候自动睡眠
  • 可编程拦截代理请求和返回,缓存文件,缓存的文件可以被网页进程取到(包括网络离线状态)
  • 离线内容开发者可控
  • 必须在 HTTPS 环境下才能工作
  • 异步实现,内部大都是通过 Promise 实现
演变过程
  • LocalServer
  • Application Cache
  • Service Worker

Service Worker 生命周期

Service Worker 生命周期

service worker 运行流程

  • install 事件会在 service worker 注册成功的时候触发,主要用于缓存资源
  • activate 事件会在 service worker 激活的时候触发,主要用于删除旧的资源
  • fetch 事件会在发送请求的时候触发,主要用于操作缓存或者读取网络资源

如果 sw.js 发生了改变,install 事件会重新触发
activate 事件会在 install 事件后触发,但是如果现在已经存在 service worker 了,那么就处于等待状态,直到当前 service worker 终止。
可以通过 self.skipWaiting() 方法跳过等待,返回一个 promise 对象。
可以通过 event.waitUntil() 方法扩的参数是一个 promise 对象,会在 promise 结束后才会结束当前生命周期函数,防止浏览器在异步操作之前流停止了生命周期。
service worker 激活后,会在下一次刷新页面的时候生效,可以通过 self.clients.claim() 立即获取控制权。

Service Worker 使用

  • 在 window.onload 中注册 service worker,防止与其他资源竞争
  • navigator 对象中内置 service worker 属性
  • service worker 在老版本的浏览器中不支持,需要进行浏览器兼容 if(‘serviceWorker’ in navigator){}
  • 注册 service worker navigator.serviceWorker.register(‘./sw.js’) ,返回一个 promise 对象
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
36
37
window.onload = function(){
if('serviceWorker' in navigator){
navigator.serviceWorker.register('./sw.js').then(resitration => {console.log(resitration)})
.catch(err => {
console.log(err);
})
}
}
// sw.js
// self.addEventListener('install',event)=>{}
self.oninstall = (e) => {
// 会让 service worker 跳过等待,直接进入 activate 状态
// 等待 skipWaiting 结束,才进入到 activate
e.waitUntil(self,skipWaiting());
e.waitUntil(
caches.open('installation').then(cache => cache.addAll([
'./',
'./styles.css',
'./script.js'
]))
)
}
self.onactivate = (e) =>{
// 表示 service worker 激活后,立即获取控制权
// self.clients.claim();
e.waitUntil(self.clients.claim());
}

// 使用离线缓存
self.onfetch = (e)=>{
const fetched = fetch(e.request);
const cached = caches.match(e.request)

e.respondWith(
fetched.catch(_ => { cached})
)
}

Service Worker 缓存策略

Cache Storage

cacheStorage:接口表示 Cache 对象的存储。配合 service worker 来实现资源的缓存

cache 基本使用

caches api:类似于数据库的操作

  • caches.open(cacheName).then(function(cache){}):用于打开缓存,返回一个匹配 cacheName 的 cache 对象的 promise,类似于连接数据库
  • caches.keys():返回一个 promise 对象,包括所有的缓存的 key(数据库名)
  • caches.delete(key):根据 key 删除对应的缓存(数据库)

cache 对象常用方法(单条数据的操作)

  • cache 接口为缓存的 Request/Response 对象提供存储机制
  • cache.put(req,res):把请求当成 key,并且把对应的响应存储起来
  • cache.add(url):根据 url 发起请求,并且把响应结果存储起来.
  • cache.addAll(url):抓取一个 url 数组,并且把结果都存储起来
  • cache.match(req):获取 req 对应的 response
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
36
37
38
39
40
41
42
43
44
45
46
47
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
<!-- 引入了引用程序清单文件 -->
<link rel="manifest" href="manifest.json" />
<link rel="stylesheet" href="index.css" />
</head>
<body>
<h1>hello pwa</h1>
<script>
window.addEventListener('load', async () => {
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.register(
'./sw.js'
)
console.log('注册成功', registration)
} catch (e) {
console.log('注册失败')
}
}
})

/*
如果页面一进来,我们发下用户没有联网,给用户一个通知
*/
if (Notification.permission === 'default') {
Notification.requestPermission()
}
if (!navigator.onLine) {
new Notification('提示', { body: '你当前没有网络,你访问的是缓存' })
}

// offline: 断线
window.addEventListener('online', () => {
new Notification('提示', {
body: '你已经连上网络了,请刷新访问最新的数据'
})
})
</script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//  manifest.json
{
"name": "豆瓣APP-PWA",
"short_name": "豆瓣APP",
"start_url": "/index.html",
"icons": [
{
"src": "images/logo.png",
"sizes": "144x144",
"type": "image/png"
}
],
"background_color": "skyblue",
"theme_color": "yellow",
"display": "standalone"
}
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
36
37
38
39
40
41
42
43
44
45
46
//  sw.js

// 主要就是缓存内容
const CACHE_NAME = 'cache_v2'
self.addEventListener('install', async event => {
// 开启一个cache, 得到了一个cache对象
const cache = await caches.open(CACHE_NAME)
// cache对象就可以存储的资源
// 等待cache把所有的资源存储起来
await cache.addAll(['/', '/images/logo.png', '/manifest.json', '/index.css'])
await self.skipWaiting()
})

// 主要清除就的缓存
self.addEventListener('activate', async event => {
// 会清除掉旧的资源, 获取到所有的资源的key
const keys = await caches.keys()
keys.forEach(key => {
if (key !== CACHE_NAME) {
caches.delete(key)
}
})
await self.clients.claim()
})

// 注释:fetch事件会在请求发送的时候触发
// 判断资源是否能够请求成功,如果能够请求成功,就响应成功的结果,如果断网,请求失败了,读取caches缓存即可
self.addEventListener('fetch', async event => {
// 请求对象
// 给浏览器响应
event.respondWith(networkFirst(event.request))
})

// 网络优先
async function networkFirst(req) {
try {
// 先从网络读取最新的资源
const fresh = await fetch(req)
return fresh
} catch (e) {
// 去缓存中读取
const cache = await caches.open(CACHE_NAME)
const cached = await cache.match(req)
return cached
}
}

Push Notification

  • Push API 的出现则让推送服务具备了向 web 应用推送消息的能力
  • Push API 不依赖 web 应用与浏览器 UI 存活,所以即使是在 web 应用与浏览器未被用户打开的时候,也可以通过后台进程接受推送消息并调用 Notification API 向用户发出通知。
  • Notification.permission 可以获取当前用户的授权情况
    • Default:默认的,未授权
    • Denied:拒绝的,如果拒绝了,无法再次请求授权,也无法弹窗提醒
    • Granted:授权的,可以弹窗提醒
  • 通过 Notification.requestPermission() 可以请求用户的授权
  • 通过 **new Notification(‘title’,{body:””,icon:””})**可以显示通知
  • 在授权通过的情况下,可以在 service worker 中显示通知 self.registration.showNotification(‘你好’,{body:’msg’})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//  sw.js
self.addEventListener('push',event => {
event.waitUntil(
// process the event and display a notification.
self.registration.showNotification('Hey');
)
})

self.addEventListener('notificationclick',event => {
// Do something with the event
event.notification.close();
})

self.addEventListener('notificationclose',event => {
// Do something with the event
})

常见的缓存策略

只读缓存
只读网络
缓存优先
网络优先

  • 对于不同的数据,需要不同的缓存策略
  • 本地的静态资源,缓存优先
  • 对于需要动态更新的数据,网络优先

避免缓存跨域资源

由于更新机制的问题,如果 service worker 缓存了错误的结果,将会对 web 应用造成灾难性的后果。

  • 响应状态码为200;避免缓存304、404、50×等常见结果。
  • 响应类型为 basic 或者 cors;即只缓存同源、或者正确地跨域结果;避免缓存错误的响应和不正确的跨域请求响应。

相关资料
[视频]PWA 入门
[视频]PWA-3小时带你实现渐进式WebAPP
深入浅出pwa
Chrome PWA应用 安装和卸载
Workbox Webpack插件
PWA之 workbox 学习

渐进式 Web 应用(PWA)
下一代 Web 应用模型 — Progressive Web App
PWA系列 – 分享PWA在阿里体系内的实践经验
PWA 介绍及快速上手搭建一个 PWA 应用
深入浅出 pwa
傻傻分不清的 Manifest
PWA 介绍及快速上手搭建一个 PWA 应用
十分钟让你完成一个可以安装到桌面的网页
饿了么的 PWA 升级实践
PWA 在饿了么的实践经验
现代化 Web 开发实践之 PWA


源码

黑马 H5豆瓣 PWA


开源
serve
http-server
vue-cli3 PWA
workbox
通用 PWA 构建器
基于 Webpack 模板的 Vue-Cli 的 PWA 模板
awesome-pwa 集合
使用VUE.JS构建实时PWA
关于Nuxt.js的零配置PWA解决方案
Vue店面 PWA
demo-progressive-web-app
项目不维护了 百度 基于vue 的 pwa 解决方案 Lavas

作者

Fallen-down

发布于

2020-08-04

更新于

2020-12-22

许可协议

You need to set install_url to use ShareThis. Please set it in _config.yml.
You forgot to set the business or currency_code for Paypal. Please set it in _config.yml.

评论

You forgot to set the shortname for Disqus. Please set it in _config.yml.
You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.