正确使用PWA
注:本文需要有一定的 PWA 基础
1. 什么是 PWA?
要知道一个东西是什么,我们通常可以从它的名字入手
因此我们看下 PWA 的全称是: Progressive Web App
回答 what 这种问题,重点在于名词,因此 PWA 是一个 APP,一个独立的、增强的、Web 实现的 APP
要达到这样的目的,PWA 提供了一系列的技术 & 标准,如下图所示:
具体每一项技术是什么就不再赘述了,感兴趣的同学自行网上搜索!
下面有一个简单的 demo 可以简单体会一下:
以后我们的 web 站点可以像 app 一样,这难道不是一个令人兴奋的事情吗?
所以 PWA 是值得我们前端开发者一直关注的技术!
按照目前的兼容性和环境来看,大家应用最多的还是 Service Worker,因此接下来我们也是把重点放在 SW 上面
那什么是 Service Worker ?
大家都知道就不卖关子了,其实就是一个 Cache
说到 Cache,就一定会想到性能优化了,请看我们的第二部分
2. 首屏优化
2.1. 静态资源优化
如何利用 Cache 来进行优化?这个基本套路应该无人不知了:
那么首次加载怎么办呢?首次加载是没有缓存资源的,所以会走到线上,所以等于没有任何优化
答案就是 Cache 的第二种常用技巧: precache(预加载)
预加载的意思就是在某个地方或特定时机预先把需要用到的资源加载并缓存
我们的做法如下图所示:
构建的时候,把整个项目用到的资源输出到一个 list 中,然后 inline 到 sw.js 里面
当 sw install 时,就会把这个 list 的资源全部请求并进行缓存
这样做的结果就是,无论用户第一次进入到我们站点的哪个页面,我们都会把整个站点所有的资源都加载回来并缓存
当用户跳转另外一个页面的时候,Cache 里面就有相应的资源了!
这是我们辅导课堂页面接入 sw 之后的首屏优化效果:
2.2. 动态数据优化
除了静态资源之外,我们还能缓存其他的内容吗?
答案肯定是可以的,我们还可以缓存 cgi 数据!
缓存 cgi 数据的流程和缓存静态资源的流程主要有2个差别,上图标红的地方:
- 需要添加一个开关功能,因为不是所有 cgi 都需要缓存的!
- 页面需要展示最新的数据,因此在返回缓存结果之后,还需要请求线上最新的数据,更新缓存,并且返回给页面展示,也就是说页面需要展示2次
页面展示2次需要考虑页面跳动的体验问题,选择权在于业务本身!
这是我们辅导上课页接入该功能后的首屏优化效果:
动态数据缓存是否有意义还需要额外的逻辑来判断,这块暂时是没有做的,后续会补上相关统计
2.3. 直出html优化
还能缓存什么?我们想到了直出的 html
这里就不展开讲了,因为我们的 jax 同学已经分享了一篇优秀的文章《企鹅辅导课程详情页毫秒开的秘密 – PWA 直出》,可以去看看哈~
3. 替代离线包
PWA 与离线包本质上是一样的,都是离线缓存
正好,我们 PC 客户端的离线包系统年久失修,在这个契机下,我们启动了使用 PWA 替换离线包的方案!
核心流程不变,基本和缓存静态资源的流程是一致的
但是离线包系统是非常成熟的系统,要完全替换掉它还需要考虑许多方面的问题。
3.1. 更新机制
离线包有个自动更新的机制,每隔一段时间就会去请求离线包管理系统是否有更新,有的话就把最新的离线包拉下来自动更新替换,这样只需要1次跳转就能展示最新的页面。
SW 没有自动更新的逻辑,它需要在页面加载(一次跳转)之后才会去请求 sw.js,判断有变化才会进行更新,更新完了要等到下一次页面跳转(二次跳转)才能展示最新的页面。
这里有两个方案:
- 参考离线包的更新机制,也给 SW 实现一个自动更新的逻辑,借用 update 接口是可以做到主动去执行 SW 更新的。但是非常遗憾,我们的客户端 webkit 内核版本太低,并不支持这个接口
- 在第一次跳转之后更新 sw,然后检测 sw 状态,发现如果有更新,就用一定的策略来进行页面的刷新
我们使用第2个方案,部分代码如下:
在检测到 sw 更新之后,我们可以选择强刷,或者提示用户手动刷新页面,具体实现页面可以通过监听事件来处理
更多详细的实现方案可以参考这篇文章哈:How to Fix the Refresh Button When Using Service Workers
3.2. 首次打开问题
一般离线包是打进 app 的安装包一起发布的,在用户下载安装之后,离线包就已经存在于本地,因此第一次打开就能享受到离线包的缓存。
但是 sw 没有这个能力,同样我们也有两个方案:
- 在 app 安装的时候,添加一步,通过创建 webview 加载页面,页面执行 SW 的初始化工作,并展示相应的进度提示,在安装完成后需要把 webview 的 SW Cache 底层文件 copy 到相应的安装位置。这个方案太复杂,衡量下来没有采用。
- 在 app 启动时,创建一个隐藏的 webview,加载空页面去加载 SW。
我们使用第2个方案,因为我们的 app 启动会先让用户登录,如下图所示:
注:这里 app 不是移动端 app,是 pc 的客户端 app
3.3. 屏蔽机制
有时候我们不想使用离线缓存能力,比如在我们开发的时候
在离线包系统,通常会有一个开发者选项是【屏蔽离线包】
SW 也是需要这种能力的,这个方案就比较简单了,在 sw.js 的逻辑里有一个全局的开关,当开关关闭时,就不会走缓存逻辑
因此,我们可以在 dev 环境下把开关关闭即可达到屏蔽的作用
3.4. 降级方案
当我们发布了一个错误代码的时候,我们需要快速降级容错的能力
在离线包系统里面有个过期的功能,可以把某个版本设置过期,也就是废弃掉:
我们利用之前提到的全局开关,通过一个管理接口来设置开关的起开和关闭,即可达到快速降级的目的:
整个流程大致是这样:
- 发布了错误代码,并且用户本地 sw 已经更新缓存了错误代码
- 在管理端关闭使用缓存开关,让用户走线上
- 快速修复代码并发布,到这里页面就已经恢复正常
- 在管理端开启使用缓存开关,恢复 SW 功能
请求管理接口是轮询的,这里后续有计划会改成 push,这样会更加及时,当然还要详细评估方案之后才能落实。
4. 如何方便接入?
我们把上述功能集成到了一个 webpack 插件当中,在构建的时候就自动输出 sw.js 并把相关内容注入到 html 文件中,该插件正准备开源哈~
5. 未来
未来对于 PWA 还能做些什么?笔者以为有以下 2 个方面
5.1. 业务深耕
目前通用的能力已经基本挖掘完成,但是 SW 因为它独特的特性,它能做的事情太多了,但是具体要不要这么做也是因业务而异,而且这些内容可能会很复杂,所以我称为业务深耕。
SW 什么特性?请看下面 2 张图就可以理解了
这种架构相信已经能够看出来了,没错,SW 有间件(层)的特性,那它能做的东西就太多了(虽然 SW 是用户端本地中间层)
简单举几个例子:
- server 负载控制:当发现 server 端高负载时,SW 可以丢弃请求,或者缓存请求,合并/延迟发送。
- 预请求:SW 能预缓存的资源是可以构建出来的资源,但是我们还有许多资源是不能在构建阶段知道的,比如图片,第三方资源等,SW 在返回资源请求(比如HTML、cgi 数据)之后,可以扫描资源里面的内容,如果发现包含了其他资源的 url,那说明页面很有可能待会就会请求这些资源,这时 SW 可以提前去请求这些资源;再比如翻页的数据
- 缓存自动更新:通过与 server 建立联系,当数据有变化时,server 直接把最新的数据 push 到 SW 进行缓存的更新
5.2. 关注 PWA
回到最开始,PWA 是一项令人兴奋的技术,但是浏览器兼容有限,因此期待并关注 PWA 技术的发展是很有必要的!
当然,能推动就更好了!比如推动我们的 X5 内核尽快支持新特性。