上一篇是服务端的优化,这篇是客户端的优化。
为了提升博客的用户体验,技术上需要深入探究一下 Gatsby (及其插件)的内在实现。PWA 是其中不可或缺的一环。本文是《PWA 开发实战》的阅读笔记(引用文本均出自此书),结合博客项实践后做了一些记录。
PWA 的诞生
历史有其必然性,要探究一项技术的诞生,我们需要观其历史。
在 Web 1.0 时代,网页也就是加了超链接的文本,人们用它来传递简单的文本信息,仅此而已。web 的力量有限,客户端大行其道。当时的主流技术架构是 Server-Client。Web 为跨平台带来新的思路。
Web 2.0 时代,CGI、MVC、ORM 等等这些技术为动态网页打下了坚实的后端基础,结合浏览器客户端的 Ajax,出现了社交网络、电商等等这些杀手级别的Web应用。此时的主流技术架构是 Server-Browser,由此引出了全栈和前端端后分离的探讨。
至于 jQ 和 前端框架又是另外一回事了,这里不再详述。
再后来智能手机的普及带来了移动互联网时代。这时候 Web 的不得不做一些转变。 移动优先
的口号出现了。移动段适配什么的,现在听起来都头大。于是很多网站索性做成,PC端和移动端两种页面。
PWA 的初衷是为了优化 WEB 站点的用户体验,并且在移动端打开市场。因为移动端的网络环境太差,于是 离线优先
的口号也出现了。
主要技术点
- 中间件 —— Service Worker 请求拦截
- 缓存 —— CacheStorage 离线缓存
消息通知 、页面通信这里就不讨论了,目前还用不上。
Service Worker
-
生命周期
installing -> installed/waiting -> activating -> activated
-
waitUntil 可以扩展生命周期
-
并不是一直在后台运行,而是当监听事件触发时唤起。
-
处理异步任务时,需要使用 waitUntil
-
为什么sw.js 更新后,页面不会产生变化?
每当页面加载一个激活的 service worker,就会检查 service worker 脚本的更新。如果文件 在当前的 service worker 注册之后发生了修改,新的文件就会被注册和安装。安装完成后, 它并不会替换现有的 service worker,而是会保持在 waiting 状态。它将一直停留在这个状 态,直到 service worker 作用域中的每个标签页和窗口关闭,或者导航到一个不在其控制 范围内的页面。只有在当前激活的 service worker 控制的页面全部关闭之后,这个旧的激 活的 service worker 才会进入废弃状态,然后新的 service worker 才会激活。 这就解释了为何我们的应用背景没有发生改变。尝试关闭标签页,然后重新打开,或者导 航到一个不同的网站,然后点击返回按钮。这样应该就能够让旧的 service worker 变成废 弃状态,并且激活新的 service worker,然后背景色最终将会变成红色。
(页码40).
CacheStorage
-
缓存大小会受到浏览器限制
-
为每一个缓存命名,以缓存名称来区分版本。在新版本sw activate 时删除旧版缓存。
-
资源分为可变的和不可变的。对于不可变的资源,可以从上一个缓存中直接获取。
-
sw.js 的缓存策略, 并不是像文档所说,这个文件不可缓存。具体需要看实际场景判断。
由于 service worker 文件在每次加载的时候都会进行检查,你应该修改你的服务器配置,提 供一个较短的过期时间头部(即 1 到 10 分钟)。如果你给它一个很长的过期时间,浏览器 就不会检查它的变化,导致不能发现 service worker 的新版本或者需要缓存的新文件。
(页码47). -
浏览器保护策略,sw.js 的最长缓存时间为24小时。这保证了bug 可以被相对及时的处理。
-
缓存策略
- 仅缓存 —— 离线应用场景
- 缓存优先,网络其次 —— 内容不经常更新
- 仅网络——传统网页场景
- 网络优先,缓存其次 —— 内容常更新,且需要响应新内容
- 先缓存,后网络 —— 双份请求,先返回缓存内容,请求内容与缓存内容对比,如果变动则更新页面。(解决冲突)
- 通用回退 —— 缓存中获取失败,网络不可用。返回旧版缓存内容/预先设置好的内容。
在 Gatsby 中的实践
静态网页的 PWA 缓存策略都差不多。 gatsby-plugin-offline
这个插件已经做了很多工作了。对于静态内容无需做过多的细节优化。下面主要介绍由 netlify functions 的产生动态内容如何优化。
看了一下 public 目录下的 sw.js 。发现了有趣的东西
// /public/sw.js
// Welcome to your Workbox-powered service worker!
workbox 是对 sw 的封装,gatsby-plugin-offline
就是使用的这个框架处理 sw 。关于 workbox 的使用参见
https://codelabs.developers.google.com/codelabs/workbox-lab-cn/index.html?index=..%2F..gddchina#0
http://taobaofed.org/blog/2018/08/08/workbox3/
通读文档后,发现这个 workbox 对上述的缓存策略都做了封装,确实很方便。
Netlify functions 请求缓存
只需对 netlify functions 的请求加一条缓存策略即可,具体使用方法查看 gatsby-plugin-offline
的文档。
// sw 重载
let swConf = config.pwa.swConf
// 对基于 nelify functions 动态页面,缓存其请求
swConf.runtimeCaching.push({
// 这里配置 notion functions url 的正则规则。
urlPattern: /^https?:.*\/.netlify\/functions\/notion/,
handler: `staleWhileRevalidate`,
})
conf.plugins.push({
resolve: `gatsby-plugin-offline`,
options: swConf,
})
因为个人站点的动态内容不太最求时效性。所以选择 staleWhileRevalidate
这个缓存策略。原理如下图所示。这样加快动态页面的加载,从缓存读取数据,当请求完成后,下次访问即可看到更新的内容。
观察动态页面的访问请求,可以看到 netlify functions 的请求已经在走 sw 了。缓存中的也可以看到返回的数据。
下面是演示视频。
秒开,爽爆🚀
新版本重新加载
就像 notion 一样,每次有新版本更新时,重新加载页面。这也是 sw 站点常用的更新手段。
// gatsby-browser.js
export const onServiceWorkerUpdateFound = () => {
const answer = window.confirm(
`好久不见,站点已经更新了。` +
`重新加载,展示新页面?`
)
if (answer === true) {
window.location.reload()
}
}
这样每次 build 后,用户再次访问站点时,不需要手动清空重载网页。网页弹出提示,刷新即可。大大提高了网页的加载速度,用户也可以及时看到最新的网站内容。
总结
总得说来,sw 拦截请求+ 缓存,确实能改善弱网环境下的用户体验,某些场景也减轻了服务端的压力。
PWA 从某种意义上说,算是前端的反击。传统的请求缓存都是后端说了算。cache-control 让你存几秒就是几秒,多一会也不行。现在前端有了 pwa 也可以自己缓存了,能力又强了不少。
目前只是体验了 PWA 的部分功能,消息通知、同步还有页面通信这些功能,我在这个站点上还没找到合适的应用场景。随着功能的不断添加,以后应该会用上。到时候再做这些功能的总结。