Progressive Web App, 简称 PWA,是提升 Web App 的体验的一种新方法,能给用户原生应用的体验。
PWA 能做到原生应用的体验不是靠特指某一项技术,而是经过应用一些新技术进行改进,在安全、性能和体验三个方
面都有很大提升,PWA 本质上是 Web App,借助一些新技术也具备了 Native App 的一些特性,兼具 Web App
和 Native App 的优点。
PWA 的主要特点包括下面三点:
可靠 - 即使在不稳定的网络环境下,也能瞬间加载并展现
体验 - 快速响应,并且有平滑的动画响应用户的操作
粘性 - 像设备上的原生应用,具有沉浸式的用户体验,用户可以添加到桌面
PWA 本身强调渐进式,并不要求一次性达到安全、性能和体验上的所有要求,开发者可以通过 PWA Checklist 查看现有的特征。

注脚

展开查看详情

1.

2. 目 录 致谢 什么是 PWA 为什么是渐进式 离线和缓存 Service Worker Service Worker 简介 怎么使用 Service Worker Service Worker 生命周期 如何进行 Service Worker 调试 离线 UX 注意事项 Web存储 网页存储概览 Progressive Web App 的离线存储 利用 indexedDB 保持应用状态的最佳实践 吸引并留住用户 如何吸引并留住用户 将站点添加至首屏 manifest.json 简介 基本功能 改善应用体验 应用添加横幅 消息推送介绍 消息通知 通知 视觉部分 行为部分 常用实现 自动登录 介绍 传统“记住密码”功能实现 凭据管理 API 凭证管理 API 账号密码凭证管理 第三方登录凭证管理 网络安全 Web安全 本文档使用 书栈(BookStack.CN) 构建 - 2 -

3. 使用 HTTPS 同源的定义 CSP(内容安全策略) 典型的安全漏洞 性能 使用 RAIL 模型评估性能 架构 App Shell 模型 工具 代码规范与代码检查工具 使用 Lighthouse 审查 WebApp 探索 搜索优化 本文档使用 书栈(BookStack.CN) 构建 - 3 -

4.致谢 致谢 当前文档 《PWA文档》 由 进击的皇虫 使用 书栈(BookStack.CN) 进行构建,生成于 2018-07-04。 书栈(BookStack.CN) 仅提供文档编写、整理、归类等功能,以及对文档内容的生成和导出工具。 文档内容由网友们编写和整理,书栈(BookStack.CN) 难以确认文档内容知识点是否错漏。如果您在阅读文档 获取知识的时候,发现文档内容有不恰当的地方,请向我们反馈,让我们共同携手,将知识准确、高效且有效地传递 给每一个人。 同时,如果您在日常工作、生活和学习中遇到有价值有营养的知识文档,欢迎分享到 书栈(BookStack.CN) , 为知识的传承献上您的一份力量! 如果当前文档生成时间太久,请到 书栈(BookStack.CN) 获取最新的文档,以跟上知识更新换代的步伐。 文档地址:http://www.bookstack.cn/books/pwa-doc 书栈官网:http://www.bookstack.cn 书栈开源:https://github.com/TruthHun 分享,让知识传承更久远! 感谢知识的创造者,感谢知识的分享者,也感谢每一位阅读到此处的读者,因为我们 都将成为知识的传承者。 本文档使用 书栈(BookStack.CN) 构建 - 4 -

5.什么是 PWA 什么是 PWA 什么是 PWA Progressive Web App, 简称 PWA,是提升 Web App 的体验的一种新方法,能给用户原生应用的体验。 PWA 能做到原生应用的体验不是靠特指某一项技术,而是经过应用一些新技术进行改进,在安全、性能和体验三个方 面都有很大提升,PWA 本质上是 Web App,借助一些新技术也具备了 Native App 的一些特性,兼具 Web App 和 Native App 的优点。 PWA 的主要特点包括下面三点: 可靠 - 即使在不稳定的网络环境下,也能瞬间加载并展现 体验 - 快速响应,并且有平滑的动画响应用户的操作 粘性 - 像设备上的原生应用,具有沉浸式的用户体验,用户可以添加到桌面 PWA 本身强调渐进式,并不要求一次性达到安全、性能和体验上的所有要求,开发者可以通过 PWA Checklist 查 看现有的特征。 可靠 当用户打开我们站点时(从桌面 icon 或者从浏览器),通过 Service Worker 能够让用户在网络条件很差的情 况下也能瞬间加载并且展现。 Service Worker 是用 JavaScript 编写的 JS 文件,能够代理请求,并且能够操作浏览器缓存,通过将缓存的 内容直接返回,让请求能够瞬间完成。开发者可以预存储关键文件,可以淘汰过期的文件等等,给用户提供可靠的体 验。 详细请看 Service Worker 介绍。 体验 如果站点加载时间超过 3s,53% 的用户会放弃等待。页面展现之后,用户期望有平滑的体验,过渡动画和快速响 应。 为了保证首屏的加载,我们需要从设计上考虑,在内容请求完成之前,可以优先保证 App Shell 的渲染,做到和 Native App 一样的体验,App Shell 是 PWA 界面展现所需的最小资源。 参考 App Shell 设计规范。 粘性 PWA 是可以安装的,用户点击安装到桌面后,会在桌面创建一个 PWA 应用,并且不需要从应用商店下载 PWA 可以借助 Web App Manifest 提供给用户和 Native App 一样的沉浸式体验 本文档使用 书栈(BookStack.CN) 构建 - 5 -

6.什么是 PWA PWA 可以通过给用户发送离线通知,让用户回流 Web App Manifest 允许开发者控制 PWA 添加到桌面,允许定制桌面图标、URL等等。 参考 Web App Manifest 和 Push Notification。 其他 上面讲到 PWA 是兼具 Web App 和 Native App 的特征的,Web App 无版本问题、可索引也是很重要的特性。 总结,PWA 具有下面一些特性 渐进式 - 适用于所有浏览器,因为它是以渐进式增强作为宗旨开发的 连接无关性 - 能够借助 Service Worker 在离线或者网络较差的情况下正常访问 类似应用 - 由于是在 App Shell 模型基础上开发,因为应具有 Native App 的交互和导航,给用户 Native App 的体验 持续更新 - 始终是最新的,无版本和更新问题 安全 - 通过 HTTPS 协议提供服务,防止窥探和确保内容不被篡改 可索引 - 应用清单文件和 Service Worker 可以让搜索引擎索引到,从而将其识别为『应用』 粘性 - 通过推送离线通知等,可以让用户回流 可安装 - 用户可以添加常用的 webapp 到桌面,免去去应用商店下载的麻烦 可链接 - 通过链接即可分享内容,无需下载安装 PWA 是对站点体验的一个飞跃式的提升,可以在移动设备上的 Chrome(version > 52) 访问 天气 PWA 体验一 下。 我们会从安全、性能和体验三个角度来分析如何打造一个完善的 PWA。 来源(书栈小编注) https://github.com/lavas-project/pwa-doc 本文档使用 书栈(BookStack.CN) 构建 - 6 -

7.为什么是渐进式 为什么是渐进式 为什么是渐进式 我们强调渐进式的改善站点体验主要有下面两个原因: 降低站点改造的代价,逐步支持各项新技术,不要一蹴而就 新技术标准的支持度还不完全,新技术的标准还未完全确定 PWA 改造的成本考虑 PWA 涉及到从安全、性能和体验等方面的优化,想要一次性支持所有特性,代价很高,老板也不一定愿意投入大量人 力来支持这项大工程。 所以,从改造的成本考虑,我们也建议采取渐进式的方式,可以考虑按照下面的步骤来改造: 第一步,应该是安全,将全站 HTTPS 化,因为这是 PWA 的基础,没有 HTTPS,就没有 Service Worker 第二步,应该是 Service Worker 来提升基础性能,离线提供静态文件,把用户首屏体验提升上来 第三步,App Manifest,这一步可以和第二步同时进行 后续,再考虑其他的特性,离线消息推送等 标准的支持度 PWA 采用的最新技术,当前浏览器还没有达到完全支持的程度,W3C 关于这些技术的标准也还在处于草稿状态,没有 定稿。 根据 Can I use 的统计(包括 PC 和 Mobile) App Manifest 的支持度达到 57.43% Service Worker 的支持度达到 72.82% Notifications API 的支持度达到 43.3% Push API 的支持度达到 72.39% Background Sync 暂未统计到,Chrome 49 以上均支持 随着 W3C 的标准的进一步完善,国内外各大浏览器都会逐步支持,拥抱标准。 我们可以通过下面两个链接关注 Service Worker 的支持度,https://lavas.baidu.com/ready 。 本文档使用 书栈(BookStack.CN) 构建 - 7 -

8.离线和缓存 离线和缓存 Service Worker 离线 UX 注意事项 Web存储 本文档使用 书栈(BookStack.CN) 构建 - 8 -

9.Service Worker Service Worker Service Worker 简介 怎么使用 Service Worker Service Worker 生命周期 如何进行 Service Worker 调试 本文档使用 书栈(BookStack.CN) 构建 - 9 -

10.Service Worker 简介 Service Worker 简介 Service Worker 简介 背景:如何让网页的用户体验做到极致 随着 Web 的快速发展,用户对站点的体验期望值越来越高,前端工程师有时候为了几十毫秒的速度优化而费劲心 思,消耗大量时间。想要让自己的产品在无数产品中脱颖而出,就必须提升产品的性能和体验。在时间成本高昂的今 天,响应速度的提升是开发者不得不面对的话题。 前端工程师有很多性能优化的手段,包括 CDN、CSS Sprite、文件的合并压缩、异步加载、资源缓存等等。其实我 们绝大部分情况是在干一件事情,那就是尽量降低一个页面的网络请求成本从而缩短页面加载资源的时间并降低用户 可感知的延时。当然减少用户可感知的延时也不仅仅是在网络请求成本层面,还有浏览器渲染效率,代码质量等等。 那么到了今天,如果有人告诉你:“ 我们的站点可以秒开,离线的情况下也能浏览,不是 file:// 协议的哦! ”,你是不是要送他一个 大大的问号脸? 我们这里要讲到的是一个叫做 Service Worker 的东东。 什么是 Service Worker W3C 组织早在 2014 年 5 月就提出过 Service Worker 这样的一个 HTML5 API ,主要用来做持久的离线缓 存。 当然这个 API 不是凭空而来,至于其中的由来我们可以简单的捋一捋: 浏览器中的 javaScript 都是运行在一个单一主线程上的,在同一时间内只能做一件事情。随着 Web 业务不断复 杂,我们逐渐在 js 中加了很多耗资源、耗时间的复杂运算过程,这些过程导致的性能问题在 WebApp 的复杂化过 程中更加凸显出来。 W3C 组织早早的洞察到了这些问题可能会造成的影响,这个时候有个叫 Web Worker 的 API 被造出来了,这个 API 的唯一目的就是解放主线程,Web Worker 是脱离在主线程之外的,将一些复杂的耗时的活交给它干,完成后 通过 postMessage 方法告诉主线程,而主线程通过 onMessage 方法得到 Web Worker 的结果反馈。 本文档使用 书栈(BookStack.CN) 构建 - 10 -

11.Service Worker 简介 一切问题好像是解决了,但 Web Worker 是临时的,每次做的事情的结果还不能被持久存下来,如果下次有同样的 复杂操作,还得费时间的重新来一遍。那我们能不能有一个Worker 是一直持久存在的,并且随时准备接受主线程的 命令呢?基于这样的需求推出了最初版本的 Service Worker ,Service Worker 在 Web Worker 的基础上加 上了持久离线缓存能力。当然在 Service Worker 之前也有在 HTML5 上做离线缓存的 API 叫 AppCache, 但 是 AppCache 存在很多 不能忍受的缺点。 W3C 决定 AppCache 仍然保留在 HTML 5.0 Recommendation 中,在 HTML 后续版本中移除。 Issue: https://github.com/w3c/html/issues/40 Mailing list: https://lists.w3.org/Archives/Public/public-html/2016May/0005.html WHATWG HTML5 作为 Live Standard,也将 AppCache 标注为 Discouraged 并引导至 Service Worker。 Ok ,那么 Service Worker 到底用来干啥的呢? Service Worker 有以下功能和特性: 一个独立的 worker 线程,独立于当前网页进程,有自己独立的 worker context。 一旦被 install,就永远存在,除非被手动 unregister 用到的时候可以直接唤醒,不用的时候自动睡眠 可编程拦截代理请求和返回,缓存文件,缓存的文件可以被网页进程取到(包括网络离线状态) 离线内容开发者可控 能向客户端推送消息 不能直接操作 DOM 必须在 HTTPS 环境下才能工作 异步实现,内部大都是通过 Promise 实现 所以我们基本上知道了 Service Worker 的伟大使命,就是让缓存做到优雅和极致,让 Web App 相对于 Native App 的缺点更加弱化,也为开发者提供了对性能和体验的无限遐想。 浏览器支持情况 虽说 W3C 组织为了让用户体验做到极致操碎了心提出了这么有用的 API ,但是根据以往经验我们可以知道标准或 草案的提出之后各大浏览器的实现步伐是不一样的,那么 Service Worker 这么好用的东西到底浏览器支持情况怎 么样呢?参考 Can I use 可得下图: 本文档使用 书栈(BookStack.CN) 构建 - 11 -

12.Service Worker 简介 看到这张图还是相当激动的,至少也绿了一片,总结一下: Chrome 作为开路先锋早早的在 V40 版本就支持了,还提供了完善的 debug 方案( Service Worker debug ) Firefox,Opera 不甘示弱在后续版本也进行了支持 安卓手机 4.x 以上版本新系统形势一片大好(具体各手机的实现还得进一步探测) 安卓 Chrome 同样给力 Apple 方面,从 MacOS Safari 11.1 和 iOS Safari 11.3 开始全面支持 IE 就不讲了,但是 Edge 从 17 版本开始全面支持 这里说明一下,Apple 和微软方面都已经支持了 Service Worker,这对于 离线可访问 这样的 PWA 特性来 讲,几乎可以在任何的现代浏览器中被实现。 如果想了解更多,https://jakearchibald.github.io/isserviceworkerready 有更详细的最新浏览器支持 情况。 本文档使用 书栈(BookStack.CN) 构建 - 12 -

13.怎么使用 Service Worker 怎么使用 Service Worker 怎么使用 Service Worker 我们在 Service Worker 简介 中介绍了 Service Worker 的背景和兼容性等内容,然后在 Service Worker 生命周期 中介绍了 Service Worker 的生命周期以及所有的事件和 API, 但是我们还是不清楚如何才能使用 Service Worker, 以及在什么场景下使用什么 API 等等,这将是这篇文档所要提到的内容。 前提条件 Service Worker 出于安全性和其实现原理,在使用的时候有一定的前提条件。 由于 Service Worker 要求 HTTPS 的环境,我们通常可以借助于 github page 进行学习调试。当然一 般浏览器允许调试 Service Worker 的时候 host 为 localhost 或者 127.0.0.1 也是 ok 的。 Service Worker 的缓存机制是依赖 Cache API 实现的 依赖 HTML5 fetch API 依赖 Promise 实现 注册 要安装 Service Worker, 我们需要通过在 js 主线程(常规的页面里的 js )注册 Service Worker 来启 动安装,这个过程将会通知浏览器我们的 Service Worker 线程的 javaScript 文件在什么地方呆着。 先来感受一段代码: 1. if ('serviceWorker' in navigator) { 2. window.addEventListener('load', function () { 3. navigator.serviceWorker.register('/sw.js', {scope: '/'}) 4. .then(function (registration) { 5. 6. // 注册成功 7. console.log('ServiceWorker registration successful with scope: ', registration.scope); 8. }) 9. .catch(function (err) { 10. 11. // 注册失败:( 12. console.log('ServiceWorker registration failed: ', err); 13. }); 14. }); 15. } 这段代码首先是要判断 Service Worker API 的可用情况,支持的话咱们才继续谈实现,否则免谈了。 本文档使用 书栈(BookStack.CN) 构建 - 13 -

14.怎么使用 Service Worker 如果支持的话,在页面 onload 的时候注册位于 /sw.js 的 Service Worker。 每次页面加载成功后,就会调用 register() 方法,浏览器将会判断 Service Worker 线程是否已注册并 做出相应的处理。 register 方法的 scope 参数是可选的,用于指定你想让 Service Worker 控制的内容的子目录。本 demo 中服务工作线程文件位于根网域, 这意味着服务工作线程的作用域将是整个来源。 关于 register 方法的 scope 参数,需要说明一下:Service Worker 线程将接收 scope 指定网域目 录上所有事项的 fetch 事件,如果我们的 Service Worker 的 javaScript 文件在 /a/b/sw.js , 不传 scope 值的情况下, scope 的值就是 /a/b 。 scope 的值的意义在于,如果 scope 的值为 /a/b , 那么 Service Worker 线程只能捕获到 path 为 /a/b 开头的( /a/b/page1 , /a/b/page2 ,…)页面的 fetch 事件。通过 scope 的意义我们也能 看出 Service Worker 不是服务单个页面的,所以在 Service Worker 的 js 逻辑中全局变量需要慎 用。 then() 函数链式调用我们的 promise,当 promise resolve 的时候,里面的代码就会执行。 最后面我们链了一个 catch() 函数,当 promise rejected 才会执行。 代码执行完成之后,我们这就注册了一个 Service Worker,它工作在 worker context,所以没有访问 DOM 的权限。在正常的页面之外运行 Service Worker 的代码来控制它们的加载。 查看是否注册成功 如果你很困惑,我的 Service Worker 到底注册成功没有呢?注册成功是什么样子呢? 可以在 PC 上打开我们的好伙伴 chrome 浏览器, 输入 chrome://inspect/#service-workers 我们还可以通过 chrome://serviceworker-internals 来查看服务工作线程详情。 如果只是想了解服务工作线程的生 本文档使用 书栈(BookStack.CN) 构建 - 14 -

15.怎么使用 Service Worker 命周期,这仍很有用,但是日后其很有可能被 chrome://inspect/#service-workers 完全取代。 当然,它还可用于测试隐身窗口中的 Service Worker 线程,您可以关闭 Service Worker 线程并重新打开, 因为之前的 Service Worker 线程不会影响新窗口。从隐身窗口创建的任何注册和缓存在该窗口关闭后均将被清 除。 注册失败的原因 为啥会导致 Service Worker 注册失败呢?原因基本就是以下几种情况: 不是 HTTPS 环境,不是 localhost 或 127.0.0.1 。 Service Worker 文件的地址没有写对,需要相对于 origin。 Service Worker 文件在不同的 origin 下而不是你的 App 的,这是不被允许的。 安装 在你的 Service Worker 注册成功之后呢,我们的浏览器中已经有了一个属于你自己 web App 的 worker context 啦, 在此时,浏览器就会马不停蹄的尝试为你的站点里面的页面安装并激活它,并且在这里可以把静态资 源的缓存给办了。 install 事件我们会绑定在 Service Worker 文件中,在 Service Worker 安装成功后,install 事件被触 发。 install 事件一般是被用来填充你的浏览器的离线缓存能力。为了达成这个目的,我们使用了 Service Worker 新的标志性的存储 cache API — 一个 Service Worker 上的全局对象,它使我们可以存储网络响应发来的资 源,并且根据它们的请求来生成key。这个 API 和浏览器的标准的缓存工作原理很相似,但是是只对应你的站点的域 的。它会一直持久存在,直到你告诉它不再存储,你拥有全部的控制权。 localStorage 的用法和 Service Worker cache 的用法很相似,但是由于 localStorage 是同步的用法, 所以不允许在 Service Worker 中使用。 IndexedDB 也可以在 Service Worker 内做数据存储。 1. // 监听 service worker 的 install 事件 2. this.addEventListener('install', function (event) { 3. // 如果监听到了 service worker 已经安装成功的话,就会调用 event.waitUntil 回调函数 4. event.waitUntil( 5. // 安装成功后操作 CacheStorage 缓存,使用之前需要先通过 caches.open() 打开对应缓存空间。 6. caches.open('my-test-cache-v1').then(function (cache) { 7. // 通过 cache 缓存对象的 addAll 方法添加 precache 缓存 8. return cache.addAll([ 9. '/', 10. '/index.html', 11. '/main.css', 12. '/main.js', 13. '/image.jpg' 14. ]); 15. }) 16. ); 本文档使用 书栈(BookStack.CN) 构建 - 15 -

16.怎么使用 Service Worker 17. }); 这里我们 新增了一个 install 事件监听器,接着在事件上接了一个 ExtendableEvent.waitUntil() 方法—— 这会确保 Service Worker 不会在 waitUntil() 里面的代码执行完毕之前安装完成。 在 waitUntil() 内,我们使用了 caches.open() 方法来创建了一个叫做 v1 的新的缓存,将会是我们的 站点资源缓存的第一个版本。它返回了一个创建缓存的 promise,当它 resolved 的时候,我们接着会调用 在创建的缓存实例(Cache API)上的一个方法 addAll() ,这个方法的参数是一个由一组相对于 origin 的 URL 组成的数组,这些 URL 就是你想缓存的资源的列表。 如果 promise 被 rejected,安装就会失败,这个 worker 不会做任何事情。这也是可以的,因为你可以 修复你的代码,在下次注册发生的时候,又可以进行尝试。 当安装成功完成之后,Service Worker 就会激活。在第一次你的 Service Worker 注册/激活时,这并 不会有什么不同。但是当 Service Worker 更新的时候 ,就不太一样了。 自定义请求响应 走到这一步,其实现在你已经可以将你的站点资源缓存了,你需要告诉 Service Worker 让它用这些缓存内容来做 点什么。有了 fetch 事件,这是很容易做到的。 每次任何被 Service Worker 控制的资源被请求到时,都会触发 fetch 事件,这些资源包括了指定的 scope 内的 html 文档,和这些 html 文档内引用的其他任何资源(比如 index.html 发起了一个跨域的请求来嵌入一 个图片,这个也会通过 Service Worker),这下 Service Worker 代理服务器的形象开始慢慢露出来了,而这 个代理服务器的钩子就是凭借 scope 和 fetch 事件两大利器就能把站点的请求管理的井井有条。 话扯这么多,代码怎么实现呢?你可以给 Service Worker 添加一个 fetch 的事件监听器,接着调用 event 上的 respondWith() 方法来劫持我们的 HTTP 响应,然后你可以用自己的魔法来更新他们。 1. this.addEventListener('fetch', function (event) { 2. event.respondWith( 3. caches.match(event.request).then(function (response) { 4. // 来来来,代理可以搞一些代理的事情 5. 6. // 如果 Service Worker 有自己的返回,就直接返回,减少一次 http 请求 7. if (response) { 8. return response; 9. } 10. 11. // 如果 service worker 没有返回,那就得直接请求真实远程服务 12. var request = event.request.clone(); // 把原始请求拷过来 13. return fetch(request).then(function (httpRes) { 14. 15. // http请求的返回已被抓到,可以处置了。 16. 17. // 请求失败了,直接返回失败的结果就好了。。 18. if (!httpRes || httpRes.status !== 200) { 19. return httpRes; 20. } 21. 本文档使用 书栈(BookStack.CN) 构建 - 16 -

17.怎么使用 Service Worker 22. // 请求成功的话,将请求缓存起来。 23. var responseClone = httpRes.clone(); 24. caches.open('my-test-cache-v1').then(function (cache) { 25. cache.put(event.request, responseClone); 26. }); 27. 28. return httpRes; 29. }); 30. }) 31. ); 32. }); 我们可以在 install 的时候进行静态资源缓存,也可以通过 fetch 事件处理回调来代理页面请求从而实现动 态资源缓存。 两种方式可以比较一下: on install 的优点是第二次访问即可离线,缺点是需要将需要缓存的 URL 在编译时插入到脚本中,增加代 码量和降低可维护性; on fetch 的优点是无需更改编译过程,也不会产生额外的流量,缺点是需要多一次访问才能离线可用。 除了静态的页面和文件之外,如果对 Ajax 数据加以适当的缓存可以实现真正的离线可用, 要达到这一步可能需要 对既有的 Web App 进行一些重构以分离数据和模板。 Service Worker 版本更新 /sw.js 控制着页面资源和请求的缓存,那么如果缓存策略需要更新呢?也就是如果 /sw.js 有更新怎么 办? /sw.js 自身该如何更新? 如果 /sw.js 内容有更新,当访问网站页面时浏览器获取了新的文件,逐字节比对 /sw.js 文件发现不同时它 会认为有更新启动 更新算法,于是会安装新的文件并触发 install 事件。但是此时已经处于激活状态的旧的 Service Worker 还在运行,新的 Service Worker 完成安装后会进入 waiting 状态。直到所有已打开的页 面都关闭,旧的 Service Worker 自动停止,新的 Service Worker 才会在接下来重新打开的页面里生效。 自动更新所有页面 如果希望在有了新版本时,所有的页面都得到及时自动更新怎么办呢?可以在 install 事件中执行 self.skipWaiting() 方法跳过 waiting 状态,然后会直接进入 activate 阶段。接着在 activate 事件发生 时,通过执行 self.clients.claim() 方法,更新所有客户端上的 Service Worker。 看一下具体实例: 1. // 安装阶段跳过等待,直接进入 active 2. self.addEventListener('install', function (event) { 3. event.waitUntil(self.skipWaiting()); 4. }); 5. 6. self.addEventListener('activate', function (event) { 本文档使用 书栈(BookStack.CN) 构建 - 17 -

18.怎么使用 Service Worker 7. event.waitUntil( 8. Promise.all([ 9. 10. // 更新客户端 11. self.clients.claim(), 12. 13. // 清理旧版本 14. caches.keys().then(function (cacheList) { 15. return Promise.all( 16. cacheList.map(function (cacheName) { 17. if (cacheName !== 'my-test-cache-v1') { 18. return caches.delete(cacheName); 19. } 20. }) 21. ); 22. }) 23. ]) 24. ); 25. }); 另外要注意一点, /sw.js 文件可能会因为浏览器缓存问题,当文件有了变化时,浏览器里还是旧的文件。这会导 致更新得不到响应。如遇到该问题,可尝试这么做:在 Web Server 上添加对该文件的过滤规则,不缓存或设置较 短的有效期。 手动更新 Service Worker 其实在页面中,也可以手动借助 Registration.update() 更新。 参考如下示例: 1. var version = '1.0.1'; 2. 3. navigator.serviceWorker.register('/sw.js').then(function (reg) { 4. if (localStorage.getItem('sw_version') !== version) { 5. reg.update().then(function () { 6. localStorage.setItem('sw_version', version) 7. }); 8. } 9. }); debug 时更新 Service Worker debug 技巧 中也会提到, Service Worker 被载入后立即激活可以保证每次 /sw.js 为最 新的。 代码如下: 1. self.addEventListener('install', function () { 2. self.skipWaiting(); 3. }); 本文档使用 书栈(BookStack.CN) 构建 - 18 -

19.怎么使用 Service Worker 意外惊喜 Service Worker 的特殊之处除了由浏览器触发更新之外,还应用了特殊的缓存策略: 如果该文件已 24 小时没 有更新,当 Update 触发时会强制更新。这意味着最坏情况下 Service Worker 会每天更新一次。 info Set request’s cache mode to “no-cache” if any of the following are true: registration’s use cache is false. job’s force bypass cache flag is set. ewestWorker is not null, and registration’s last update check time is not null and the time difference in seconds calculated by the current time minus registration’s last update check time is greater than 86400. 本文档使用 书栈(BookStack.CN) 构建 - 19 -

20.Service Worker 生命周期 Service Worker 生命周期 Service Worker 生命周期 Service Worker 的使用过程很简单,所处理的事情也相对单一,我们基本上需要做的就是利用这个 API 做好站 点的缓存策略。在页面脚本中注册 Service Worker 文件所在的 URL。Worker 就可以开始激活了,激活后的 Service Worker 可以监听当前域下的功能性事件,比如资源请求( fetch )、推送通知( push )、后台同步 ( sync )。在这一系列的流程中,从 Service Worker 的注册到消失,经历了生命周期中不同的状态。 如何工作 我们之前 介绍 了这么多 Service Worker 相关的背景和现状,我们已经知道 Service Worker 是干嘛的了, 但是我们还不是很清楚它具体是怎么运作起来的。 通常我们如果要使用 Service Worker 基本就是以下几个步骤: 首先我们需要在页面的 JavaScript 主线程中使用 serviceWorkerContainer.register() 来注册 Service Worker ,在注册的过程中,浏览器会在后台启动尝试 Service Worker 的安装步骤。 如果注册成功,Service Worker 在 ServiceWorkerGlobalScope 环境中运行; 这是一个特殊的 worker context,与主脚本的运行线程相独立,同时也没有访问 DOM 的能力。 后台开始安装步骤, 通常在安装的过程中需要缓存一些静态资源。如果所有的资源成功缓存则安装成功,如果 有任何静态资源缓存失败则安装失败,在这里失败的不要紧,会自动继续安装直到安装成功,如果安装不成功无 法进行下一步 — 激活 Service Worker。 开始激活 Service Worker,必须要在 Service Worker 安装成功之后,才能开始激活步骤,当 Service Worker 安装完成后,会接收到一个激活事件(activate event)。激活事件的处理函数中,主 要操作是清理旧版本的 Service Worker 脚本中使用资源。 激活成功后 Service Worker 可以控制页面了,但是只针对在成功注册了 Service Worker 后打开的页 面。也就是说,页面打开时有没有 Service Worker,决定了接下来页面的生命周期内受不受 Service Worker 控制。所以,只有当页面刷新后,之前不受 Service Worker 控制的页面才有可能被控制起来。 生命周期 我们已经知道了,Service Worker 的工作原理是基于注册、安装、激活等步骤在浏览器 js 主线程中独立分担缓 存任务的,那么我们如何在这些 API 自身一系列的操作中进行一些我们自己想让 worker 干的事情呢? 这里我们需要了解一下 Service Worker 的生命周期的概念,这有利于我们学会在各个生命周期的阶段进行有目的 性的回调,让我们自定义的工作在 Service Worker 中正确有效的开展下去。MDN 给出了详细的 Service Worker 生命周期图: 本文档使用 书栈(BookStack.CN) 构建 - 20 -

21.Service Worker 生命周期 我们可以看到生命周期分为这么几个状态 安装中 , 安装后 , 激活中 , 激活后 , 废弃 安装( installing ):这个状态发生在 Service Worker 注册之后,表示开始安装,触发 install 事 件回调指定一些静态资源进行离线缓存。 install 事件回调中有两个方法: event.waitUntil() :传入一个 Promise 为参数,等到该 Promise 为 resolve 状态为止。 本文档使用 书栈(BookStack.CN) 构建 - 21 -

22.Service Worker 生命周期 self.skipWaiting() : self 是当前 context 的 global 变量,执行该方法表示强制当前处在 waiting 状态的 Service Worker 进入 activate 状态。 安装后( installed ):Service Worker 已经完成了安装,并且等待其他的 Service Worker 线程被 关闭。 激活( activating ):在这个状态下没有被其他的 Service Worker 控制的客户端,允许当前的 worker 完成安装,并且清除了其他的 worker 以及关联缓存的旧缓存资源,等待新的 Service Worker 线程被激活。 activate 回调中有两个方法: event.waitUntil() :传入一个 Promise 为参数,等到该 Promise 为 resolve 状态为止。 self.clients.claim() :在 activate 事件回调中执行该方法表示取得页面的控制权, 这样之后打开页面都 会使用版本更新的缓存。旧的 Service Worker 脚本不再控制着页面,之后会被停止。 激活后( activated ):在这个状态会处理 activate 事件回调 (提供了更新缓存策略的机会)。并可以处 理功能性的事件 fetch (请求) 、 sync (后台同步) 、 push (推送) 。 废弃状态 ( redundant ):这个状态表示一个 Service Worker 的生命周期结束。 这里特别说明一下,进入废弃 (redundant) 状态的原因可能为这几种: 安装 (install) 失败 激活 (activating) 失败 新版本的 Service Worker 替换了它并成为激活状态 支持的事件 MDN 也列出了 Service Worker 所有支持的事件: install:Service Worker 安装成功后被触发的事件,在事件处理函数中可以添加需要缓存的文件(详见 使用 Service Worker ) activate:当 Service Worker 安装完成后并进入激活状态,会触发 activate 事件。通过监听 activate 事件你可以做一些预处理,如对旧版本的更新、对无用缓存的清理等。(详见 更新 Service Worker ) 本文档使用 书栈(BookStack.CN) 构建 - 22 -

23.Service Worker 生命周期 message:Service Worker 运行于独立 context 中,无法直接访问当前页面主线程的 DOM 等信息,但 是通过 postMessage API,可以实现他们之间的消息传递,这样主线程就可以接受 Service Worker 的 指令操作 DOM。 Service Worker 有几个重要的功能性的的事件,这些功能性的事件支撑和实现了 Service Worker 的特性。 fetch (请求):当浏览器在当前指定的 scope 下发起请求时,会触发 fetch 事件,并得到传有 response 参数的回调函数,回调中就可以做各种代理缓存的事情了。 push (推送):push 事件是为推送准备的。不过首先需要了解一下 Notification API 和 PUSH API。 通过 PUSH API,当订阅了推送服务后,可以使用推送方式唤醒 Service Worker 以响应来自系统消息传递 服务的消息,即使用户已经关闭了页面。 sync (后台同步):sync 事件由 background sync (后台同步)发出。background sync 配合 Service Worker 推出的 API,用于为 Service Worker 提供一个可以实现注册和监听同步处理的方法。 但它还不在 W3C Web API 标准中。在 Chrome 中这也只是一个实验性功能,需要访问 chrome://flags/#enable-experimental-web-platform-features ,开启该功能,然后重启生效。 本文档使用 书栈(BookStack.CN) 构建 - 23 -

24.如何进行 Service Worker 调试 如何进行 Service Worker 调试 如何进行 Service Worker 调试 开发 Service Worker 的过程中,我们如何让我们的开发更加高效并且准确的找出 bug 所在呢?我们在这一章专 门讲一讲如何调试 Service Worker。 Service Worker 作为独立于主线程的独立线程,在调试方面有其实和常规的 JavaScript 开发类似,我们关注 的点大概有如下几点: 代码是否有报错 Service Worker 能否顺利更新 在不同机型上的兼容性问题 debug 不同类型资源和请求的缓存策略的验证 debug 环境下的开发跳过等待状态 我们都知道,根据 Service Worker 生命周期的特性,如果浏览器还在使用旧的 Service Worker 版本,即使 有 Service Worker 新的版本也不会立即被浏览器激活,只能进行安装并进入等待状态,直到浏览器 Tab 标签被 重新关闭打开。 在开发调试 Service Worker 时肯定希望重新加载后立即激活,我们不希望每次都重新打开当前页面调试,为此我 们可以在 install 事件发生时通过 skipWaiting() 来设置 skip waiting 标记。 这样每次 Service Worker 安装后就会被立即激活。 1. self.addEventListener('install', function () { 2. if (ENV === 'development') { 3. self.skipWaiting(); 4. } 5. }); 但是当浏览器未检测到 Service Worker 发生变化时(比如该文件设置了 HTTP 缓存), 甚至连安装都不会被触 发。现在可以借助于浏览器 DevTools 调试了: 比如在 Chrome DevTools 的 Application 标签页勾选 Update on reload ,Chrome 会在每次刷新时去访问 Service Worker 文件并重新安装和激活。 借助 Chrome 浏览器 debug 使用 Chrome 浏览器,可以通过进入控制台 Application -> Service Workers 面板查看和调试。如下图所示: 本文档使用 书栈(BookStack.CN) 构建 - 24 -

25.如何进行 Service Worker 调试 如果 Service Worker 线程已安装到当前打开的页面上,您会看到它将列示在此窗格中。 例如,在上方的屏幕截 图中, https://lavas-project.github.io/lavas-demo/news-v2/#/ 的作用域内安装了一个 Service Worker 线程。 我们熟悉一下这些个选项: offline: 复选框可以将 DevTools 切换至离线模式。它等同于 Network 窗格中的离线模式。 Update on reload:复选框可以强制 Service Worker 线程在每次页面加载时更新。 Bypass for network:复选框可以绕过 Service Worker 线程并强制浏览器转至网络寻找请求的资源。 Update:按钮可以对指定的 Service Worker 线程执行一次性更新。 Push:按钮可以在没有负载的情况下模拟推送通知。 Sync:按钮可以模拟后台同步事件。 Unregister:按钮可以注销指定的 Service Worker 线程。 Source:告诉您当前正在运行的 Service Worker 线程的安装时间。 链接是 Service Worker 线程源 文件的名称。点击链接会将您定向至 Service Worker 线程来源。 Status:告诉您 Service Worker 线程的状态。此行上的数字(上方屏幕截图中的 #1)指示 Service Worker 线程已被更新的次数。如果启用 update on reload 复选框,您会注意到每次页面加载时此数字都会 增大。在状态旁边,您将看到 start 按钮(如果 Service Worker 线程已停止)或 stop 按钮(如 果 Service Worker 线程正在运行)。 Service Worker 线程设计为可由浏览器随时停止和启动。 使用 stop 按钮明确停止 Service Worker 线程可以模拟这一点。停止 Service Worker 线程是测试 Service Worker 线程再次重新启动时的代码行为方式的绝佳方法。它通常可以揭示由于对持续全局状态的不 完善假设而引发的错误。 Clients:告诉您 Service Worker 线程作用域的原点。 如果您已启用 show all 复选框, focus 按钮将非常实用。 在此复选框启用时,系统会列出所有注册的 Service Worker 线程。 如果您点击正在不 同标签中运行的 Service Worker 线程旁的 focus 按钮,Chrome 会聚焦到该标签。 如果 Service Worker 文件在运行过程中出现了任何的错误,将显示一个 Error 新标签。 本文档使用 书栈(BookStack.CN) 构建 - 25 -

26.如何进行 Service Worker 调试 当然我们也可以直接访问 Chrome://serviceworker-internals 来打开 serviceWorker 的配置面板,查看所有注册 的 Service Worker 情况。注意一点,如无必要,不要选中顶部的 Open DevTools window and pause javaScript execution on Service Worker startup for debugging 复选框,否则每当刷新页面调试时都会弹出一个开发者窗口来。 在 Firefox 中,可以通过 Tools -> Web Developer -> Service Workers 打开调试面板。也可以访问 about:debugging#workers 直接进入该面板。 查看 Service Worker 缓存内容 我们已经了解过,Service Worker 使用 Cache API 缓存只读资源,我们同样可以在 Chrome DevTools 上查 看缓存的资源列表。 Cache Storage 选项卡提供了一个已使用(Service Worker 线程)Cache API 缓存的只读资源列表。 这里有个地方需要注意一下:第一次打开缓存并向其添加资源时,Chrome DevTools 可能检测不到更改。 重新加 载页面后,您应当可以看到缓存。 如果您打开了两个或多个缓存,您将看到它们列在 Cache Storage 下拉菜单下方。 本文档使用 书栈(BookStack.CN) 构建 - 26 -

27.如何进行 Service Worker 调试 当然,Cache Storage 提供清除 Cache 列表的功能,在选择 Cache Storage 选项卡后在 Cache Storge 缓 存的 key 的 item 上右键点击出现 delete ,点击 delete 就可以清除该缓存了。 也可以选择 Clear Storage 选项卡进行清除缓存。 网络跟踪 此外经过 Service Worker 的 fetch 请求 Chrome 都会在 Chrome DevTools Network 标签页里标注出 来,其中: 来自 Service Worker 的内容会在 Size 字段中标注为 from ServiceWorker Service Worker 发出的请求会在 Name 字段中添加 ⚙ 图标。 例如下图中,第一个名为 300 的请求是一张 jpeg 图片, 其 URL 为 https://unsplash.it/200/300 ,该请求 是由 Service Worker 代理的, 因此被标注为 from ServiceWorker 。 为了响应页面请求,Service Worker 也发出了名为 300 的请求(这是图中第二个请求), 但 Service Worker 把 URL 改成了 https://unsplash.it/g/200/300 ,因此返回给页面的图片是灰色的。 本文档使用 书栈(BookStack.CN) 构建 - 27 -

28.如何进行 Service Worker 调试 安卓真机 debug 由于目前 iOS 不支持 Service Worker,我们在这里只讨论安卓机的调试。而 Service Worker 必须要在 HTTPS 环境下才能被注册成功,所以我们在真机调试的过程中还需要解决 HTTPS 调试问题,当然 127.0.0.1 和 localhost 是被允许的 host,但是我们在真机调试上并不能采用 :(。 本文档使用 书栈(BookStack.CN) 构建 - 28 -

29.离线 UX 注意事项 离线 UX 注意事项 离线 UX 注意事项 本文转载自 developers.google.com 作者:Mustafa Kurtuldu 原文链接:Offline UX 注意事项 本文将介绍为慢速网络和离线工作打造出色体验所需的多个设计注意事项。 网络连接的质量会受许多因素的影响,例如: 网络提供商的覆盖范围很差。 极端的天气状况。 停电。 用户经过“死区”,如拦截其网络连接的建筑物。 用户正在乘坐火车和经过隧道。 互联网连接由第三方(如机场或酒店)管理,并有启用或禁用的时间限制。 在特定时段或特定日期对互联网进行限制或不允许访问互联网的文化习俗。 您的目标是提供良好的体验,从而减少网络连接发生变化所产生的影响。 当用户的网络连接不佳时您想向用户显示什么? 第一个必须要问的问题是:网络连接成功和失败分别是怎样的? 成功的网络连接指的是您的应用可以正常在线。 不 过,连接失败分两种情况:您的应用处于离线状态以及在网速慢时应用如何响应。 在考虑网络连接是否成功时,您需要问自己以下重要的 UX 问题: 为确定连接是否成功您需要等待多长时间? 在确定连接是否成功时您能做什么? 如果连接失败,您应该怎么做? 您如何将上述情况通知给您的用户? 通知用户其当前状态和状态的变化 通知用户在网络连接失败时他们仍可以进行的操作以及应用的当前状态。 例如,可以显示以下通知: 1. “You seem to have a bad network connection. [Not to worry!] Messages will be “sent when the network is restored.” 本文档使用 书栈(BookStack.CN) 构建 - 29 -