在
Service Worker的install事件中,我们一般会对静态资源进行缓存处理,比如:
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-editor 的
copyTpl方法来生成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 Worker的activate事件中对缓存进行更新操作,根据precacheName是否改变,存在以下两种更新策略:
- 如果
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);
}
}
})());
});
- 如果
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 应用依旧可以提供良好的用户体验,而不是出现类似以下的异常页面:
