如何使用 service worker 管理复杂 WEB 站点
作者:翔天盛世
发布时间:2022-08-07 15:00
浏览数:578

通过阅读如何优雅的为 PWA 注册 Service Worker一文,相信大家已经对什么是 Service Worker 以及如何使用注册有所了解。对于一些比较简单的小型站点这已足够,但当我们的业务壮大,站点复杂之后,会出现一些新的问题。

通常一个复杂的 WEB 站点的内容会由多个模块提供,这些模块可以拥有自己的 URL pattern(如用户模块/user,信息模块/about等等)。它们相互独立,自己决定自己的展现内容,逻辑,样式等等。理所当然地,如果我们引入 Service Worker 优化站点,那么每个模块也应当能够管理自身使用到的静态资源。

一个大家比较熟知的例子是百度搜索结果页。当我们搜索关键词“北京旅游”,能够得到的页面其实由不同的模块生成,如下:

这个页面上了多个模块的渲染结果,仔细观察网络请求不难发现,在请求每个模块的静态资源时,header 信息中所携带的 referrer 是各不相同的,因此实际上和多个模块拥有不同 URL 的情况是类似的。

Service Worker 组织方式

面对由多个模块组成的站点,Service Worker有两种组织方式。

全局仅注册一个 service-worker.js, scope 设置为 '/' ,统一管理所有模块的静态资源。每个模块注册一个 service-worker.js,scope 设置为每个模块的 URL pattern ,互相独立地管理自身

方案1的缺点显而易见,任何模块对缓存内容或策略的修改都必须同步到全局 js 文件中,因此这个 js 文件的维护是一个重点。方案2看起来更加符合独立的思路,因此方案2胜出?

如果一个站点的所有模块都是互斥的,那方案2的确更优秀一些,但实际操作中并没有如此理想。一个最简单的例子,主页一般 URL 为 /,那么他所在的模块的 scope 就会包含其他模块。一旦有了这样的父子层级,会导致主页模块的 Service Worker 能缓存其他所有模块的静态资源。这样静态资源会存在多份,就会出现不同步和更新的问题。

因此我们其实更倾向于方案1,那么有没有方法用程序生成这个全局的 Service Worker,减低维护成本呢?

解决方案

我们可以通过请求静态资源时 header 的 referrer 信息(即模块的 URL pattern)来把模块相互区分开来。每个模块提供自身的缓存配置信息,主要包含三点内容:

如何确定是本模块(提供自身的 referrer 规则)本模块需要缓存哪些静态资源本模块需要缓存的静态资源应该以什么策略缓存

通过每个模块提供的配置信息,系统自动生成一份 service-worker.js,经由下方的注册代码,可以实现为每个模块分别缓存数据,互不影响,从而解决上述问题。

if ('serviceWorker' in navigator) { navigator.serviceWorker.register('service-worker.js').then(function (registration) { // registration was successful console.log('service worker registration successful with scope ', registration.scope); }).catch(function (err) { // registration failed console.log(err); }); }

那么问题归结为如何通过这些配置信息生成 service-worker.js。

设计思路

在实际设计解决方案之前,让我们先考虑下除了满足“互相隔离”,“自动快速生成”等功能性需求之外,还有什么需要考虑的?这里有几点参考:

缓存策略易于扩展模块较多时拥有较好的性能,否则起不到 PWA 本身的目的各类浏览器和环境下比较完备的

基于这样的需求,我们给出一种设计思路,供大家讨论交流:

sw-base

对外唯一接口,提供 add, precache 等方法对需要缓存的规则或者文件进行注册。此外还应在这里调用 service worker 的 self.addEventListener 方法进行注册。

defaults

记录配置项和缓存策略的默认值,同时也确保用户配置文件的合法性,过滤非法项并和默认值进行合并

precache

记录预缓存文件列表

strategy

保存所有缓存策略,由统一出口(如 index.js)对外暴露的接口,这样成功将策略部分和上层部分解耦,保证缓存策略的可扩展性

router

提供 add 和 ** tch 方法。add 用于添加路由规则, ** tch 用于匹配路由规则并选择缓存规则。其中匹配部分优先匹配 referrer 再匹配路由规则,因此在模块较多时是先选定一个模块再匹配路由规则,避免匹配内容过多影响性能。

listeners

提供 service worker 的各个阶段事件的响应函数,如 install, activate 和 fetch 事件。

此外,最外层还应有一个主入口文件(如 ** in.js),将所有位于配置目录下的配置文件加载并逐个调用 sw-base 的 add 和 precache 方法,完成注册。

其他细节

A. 构建

让我们先理清有哪些构建的要求:

最基本的代码合并和混淆,最终生成一个 service-worker.js 文件插入 serviceworker-cache-polyfill 以解决 cache 接口在旧版浏览器的兼容问题源代码采用 es6/7 编写,因此需要引入 babel 编译成 es5为了支持,需要一个额外的入口引入相关的缓存配置,而非所有正式缓存配置

显然 webpack 可以很好的完成这些构建需求。

B.

我们可以参考 sw-toolbox 的方法,在构建出供的 service-worker.js 之后,进行如下流程:

使用 selenium-assistant 下载需要的浏览器内核(如 chrome, firefox 等)使用 express 编写一个简易服务器,供使用使用 mocha 启动上述服务器,并逐个运行用例

用例可以涵盖所有策略,也包括其他的一些功能(如配置项,HTTP方法和预缓存等),基本思路都是先验证 cache 的某个 key 能否正常读写以及淘汰机制等等。

C. 验证

通过,我们可以确认项目自身代码的可靠性(包括缓存策略本身以及使用策略等等)。但每个模块的配置文件是由用户自身提供的,并不能覆盖这一部分,而错误的配置文件会导致生成的 service-worker.js 对站点产生不可预知的影响(如多个模块的 referrer 存在包含关系导致同一个文件出现多份缓存等)。因此我们需要对用户提供的缓存配置项进行验证。

为了支持验证,用户在提交配置文件时,也要提交符合自身提供规则的验证 URL。举例来说,用户提交的 referrer 为 '/user/*',那么也应该提交一条符合这条规则的 validateReferrer,例如 '/user/info'。

验证主要分为两大类:

内部检查检查每条 validate 是否能被 urlPattern 匹配,如不能则报错检查每条 precache 是否能被 urlPattern 匹配,如不能则报错交叉检查 (两两比对)检查两个配置文件的 name 是否相同,如相同则报错检查配置A的每条 validate url 是否能被配置B的 urlPattern 匹配,如能则报错

具体实现

事实上我们根据上述设计思路已经实现了一个项目 crater (github),并将于近期在百度搜索结果页进行上线。上线之后,将对百度搜索结果页中的两类内容(普通搜索结果和智能搜索聚合)的静态资源进行缓存,以提升访问性能和用户体验。

也欢迎各位同僚来到 github 项目社区提出宝贵建议和意见,共同打造以 PWA 为主的 WEB 新生态,提升 WEB 站点的用户体验。

地址:北京珠江摩尔国际大厦
电话:18516882688
邮箱:xcni@qq.com
关注我们
Copyright @ 2010 - 2022 京ICP备11047770号-8 京公网安备11011402012373号