Workbox, not sw-toolbox & sw-precache

Workbox, not sw-toolbox & sw-precache

技术向约 2.1 千字

已经 2018 年了,Service Worker 已经不再是一项新奇的黑科技了。Google Chrome 团队推出了 Workbox,计划取代 sw-toolbox 和 sw-precache 实现 Service Worker 动态缓存,用于构建新一代 PWA。

sw-toolbox 和 sw-precache 并不完善、API 的使用也颇为不便,Google Chrome 团队在 2017 年的时候便停止了对 sw-toolbox 和 sw-precache 的更实现动态缓存,使用已经停止维护的 sw-toolbox 也许不是什么好主意吧。

Workbox 概述

如果之前使用过 sw-toolbox,你会觉得 Workbox 的用法和设计意图和要被它取代的东西非常相似,很多实现也都是一脉相承的。简单来讲,Workbox 要实现的也是:

  • 在构建 PWA 时使其支持离线使用
  • 改造任何一个现有的站点使其支持离线访问
  • 高效使用 Cache API 让站点的速度更快
  • 多种比 Browser Cache 更可控的缓存策略
  • 对 Service Worker 的 API 的支持和扩展

Workbox 操作 Cache Storage 的 API 和 sw-toolbox 的一脉相承,不过新增了一个更灵活和强大的 Cache API staleWhileRevalidate,具体请看 Workbox Strategies | Workbox | Google Developers

用法

首先自然先在站点中注册 Service Worker:

<script>
  // Google 官方给出的加载方式
  // 你也可以用 if (navigator.serviceWorker)
  if ('serviceWorker' in navigator) {
    // 为了保证首屏渲染性能,在页面 onload 完之后注册 Service Worker
    // 不使用 window.onload 以避免冲突
    window.addEventListener('load', () => {
      navigator.serviceWorker.register('/sw.js');
    };
  }
</script>

从 Workbox 3.0 开始,Google 将 Workbox 核心组件和插件都放在 Google CDN 上维护。不过 Google Global Cache 在国内的可访问性大家也心知肚明,好在 Workbox 也提供了 Self Hosted 的相关 API。
如果单纯想要把所有 Workbox 的核心和插件都放在本地的话,推荐安装 workbox-cli

$ npm i workbox-cli -g
# Or in Yarn
$ yarn global add workbox-cli
# After installation, run copyLibraries to get a copy of workbox libs
$ workbox copyLibraries {path/to/workbox/}

然后就是在 sw.js 里引入 Workbox:

// load from google cdn
importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.6.1/workbox-sw.js');

/* Or */

// load from local path
importScripts('{path/to}/workbox/workbox-sw.js');
workbox.setConfig({
    modulePathPrefix: '{path/to}/workbox/'
});

如果你想使用公共 CDN 加载 Workbox,可以尝试使用 jsDelivr 或者其它的 NPM CDN。我和 Nuxt-Community 的人合作维护了一个 workbox-cdn 的 NPM 包用来存放 Workbox 的 prebuilt 库。使用 jsDelivr 加载的代码是:

// load from jsDelivr CDN
importScripts('https://cdn.jsdelivr.net/workbox-cdn@3.6.3/workbox/workbox-sw.js');
workbox.setConfig({
    modulePathPrefix: 'https://cdn.jsdelivr.net/workbox-cdn@3.6.3/workbox/'
});

Workbox 实践

通常对于大部分项目使用 Workbox 时一般会引入相应的 gulp 或者 webpack 插件,在构建流程中完成对 Service Worker 的注册、将指定 URL 进行 Precache、完成 sw.js 的生成,等等。但是对于 Hexo、Jekyll 等等静态站点生成器或者 WordPress 这种 CMS,如果不安装相应的插件,就需要自己从头编写一个 sw.js

接下来,我来介绍我的博客在 Workbox 的实践。来简单窥视 Workbox 在动态缓存的应用。

// 定义缓存版本号和默认 Cache Storage 条目数
let cacheSuffixVersion = '-181111';
const maxEntries = 100;

图片静态文件

我的博客里几乎所有的图片都在 ae01..com 域名下面,每张图片有其唯一的 URL 且几乎不更新,所以可以使用 cacheFirst 持久化缓存。

注意,Workbox 和 sw-toolbox 一个很大的不同就是 Workbox 的 cacheFirst 不主动缓存跨域请求、防止错误的请求被缓存。不过 Workbox 提供了一个 cacheableResponse.Plugin override 实现强制缓存的方法。而 Workbox 的 staleWhileRevalidatenetworkFirst 不会有跨域限制,因为这两类 handler 优先使用网络、可能修复错误的请求。

workbox.routing.registerRoute(
    // 使用正则表达式匹配路由
    new RegExp('^https://ae01\.alicdn\.com'),
    workbox.strategies.cacheFirst({
        // cache storage 名称和版本号
        cacheName: 'img-cache' + cacheSuffixVersion,
        plugins: [
            // 使用 expiration 插件实现缓存条目数目和时间控制
            new workbox.expiration.Plugin({
                // 最大保存项目
                maxEntries,
                // 缓存 30 天
                maxAgeSeconds: 30 * 24 * 60 * 60,
            }),
            // 使用 cacheableResponse 插件缓存状态码为 0 的请求
            new workbox.cacheableResponse.Plugin({
                statuses: [0, 200],
            }),
        ]
    })
);

公共 CDN 库

前端开源项目公共 CDN 的文件,如果指定了库的版本,也几乎是永久不会发生改变的,一样可以使用 cacheFirst 做持久化缓存。

workbox.routing.registerRoute(
    new RegExp('^https://cdn\.jsdelivr\.net'),
    workbox.strategies.cacheFirst({
        cacheName: 'static-lib' + cacheSuffixVersion,
        plugins: [
            new workbox.expiration.Plugin({
                maxAgeSeconds: 30 * 24 * 60 * 60,
            }),
            new workbox.cacheableResponse.Plugin({
                statuses: [0, 200],
            }),
        ]
    })
);

站点访问统计

虽然 Workbox 有针对离线访问的 Google Analytics 统计插件,可以统计用户的离线访问、并在用户联网后异步回传数据;但是由于我的博客使用的是非官方的 GA 实现,所以只能直接对相关请求做 networkOnly 、放弃对离线访问的统计。如果想要实现 GA 离线统计的可以查看 Workbox 关于 Enable Offline Google Analytics 文档

workbox.routing.registerRoute(
    new RegExp('^https://ga\.skk\.moe'),
    workbox.strategies.networkOnly()
);

Disqus API 反代

我开发了 DisqusJS 作为「评论基础模式」,当然自己也要使用了。DisqusJS 依赖一个 Disqus API 的反代,针对相关请求使用 staleWhileRevalidate,即缓存后优先使用缓存,同时后台异步再发起一次完整网络请求以更新 Cache。针对 Disqus API 使用这个 Cache API 可以相对及时的更新评论数据、同时在用户离线时也能阅读评论。

虽然为了提供更及时的数据应该使用 networkFirst,但是 staleWhileRevalidate 响应更快。

workbox.routing.registerRoute(
    new RegExp('https://disqus\.skk\.moe'),
    workbox.strategies.staleWhileRevalidate({
        cacheName: 'disqus-api-cache' + cacheSuffixVersion,
    })
);

Disqus

DisqusJS 判断访客的 Disqus 可用性是通过检查 shortname.disqus.com/favicon.icodisqus.com/favicon.ico,所以显然不能被缓存;另外,Disqus 本身也没有缓存的必要,所以对 *.disqus.com 一律使用 networkOnly;然而 *.disquscdn.com 域名下的静态文件(比如头像、还有一些 Disqus 的 JS)是可以缓存的,所以用 cacheFirst 缓存 10 天:

workbox.routing.registerRoute(
    new RegExp('^https://(.*)disqus\.com'),
    workbox.strategies.networkOnly()
);
workbox.routing.registerRoute(
    new RegExp('^https://(.*)disquscdn\.com'),
    workbox.strategies.cacheFirst({
        cacheName: 'disqus-cdn-cache' + cacheSuffixVersion,
        plugins: [
            new workbox.expiration.Plugin({
                maxAgeSeconds: 10 * 24 * 60 * 60,
            }),
            new workbox.cacheableResponse.Plugin({
                statuses: [0, 200],
            }),
        ]
    })
);

后缀匹配

针对其余没有被域名匹配到的静态文件,通过文件后缀进行匹配、使用 staleWhileRevalidate,可以兼顾速度和尽可能及时的更新。

workbox.routing.registerRoute(
    // Cache Image File
    /.*\.(?:png|jpg|jpeg|svg|gif)/,
    workbox.strategies.staleWhileRevalidate({
        cacheName: 'img-cache' + cacheSuffixVersion,
    })
);

workbox.routing.registerRoute(
    // Cache CSS & JS files
    /.*\.(css|js)/,
    workbox.strategies.staleWhileRevalidate({
        cacheName: 'static-assets-cache',
    })
);

默认行为

使用 Workbox 的 defaultHandler 匹配剩下的请求(包括页面自身),一律使用 networkFirst,借助 Workbox 的 runtimeCache 起到加速和离线效果:

workbox.routing.setDefaultHandler(
    workbox.strategies.networkFirst({
        options: [{
            // 超过 3s 请求没有响应则 fallback 到 cache
            networkTimeoutSeconds: 3,
        }]
    })
);

如果对以上提到的各种 handler 和各自的含义有疑问的,可以阅读 The Offline Cookbook - responding to requests | Web Fundamentals | Google Developers

经过实践可以发现,Workbox 的动态缓存的思路和行为和 sw-toolbox 是相似的。但是在使用过程中也可以发现 Workbox 显然要严格一些(比如在对跨域请求的处理);相比 sw-toolbox 从 origin 和 path 两部分匹配路由,Workbox 直接用正则表达式匹配完整 URL 的方法也更容易;Workbox 推出的 staleWhileRevalidate 也很好的照顾了经常更新和需要缓存的文件(比如 css)。

sw-toolbox 和 Workbox 对比

sw-toolbox 是一个完整的、包含了完整的 Service Worker 的 API 的库,而 Workbox 使用插件机制、动态载入相关组件;sw-precache 和 sw-toolbox 问世较早,相应的 Gulp、Grunt、Webpack 插件层出不穷,也有很多相关资料;Workbox 暴露了很多插件接口供开发者编写自己的插件;在实践中也可以发现 Workbox 对路由管理的实现比 sw-toolbox 要更好,staleWhileRevalidate 也非常强大;Workbox 吸收了之前包括 sw-precache 和 sw-toolbox 在内的类库,集成了预缓存和动态缓存的实现。

目前,Google Chrome 团队并没有停止对 sw-toolbox 和 sw-precache 的支持,所以现有的使用 sw-toolbox 和 sw-precache 的项目可以维持现状,但是新的项目还是应该使用更新的 Workbox。

参考资料

Workbox, not sw-toolbox & sw-precache
本文作者
Sukka
发布于
2018-11-02
更新于
2019-01-30
许可协议
转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!
如果你喜欢我的文章,或者我的文章有帮到你,可以考虑一下打赏作者
评论加载中...