PWA 一隅

文章创做于 2019-08-30,2019-12-20 迁移至此

PWA 简介

PWA,全称是 Progressive Web Application,它不特指某一项具体技术,能够看作是一些新技术的集合。PWA 本质上是 Web App,借助新技术,具有了 Native App 的一些特性。css

亮点

MDN 上列举了 PWA 的优势,这些优势主要是对目前 Web App 的痛点进行改进。下面列举比较能体现 PWA 特点的几点。html

渐进式(Progressive)

各项技术相互之间没有依赖,能够独立实施。若是某项技术在客户端上不支持,那就对其无效,仅此而已。实施新特性无需破坏应用的向后兼容性。前端

采用渐进式的考虑主要体如今:vue

  • 能够下降站点改造的代价
  • 新技术标准的支持度还不彻底,标准还未最终肯定

链接独立性(Connectivity independent)

借助 Service Worker,能够在离线或低速网络状态下工做。react

可安装(Installable)

容许用户将应用添加到桌面。webpack

再次访问的吸引力(Re-engageable)

经过 Web Push API 实现消息推送, Notifications API 实现桌面通知,可以吸引用户从浏览器外再次访问。git

PWA Checklist

PWA Checklist 给出了 PWA 应用能够参照的标准,除了阅读这个标准外,也能够经过 Lighthouse tool 对 Web 应用进行分析,获得 PWA 的改进建议。程序员

相关核心技术

  • Web App Manifest
  • Service Worker
  • Notifications API
  • Push API

例子 - 豆瓣 PWA

若是你已经阅读了上文的 PWA Checklist,你可能会发现,现有的不少网站已经具有了部分的 PWA 能力(如 HTTPS,响应式)。但做为一个稍微有点追求的程序员,对我来讲只有使用上 Service Worker、App Manifest 等核心技术的 Web App 在我心中才有资格称得上是真正的 PWA。github

以这个标准来衡量一个 Web App 是不是 PWA 的话,目前国内厂商中使用 PWA 技术的主要有 豆瓣移动版微博移动版饿了么-H5阿里巴巴(国际)-移动版。细心的你可能会发现,这些应用都是移动端的。若是咱们考虑 PWA 的出现是为了使 Web App 拥有某些 Native App 的能力(如离线使用、消息推送),而这些能力在移动端能发挥出更大的价值的话,也许就不难理解厂商为何首先在移动端使用 PWA 技术了。(固然也有像 Vue 官网谷歌邮箱 这样同时在 PC 端提供 PWA 技术的,由于这些技术对于 PC 端一样有帮助,只是在移动端更加明显而已)。web

OutwebAppscope 是两个收录 PWA 应用的网站,能够在上面查找 PWA 应用。

下面以 豆瓣移动版 为例,大体地感觉一下 PWA 与传统 Web App 之间的区别。

首先让咱们用上文提到的 Lighthouse tool 跑个分。

36600763

嗯,PWA 单项得分 91 分,好像还不错的样子,不过不够直观,因此咱们仍是看看程序吧。

74040098

62878046

如图 (a) - (f) 是使用 Android 的 Chrome 浏览器浏览豆瓣移动版时的截图。这里主要涉及 Manifest(a-e)和 Service Worker(f)两项技术。

第一次进入页面的时候,浏览器会提示将应用添加到主屏幕(a),点击添加(b)后,手机桌面上将生成豆瓣手机版的图标(c),点击桌面图标再次进入页面时,能够看到应用的欢迎界面,浏览器的地址栏也会消失不见(d)。此时,若是查看后台应用,能够发现系统进程中当前页面以 “豆瓣(手机版)” 而不是 “Chrome 浏览器” 的名义显示(e)。关闭移动数据和无线网络,刷新页面后仍能够正常浏览以前浏览过的内容(f)。

61229909

咱们经过 PC 端的 Chrome 浏览器控制台能够看到,首页的推荐信息流以 JSON 的形式被保存在 Cache 中,若是浏览了相关文章,Cache 中也会有相关的 JSON 文件被保存下来。当咱们离线使用时,若是命中了相关的 Cache,其中的内容将被取出用于渲染页面,这也是咱们为何能在离线状态下看到(f)的缘由。

Service Worker

前身

Service Worker 与另外两项技术有所关联: Web WorkerApplication Cache

浏览器的 JavaScript 都是运行在一个主线程上,随着业务不断复杂,性能问题不断凸显。W3C 提出了 Web Worker API,将一些耗时、耗资源的任务交给这个 API,完成后经过 post Message 方法告诉主线程,主线程经过 onMessage 方法获得反馈结果。但 Web Worker 是临时的,每次进行的操做不能被持久化保存下来,不能解决重复访问时的耗时问题。在此基础上,Service Worker 被提出,在 Web Worker 的基础上增长了持久的离线缓存能力。

Application Cache 则是在 HTML5 早些时候提出的一种应用程序缓存机制。这个标准也试图让应用在离线状态下可用。可是因为开发者没法对缓存进行有效控制,以及其它一些更新逻辑的缺陷,目前已从 Web 标准中移除。

Service Worker 能作什么?

  • 拦截网络请求
  • 缓存可用时返回缓存内容
  • 对缓存内容进行管理
  • 向客户端推送信息
  • 后台数据同步
  • 资源预取

特色

  • 必须在 HTTPS 环境下才能工做
  • 独立的线程,有本身的 worker context
  • 使用时被自动唤醒,不用时自动休眠
  • 不能直接操做 DOM
  • 异步实现,内部大都是经过 Promise 实现

相关依赖

  • HTTPS
  • Promise
  • Fetch API(获取资源)
  • Cache API(缓存)
  • Push API(消息推送)

兼容性

截至目前(2018-08-30),大约 83.89% 的浏览器支持 Service Worker。

72417529

生命周期方法

1658a6b5d94c511b

Service Worker 的生命周期大体能够分为四个阶段。

  • Parse 解析
  • Install 安装
  • Activate 激活
  • Redundant 废弃

Parse

Service Worker 是挂载到 navigator 下的对象,使用前须要检查其可用性,若是可用则进行 Service Worker 的注册。始终要记住的一点是 Service Worker 须要工做在 HTTPS 下,不然会无条件地注册失败。

if ('serviceWorker' in navigator) {
    navigator.serviceWorker
            .register('service-worker.js')
            .then(function() { console.log('Service Worker Registered');
    });
}

register() 方法有一个可选的参数 scope,能够指定让 Service Worker 控制缓存哪一个目录下的文件(做用域),不填写时默认为 Service Worker 文件所在的目录。

.register('service-worker.js', {scope: '/'})

注册成功后在 Chrome 控制台 Application 下的 Service Worker 中能够看到。

Install

const PRECACHE_URLS = ["../", "../styles/index.css", "../scripts/index.js"]

self.addEventListener("install", event => {
  event.waitUntil(
    caches
      .open('shell')
      .then(cache => cache.addAll(PRECACHE_URLS))
      .then(self.skipWaiting())
  )
})
  1. self.skipWaiting() 用于跳过等待状态,这个方法主要涉及新的 Service Worker 安装和老的 Service Worker 废弃的过程,即 Service Worker 的更新。通常状况下,新的 Service Worker 安装完成后将会进入等待状态,须要在老的 Service Worker 中止工做(通常是关闭浏览器)后才会取代。
  2. 因为系统会随时休眠 Service Worker,为了防止执行中断,须要使用 event.waitUntil() 进行捕获,它会监听异步请求返回的 promise,若是其中有 reject 的状况,则会致使 Service Worker 开启失败。

为了防止因为某些大文件或不稳定的文件下载失败致使 Service Worker 启动失败,能够只让一部分文件经过 cache.addAll() 返回。

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('shell').then(function(cache) {
    // 不稳定文件或大文件加载
      cache.addAll(
        //...
      );
      // 稳定文件或小文件加载
      return cache.addAll(
        //
      );
    })
  );
});

其中,第一个 cache.addAll() 将不会被捕获。

Activate

Service Worker 处于 activated 状态下时能够处理事件,如请求拦截与缓存捕获。在这以前,咱们能够监听 activate 事件,在回调函数中对旧的无用缓存文件进行清理。

self.addEventListener("activate", event => {
  const currentCaches = [SHELL, RUNTIME];
  event.waitUntil(
    caches
      .keys()
      .then(cacheNames => {
        return cacheNames.filter(
          cacheName => !currentCaches.includes(cacheName)
        );
      })
      .then(cachesToDelete => {
        return Promise.all(
          cachesToDelete.map(cacheToDelete => {
            return caches.delete(cacheToDelete);
          })
        );
      })
      .then(() => self.clients.claim())
  );
});

self.clients.claim() 作的是在不从新加载的前提下取得页面控制权。

fetch

下面介绍一下请求拦截与缓存捕获这一步。PWA 最吸引人的地方之一离线能力就是这一部分操做实现的。

这个部分涉及上文提到的两个 API:

  • Fetch API
  • Cache API

下面是一个简单的例子。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        if (response) {
          return response;
        }
        return fetch(event.request);
      }
    )
  );
});

首先咱们须要监听浏览器自己的 fetch 事件,respondWith 用来响应页面的请求。这里使用了 Catch API 的 match 方法来查找 Cache 中是否存在与 request 请求匹配的缓存,若是不存在则再经过 Fetch API 进行远程请求。

若是咱们在 install 时把页面和相关的资源缓存下来,在这一步已经可以实现页面的离线访问了。

这段代码能够进行优化,当没有命中 cache 进行远程请求后,能够将 fetch 的内容加入缓存中,这样这些资源在下一次访问的时候就能够直接使用了。

self.addEventListener("fetch", event => {
  if (event.request.url.startsWith(self.location.origin)) {
    event.respondWith(
      caches.open(RUNTIME).then(function(cache) {
        return cache.match(event.request).then(function(response) {
          var fetchPromise = fetch(event.request).then(function(networkResponse) {
            cache.put(event.request, networkResponse.clone());
            return networkResponse;
          })
          return response || fetchPromise;
        })
      })
    );
  }
});

值得注意的是这里的 response 须要给浏览器进行渲染,并同时保存的缓存中。因为 caches.put 使用的是文件的响应流,一旦使用就会形成 response 没法访问(能够理解为破坏性读出),因此须要事先使用 clone 方法复制一份。

咱们再捋一下上面代码的逻辑。

  1. 监听浏览器 fetch 事件,拦截本来的请求,
  2. 检查 cache 中是否存在将要请求的资源,有则返回缓存,无则进入下一步,
  3. 远程请求资源,将资源缓存后返回。

这是典型的 “缓存优先” 策略。事实上,经过 Fetch API 和 Cache API 顺序的排列组合,在拦截浏览器 fetch 事件后能够实现多种策略。

  • 缓存优先
  • 网络优先
  • 仅使用缓存
  • 仅使用网络
  • 速度优先

若是对策略的具体实现有所疑惑,能够参考 Service Worker最佳实践 - 腾讯浏览服务,此处再也不赘述。

Redundant

新的 Service Worker 进入 activated 状态后,老的 Service Worker 将被废弃,即 redundant 状态。

Service Worker 更新

更新 Service Worker 只需直接改动对应的 JavaScript 文件便可。浏览器会自动检测差别性进行获取。

当新的 Service Worker 被下载并 install 后,将进入 waiting 状态。此时两个 Service Worker 同时存在,仍由老的 Worker 控制页面。只有当老的 Service Worker 中止工做时,新的 Service Worker 才会进入 activated 状态并掌管页面。

Web App Manifest

注意这里的 ManifestApp Cache 中的 Manifest 彻底不一样,后者已从 Web 标准中移除,并由 Service Worker 替代。

JSON ? meta?

Web App Manifest(Web 应用程序清单)归纳地说是一个以 JSON 形式集中书写页面相关信息和配置的文件。这种清单的形式早在 浏览器插件开发中就已经出现(也许 PWA 中的 Manifest 是借鉴了其中的形式,只是属性有所区别)。

W3C 上提到了 Manifest 采用 JSON 形式的外置文件的一些考虑。总结一下主要有如下几点:

  1. 解耦。无需在各个页面重复声明 meta 标签,利于维护。
  2. 可缓存。HTML 可能常常变更,意味着用户代理一般须要下载整个 HTML 文件。使用外置的 Manifest 文件可以更好地利用缓存。
  3. 书写更灵活。这点的考虑其实能够借鉴 XML 与 JSON 的比较。标签化的结构适合 UI,而像 JSON 这样的结构更适合数据。Manifest 就是应用程序的一些数据,从这个角度看 JSON 比 meta 标签更合适。

用法

Manifest 的使用方法很是简单。首先须要在 head 中引用。

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

在 Manifest 文件中,用 JSON 的形式书写应用的相关信息。

{
  "name": "App name",
  "short_name": "App short name",
  "description": "Here is the description",
  "start_url": ".",
  "display": "standalone",
  "background_color": "#fff",
  "icons": [
      {
        "src": "images/homescreen.png",
        "sizes": "48x48 72x72 96x96 128x128 256x256",
        "type": "image/png"
      },
      {
        "src": "icon/logo.ico",
        "sizes": "96x96"
      }
  ]
}

相关字段说明

下面介绍几个经常使用的字段。

name

Web App 的全名,做为 App 图标的文字标签。

short_name

为 Web App 提供简短易读的名称,以便在没有足够空间显示应用程序全名时使用。

description

有关 Web App 的描述信息。

start_url

设置用户从主屏启动 App 时加载的 URL(用户在详情页将应用添加到首屏,此时若 start_url 为空,则用户从首屏打开应用时打开的页面将是详情页)。

scope

定义 Web 应用程序上下文的导航范围,若是用户在范围以外浏览应用程序,则返回正常的网页。

display

定义应用程序的首选显示模式,拥有四个可选的值,目前使用 PWA 的网站用得比较多的是 standalone 。

  • fullscreen 全屏显示,不显示状态栏
  • standalone 像一个独立的应用程序,具备不一样的窗口、图标,浏览器用于控制导航的 UI 将被移除,可能包含其它 UI 元素(如状态栏)
  • minimal-ui 像一个独立的应用程序,但包含浏览器地址栏
  • browser(默认) 以传统浏览器标签或新窗口形式打开

background_color

使浏览器能够在 CSS 加载前绘制 Web App 的背景颜色。

icons

指定各类环境下,应用程序图标的的图像对象数组。


除此以外,还有其它的属性,如需查看它们的用法能够参考 MDN 文档W3C文档

Manifest 更新

一旦用户将应用 icon 添加到桌面,以后 icon 将不能被更新,除非用户将其删除后从新添加到桌面。

额外须要考虑的问题

Service Worker 启动性能

在 W3C 的 Github 上关于 Service Worker的讨论中,能够看到目前 Chrome 中 Service Worker 的启动耗时大概在 200ms 左右,这意味着若是使用 Service Worker 以后减小的加载时间若是不足 200ms,反而会延长页面的加载时间。但这个问题这有望在今年 Chrome 后续版本的更新中获得改善。

没法优化 “首次加载” 速度

从 PWA 的流程上能够发现,PWA 不能完全优化 “首屏加载” 的性能问题(如白屏)。当新用户 “首次加载” 或用户清除浏览器缓存以后进入页面,到真正的使用上某个文件的缓存,须要通过三次网络请求。第一次是请求 Service Worker 所在的脚本文件,第二次是请求这个须要缓存的文件自己,到了第三次请求的时候,这个缓存才能真正生效。若是想要让用户在“首次加载”的时候一样拥有流畅的体验,单靠 PWA 是不够的。

优化“首次加载”的速度,首先想到的方式多是使用 SSR (Server Side Render,浏览器端渲染)代替 CSR (Client Side Render,客户端渲染)。Vue 甚至有官方的 SSR 指南 介绍使用过程。这里分享一个腾讯视频前端团队的演讲 —— 《极致流畅的移动 Web 应用解决方案》,演讲中详细比较了 SSR 和 CSR 的关键渲染路径及相关性能。下图摘自演讲 PPT ,从图中能够看出 SSR 相比 CSR 节省的主要时间来自 JS 生成 HTML 以及请求数据填充内容的过程。

65175998

值得一提的是,这个演讲中我的感受最有趣的一点是他们使用了 Web Socket 代替 AJAX 请求数据,减小了重复建立链接所消耗的时间。

对于 CSR,一样有相应的优化措施。好比,能够在 webpack 打包的过程当中,使用 prerender-spa-plugin 在构建时生成页面首屏,也能够大大减小 FCP(First Contentful Paint)时间,达到优化首次加载的目的。

国内 PWA 生态环境

在 PWA 技术推广上,谷歌表现得较为积极,其 PWA 生态也较为完整。但因为众所周知的缘由,谷歌提供的服务在国内没法正常使用。好比,在消息推送上,谷歌提供了FCM 服务,但国内没有相关的能够替代的基础设施,致使 PWA 的这一功能在国内实施起来较有难度。

另外一方面, PWA 技术的讨论和试验也比较活跃。也许是由于其渐进式(Progressive)的思想,其中一部分特性如今已有比较普遍的应用。好比 Service Worker,当你打开浏览器控制台后你会发现,像淘宝QQ音乐百度网盘 这样耳熟能详的产品网站已经应用了相关的技术,以优化页面资源下载时间。

浏览器之间存在差别

因为本人精力及设备有限,仅测试了 2018 年 7 月的 艾瑞数据 中排名靠前而且可以在应用市场下载使用的浏览器。

说明:

  1. 测试环境: Android 7.0.0
  2. 测试网站: 豆瓣
  3. 测试标准: Service Worker 仅对其离线能力(有或无)进行测试,Manifest 部分因为各家没有统一的表现,在这各家中 Chrome 浏览器的功能点最多,故以 Chrome 浏览器为基准,从五个方面进行评估。
  4. “添加到桌面” 一项中,“手动” 指的是在浏览器中能够找到添加到桌面的入口并将网页手动添加到桌面;“提示” 指的是进入页面时,浏览器提示用户将应用添加到桌面,虽然以后确认添加这一过程仍需手动点击,但免去了寻找添加入口的冗余操做步骤。
  5. “地址栏自动隐藏” 指的是从桌面点击进入应用以后的全过程,浏览器会根据 Manifest 配置决定是否隐藏地址栏。有些浏览器会有其它触发隐藏地址栏的设定,但这种触发跟用户操做相关(如上滑和下滑),而不会在全过程隐藏地址栏。
浏览器名称 离线能力 添加到桌面 桌面图标及名称与配置一致 欢迎屏幕 地址栏自动隐藏 后台驻留以应用形式显示
Chrome(68.0.3440.70) 支持 提示 + 手动
QQ 浏览器(8.8.0.4420) 不支持 手动
UC 浏览器(12.1.0.990) 支持 手动
360 浏览器(8.2.0.128) 支持 提示 + 手动
百度浏览器(7.18.20.0) 支持 提示 + 手动
三星浏览器(7.2.10.33) 支持 手动
搜狗浏览器(5.15.15) 支持 手动

从上表能够看出,国内主要浏览器厂商对于 Service Worker 的离线能力支持度仍是很高的,也许是由于这些浏览器基于 Chromium 内核进行开发。QQ 浏览器因为使用的是 X5 内核,表现上与 Chromium 内核存在差别。至于 Manifest,其功能点可能更多的属于软件自己实现而不是内核的问题,各家的表现差别较大。

小结

PWA 虽然不是完美的最终解决方案,而且目前在国内实现其全部特性尚有难度,但其中包含的不少技术,如对缓存的精细控制、消息推送等已经填补了以前 Web 生态的空白,瑕不掩瑜或许是对其最准确的评价。对我来讲,至少 “渐进式” 的思想足以让我相信并期待着 PWA 乃至整个 Web App 的美好将来。Make Web App great again!

相关连接

PWA 网址导航

国内部分 PWA

工具

改造案例

参考

相关文章
相关标签/搜索