Service Workerinstall 事件中,我们一般会对静态资源进行缓存处理,比如:

self.addEventListener('install', event => {
  event.waitUntil((async () => {
    const cache = await caches.open('precache');
    await cache.addAll([
      '/',
      '/index.html',
      '/main.css',
      '/main.js',
      '/image.jpg'
    ]);
  })());
});

这种在安装阶段将资源进行缓存以便 Service Worker 变为可用后可直接从本地缓存中获取资源的能力,我们称之为预缓存(prechching)。它与运行时缓存(Service Worker 可用后,通过监听其 fetch 事件,将资源请求结果动态添加到缓存中的机制)一起为 Web 应用的离线访问提供了技术支持

# 自动生成预缓存资源列表

上例中,我们以硬编码的形式定义了预缓存资源列表,这在 Web 应用愈加复杂、前端构建及工程体系逐步完善的今天,既效率低下,又容易出错,因此本节我们借用 webpack 来简单实现资源列表的自动生成。

首先,我们如下修改 sw.js 文件:

const precacheList = <%- precacheList %>;
self.addEventListener('install', event => {
  event.waitUntil((async () => {
    const cache = await caches.open(precacheName);
    await cache.addAll(precacheList);
  })());
});

我们通过定义一个 precacheList 常量并将其作为参数传递给 cache.addAll 方法来替换硬编码资源列表,precacheList 的值使用了 ejs 模板语法,该值会在执行 build时替换成真实资源列表,比如:

const precacheList = ["/db.90cab081eccbdfa6e090fc6ebbadb90f.js","/plus.6b433cf1453965994b3029ea10ec8449.png","/home.5704e93d911a9fcdaf14.css"];

可究竟如何生成这些真实的资源信息呢?非常简单,我们只需实现一个简单的 webpack plugin 即可。

创建 SWFilePlugin.js

const path = require('path');
const memFs = require('mem-fs');
const editor = require('mem-fs-editor');

class SWFilePlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync('SWFilePlugin', (compilation, callback) => {
      const publicPath = compilation.mainTemplate.getPublicPath({
        hash: compilation.hash
      });
      const assets = Object.keys(compilation.assets).map(asset => `${publicPath}${asset}`);
      const fsEditor = editor.create(memFs.create());
      fsEditor.copyTpl(
        path.join(__dirname, '../../client/sw.js'),
        path.join(__dirname, '../../public/sw.js'),
        {
          precacheList: JSON.stringify(assets)
        }
      );
      fsEditor.commit(() => {
        callback();
      })
    });
  }
}

module.exports = SWFilePlugin;

在 apply 方法中,我们在 webpack compiler 的 emit 钩子中通过:

const publicPath = compilation.mainTemplate.getPublicPath({
  hash: compilation.hash
});
const assets = Object.keys(compilation.assets).map(asset => `${publicPath}${asset}`);
  • 来获取资源列表,然后通过 mem-fs-editorcopyTpl 方法来生成 precacheList 值已被替换的 sw.js 文件。
  • 代码非常简单,唯一需要注意的是 precacheList 的值需通过 JSON.stringify(assets) 将其转换为字符串,否则,将生成以下内容:
const precacheList = /db.90cab081eccbdfa6e090fc6ebbadb90f.js,/plus.6b433cf1453965994b3029ea10ec8449.png,/home.5704e93d911a9fcdaf14.css;

这样的代码是无法解释执行的。

完成了 SWFilePlugin,接下来需要如下修改 webpack.config.js

const { SWFilePlugin } = require('./webpack/plugins');

module.exports = {
  //... 其他配置
  plugins: [
    //... 其他插件
    new SWFilePlugin()
  ]
};

至此我们便完成了自动生成资源列表所需的所有工作,可下载示例,执行 yarn build 命令来查看最终生成的 public/sw.js 与原始文件 client/sw.js 的差异。

# 资源更新

  • 当资源或 Service Worker 更新后,我们需要对缓存进行更新。对于资源更新后的缓存更新,我们在后面的缓存策略中进行讨论,本节我们只讨论 Service Worker 更新后的缓存更新机制。
  • 我们一般在 Service Workeractivate事件中对缓存进行更新操作,根据 precacheName 是否改变,存在以下两种更新策略:
  1. 如果 precacheName 改变,直接删除 cacheName 与当前 precacheName 不相同且符合预缓存命名规则(非预缓存资源无需删除)的缓存,比如:
self.addEventListener('activate', event => {
  event.waitUntil((async () => {
    const cacheNames = await caches.keys();
    for (const cacheName of cacheNames) {
      if (cacheName !== precacheName && /^precache\-\d+$/.test(cacheName)) {
        await caches.delete(cacheName);
      }
    }
  })());
});
  1. 如果 precacheName 尚未改变,则删除 precacheList 中不存在的预缓存项,比如:
 self.addEventListener('activate', event => {
  event.waitUntil((async () => {
    const cache = await caches.open(precacheName)
    const requests = await cache.keys();
    for (const request of requests) {
      const { pathname } = new URL(request.url, location);
      if (!precacheList.includes(pathname)) {
        await cache.delete(request);
      }
    }
  })());
});

在本文附带的示例中,我们采用第一种方式进行来处理 Service Worker 更新后的缓存更新。precacheName 的动态赋值与上文中的 precacheList 类似,首先在 sw.js 中定义以下常量:

const precacheName = '<%= precacheName %>';

然后修改 SWFilePlugin.js

//... require 依赖

class SWFilePlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync('SWFilePlugin', (compilation, callback) => {
      //... 资源列表获取
      fsEditor.copyTpl(
        path.join(__dirname, '../../client/sw.js'),
        path.join(__dirname, '../../public/sw.js'),
        {
          precacheName: `precache-${(new Date()).getTime()}`,
          //... 其他属性设置
        }
      );
      fsEditor.commit(() => {
        callback();
      })
    });
  }
}

module.exports = SWFilePlugin;

# 总结

上文中,我们从自动生成预缓存资源列表与资源更新两个方面对 Service Worker 预缓存的使用进行了说明,当它与接下来要介绍的应用 Shell 组合使用后,即使在 Service Worker 变为可用后瞬间掉线,我们的 Web 应用依旧可以提供良好的用户体验,而不是出现类似以下的异常页面:

阅读全文