初探PWA

409 阅读8分钟

PWA是什么

PWA(Progressive web apps,渐进式 Web 应用)运用现代的 Web API 以及传统的渐进式增强策略来创建跨平台 Web 应用程序。这些应用无处不在、功能丰富,使其具有与原生应用相同的用户体验优势。(摘自MDN

总的来说就是web页面在移动端也能展现出类似原生的用户体验。

14年3月的时候,Chromium就已经开始研发service workers,同年11月份Chrome for Android release 40正式支持。谷歌开发者大会2016大力推广PWA相关技术,让PWA概念深入人心。在此之后的各种大型技术会议,PWA成了不可或缺的主题。iOS Safari 在18年2月发布的Safari Technology Preview Release 49宣布正式支持Service Workers,扫清了PWA发展的最大障碍。

18年算是PWA快速发展的一年。到了19年会发现这股热情似乎降了下来,甚至还有人在知乎问出2019 年 PWA(Progressive Web App) 凉了吗?,总的来说,该技术的发展主要还是依赖于谷歌的推广。

优势

PWA是基于web的,所以h5能做的它也能做

  • 易安装 通过配置manifest,可以主动提示用户或者被动由用户安装到桌面;或者借助第三方服务(如Microsoft开发的pwabuilder)将其包装成原生的app

  • 独立于网络 利用ServiceWorker将页面资源提前加载并离线运行

  • 可链接 可通过生成的桌面图标进入应用,原生般的体验(没有浏览器地址栏)

  • 可推送 ServiceWorker有一定的后台运行权限,可以像原生app一样接受来自网站的推送

  • 自动更新 通过ServiceWorker可以在后台可控地自动更新PWA

  • 渐进式 上面的扩展都不是必选的,你可以在觉得合适的时候再在已有的web app上渐进支持,这样可以无缝兼容和迁移旧浏览器和已有的站点

  • 安全: 需要https的网络或者localhost(便于本地调试)

兼容性

service wokers兼容性

service workers

PWA与小程序等其他app的比较

PWA小程序HTML5native(iOS/Android)Electron
安装方式点开即用,可选添加到桌面全屏运行即点即用,支付宝/微信等端内运行点开即用,可以添加快捷方式到桌面从appstore安装去官网等地方下载安装包
占用空间轻量轻量,有大小限制轻量可大可小,限制不多大,自带chromium runtime
性能
跨平台iOS/Android/桌面(需要比较新的浏览器)依赖于支付宝/微信通过浏览器仅支持目标平台跨桌面,不支持移动设备
独立运行是(依赖于浏览器)是(自带浏览器)
与native交互少(通过schema等)其他能力依赖于浏览器实现容易(通过容器提供的jsbridge等接口)少(通过schema等)其他能力依赖于浏览器实现天然支持可以(通过electron的api)
分发方式取决于网站本身,可以用CDN支付宝/微信提供CDN取决于网站本身,可以用CDNappstore手动安装或者macappstore等
离线运行
版本控制通过ServiceWorker通过支付宝/微信appstore推送手动升级
推送消息ServiceWorker借用支付宝/微信能力ServiceWorker原生推送(FCM/APNS)自行维护后台推送
热更新ServiceWorker借用支付宝/微信能力每次刷新都是新的难,需要hack可借助插件
安全性强制HTTPS由平台保证不强制HTTPS

核心点

service workers

PWA的核心,和Cookie一样是有Path路径的概念的,建议将其注册到顶级目录,以便管理全部页面

在网页已经关闭的情况下还可以运行, 用来实现页面的缓存和离线, 后台通知等等功能

特点

  • 在页面中注册并安装成功后,运行于浏览器后台,不受页面刷新的影响,可以监听和截拦作用域范围内所有页面的 HTTP 请求。
  • 网站必须使用 HTTPS。除了使用本地开发环境调试时(如域名使用 localhost)
  • 运行于浏览器后台,可以控制打开的作用域范围下所有的页面请求
  • 单独的作用域范围,单独的运行环境和执行线程
  • 不能操作页面 DOM。但可以通过事件机制来处理
  • 事件驱动型服务线程

全局变量

  • self: 表示 Service Worker 作用域, 也是全局变量
  • caches: 表示缓存
  • skipWaiting: 表示强制当前处在 waiting 状态的脚本进入 activate 状态(刷新页面的时候只是调用了注册,是否重新注册决定于当前的sw是否已经存在,文件名改变或者文件内容改变的时候会重新注册,新的sw会先install,然后waiting。等浏览器重启时替换掉老的sw,新的sw则进入active状态,通过install是调用skipWaiting达到则跳过了等浏览器重启的过程)
  • clients: 表示 Service Worker 接管的页面

生命周期

register(注册)
if ('serviceWorker' in navigator) {
      window.onload = () => {
        navigator.serviceWorker.register('sw.js')
        .then((registration) => {
          console.log('serviceWorker registration successful,it is: ', registration);
        })
        .catch(function (err) {
          console.log('serviceWorker registration failed: ', err)
        });
      }
    }
install(安装)
## cache相关API
Cache.match(request, options) 返回一个 Promise对象,resolve的结果是跟 Cache 对象匹配的第一个已经缓存的请求。
Cache.matchAll(request, options) 返回一个Promise 对象,resolve的结果是跟Cache对象匹配的所有请求组成的数组。
Cache.addAll(requests)接收一个URL数组,检索并把返回的response对象添加到给定的Cache对象。
Cache.delete(request, options)搜索key值为request的Cache 条目。如果找到,则删除该Cache 条目,并且返回一个resolve为true的Promise对象;如果未找到,则返回一个resolve为false的Promise对象。
Cache.keys(request, options)返回一个Promise对象,resolve的结果是Cache对象key值组成的数组。
const CACHE_VERSION = 'pwa-cache-key';
const CACHE_LIST = [
  "./css/index.css",
  "./image/logo.png"
];

// 监听install事件,安装完成后,进行文件缓存
self.addEventListener('install', e => {
  console.log('install', );
  // 将promise对象传给event
  e.waitUntil(
    // 找到key对应的缓存并且获得可以操作的cache对象
    caches.open(CACHE_VERSION)
    // 将需要缓存的文件加进来
    .then(cache => cache.addAll(CACHE_LIST))
    .then(() => {
      // 调用 self.skipWaiting() 方法是为了在页面更新的过程当中, 
      // 新的 Service Worker 脚本能立即激活和生效。
      self.skipWaiting();
    })
  );
});
self.addEventListener('fetch', function (e) {
  console.log('fetch:' + e.request.url)

  e.respondWith(
    // 判断当前请求是否需要缓存
    caches.match(e.request).then(function (cache) {
      // 有缓存就用缓存,没有就从新发请求获取
      return cache || fetch(e.request)
    }).catch(function (err) {
      console.log(err)
      // 缓存报错还直接从新发请求获取
      return fetch(e.request)
    })
  )
})
activate(激活)
// 监听activate事件,激活后通过cache的key来判断是否更新cache中的静态资源
self.addEventListener('activate', function (e) {
  console.log('Service Worker 状态: activate')
  var cachePromise = caches.keys().then(function (keys) {
    // 遍历当前scope使用的key值
    return Promise.all(keys.map(function (key) {
      // 如果新获取到的key和之前缓存的key不一致,就删除之前版本的缓存
      if (key !== cacheName) {
        return caches.delete(key)
      }
    }))
  })
  e.waitUntil(cachePromise)
  // 保证第一次加载fetch触发
  return self.clients.claim()
})
redundant(废弃)

浏览器缓存模式

from memory cache 内存,只存在浏览器运行时,如base64图片数据和静态资源,不可控

from disk cache 硬盘,长期缓存在硬盘中,如静态资源,不可控

from ServiceWorker sw代理,完全可控

示例

mainifest.json配置文件

需要将该配置文件引入到入口页面

{
  "name": "pwa demo", // 必填,插件名称
  "short_name": "demo PWA", // 选填 在APP launcher和新的tab页显示,如果没有设置,则使用name
  "description": "The app that helps you understand PWA", // 应用描述
  "display": "standalone", // 应用的显示模式。fullscreen:全屏显示,会尽可能将所有的显示区域都占满。
standalone:浏览器相关UI(如导航栏工具栏等)将会被隐藏,因此看起来更像一个Native App
minimal-ui:显示形式与standalone类似,浏览器相关UI会最小化为一个按钮,不同浏览器在实现上略有不同
browser:一般来说,会和正常使用浏览器打开样式一致(PS: 当一些系统的浏览器不支持fullscreen时将会显示成 standalone 的效果,当不支持 standalone 属性时,将会显示成 minimal-ui 的效果,以此类推)
  "start_url": "/", // 应用启动时的url
  "theme_color": "#8888ff", // 桌面图标的背景色
  "background_color": "#aaaaff", // 为web应用程序预定义的背景颜色。在启动web应用程序和加载应用程序的内容之间创建了一个平滑的过渡。
  "icons": [ // 桌面图标
    {
      "src": "image/logo.png",
      "sizes": "256x256", // 多个图片尺寸以空格分隔
      "type": "image/png" // 方便userAgent快速排除不支持的类型
    }
  ]
}

官方配置说明地址

用例

谷歌作为推广者,应用PWA的相对较多,比如:Youtube MusicGoogle PhotosAndroid MessagesFacebook移动版Twitter

what web can do today: PWA的兼容性检测网站,可以看到你的浏览器支持哪些PWA特性

AppScope: PWA应用商店,本身就是一个PWA,里面收录的app也都是PWA

React中比较好的PWA相关文档

官方Create React App的PWA文档

第三方React PWA文档

社区开源React PWA框架