通过 Service Worker,我们放弃了尝试解决离线问题,而是为开发者提供了移动部件,让他们自行解决。它让您可以控制缓存以及请求的处理方式。这意味着您可以创建自己的模式。让我们单独了解一些可能的模式,但在实践中,您可能会根据 URL 和上下文同时使用其中的许多模式。
有关其中一些模式的实际演示,请参阅 Trained-to-thrill,以及此视频,其中展示了性能影响。
缓存机制——何时存储资源
Service Worker 让您可以独立于缓存处理请求,因此我将分别演示它们。首先是缓存,应该在何时完成?
安装时 - 作为依赖项

Service Worker 为您提供一个 install
事件。您可以使用它来准备好东西,这些东西必须在您处理其他事件之前准备好。当这种情况发生时,您 Service Worker 的任何先前版本仍在运行并提供页面,因此您在此处执行的操作不得中断它。
理想用途:CSS、图像、字体、JS、模板……基本上任何您认为对您网站的“版本”是静态的内容。
这些是如果无法获取,将导致您的网站完全无法运行的内容,等效的特定于平台的应用程序会将这些内容作为初始下载的一部分。
self.addEventListener('install', function (event) {
event.waitUntil(
caches.open('mysite-static-v3').then(function (cache) {
return cache.addAll([
'/css/whatever-v3.css',
'/css/imgs/sprites-v6.png',
'/css/fonts/whatever-v8.woff',
'/js/all-min-v4.js',
// etc.
]);
}),
);
});
event.waitUntil
接受一个 Promise 以定义安装的持续时间和成功与否。如果 Promise 被拒绝,则安装将被视为失败,并且此 Service Worker 将被放弃(如果较旧版本正在运行,它将保持不变)。caches.open()
和 cache.addAll()
返回 Promise。如果任何资源无法获取,则 cache.addAll()
调用将拒绝。
在 trained-to-thrill 中,我使用它来缓存静态资源。
安装时 - 不作为依赖项

这与上述类似,但不会延迟安装完成,也不会在缓存失败时导致安装失败。
理想用途:不需要立即使用的大型资源,例如游戏中稍后关卡的资源。
self.addEventListener('install', function (event) {
event.waitUntil(
caches.open('mygame-core-v1').then(function (cache) {
cache
.addAll
// levels 11–20
();
return cache
.addAll
// core assets and levels 1–10
();
}),
);
});
上面的示例未将关卡 11–20 的 cache.addAll
Promise 传递回 event.waitUntil
,因此即使失败,游戏仍可离线使用。当然,您必须考虑可能缺少这些关卡的情况,并在它们丢失时重新尝试缓存它们。
Service Worker 可能会在关卡 11–20 下载时被终止,因为它已完成事件处理,这意味着它们不会被缓存。将来,Web Periodic Background Synchronization API 将处理此类情况以及更大的下载(如电影)。该 API 目前仅在 Chromium 分支中受支持。
激活时

理想用途:清理和迁移。
一旦新的 Service Worker 已安装且未使用以前的版本,则新版本将激活,并且您将获得一个 activate
事件。由于旧版本已不再使用,因此这是处理 IndexedDB 中的架构迁移以及删除未使用的缓存的好时机。
self.addEventListener('activate', function (event) {
event.waitUntil(
caches.keys().then(function (cacheNames) {
return Promise.all(
cacheNames
.filter(function (cacheName) {
// Return true if you want to remove this cache,
// but remember that caches are shared across
// the whole origin
})
.map(function (cacheName) {
return caches.delete(cacheName);
}),
);
}),
);
});
在激活期间,诸如 fetch
之类的其他事件将放入队列中,因此长时间激活可能会阻止页面加载。尽可能保持激活精简,并且仅将其用于您在旧版本处于活动状态时无法执行的操作。
在 trained-to-thrill 中,我使用它来删除旧缓存。
用户交互时

理想用途:当整个网站无法离线时,并且您选择允许用户选择他们想要离线可用的内容。例如,YouTube 上的视频、Wikipedia 上的文章、Flickr 上的特定图库。
为用户提供“稍后阅读”或“保存以供离线使用”按钮。单击该按钮时,从网络获取您需要的内容并将其放入缓存中。
document.querySelector('.cache-article').addEventListener('click', function (event) {
event.preventDefault();
var id = this.dataset.articleId;
caches.open('mysite-article-' + id).then(function (cache) {
fetch('/get-article-urls?id=' + id)
.then(function (response) {
// /get-article-urls returns a JSON-encoded array of
// resource URLs that a given article depends on
return response.json();
})
.then(function (urls) {
cache.addAll(urls);
});
});
});
caches API 可从页面以及 Service Worker 中获得,这意味着您可以直接从页面添加到缓存。
网络响应时

理想用途:频繁更新的资源,例如用户的收件箱或文章内容。也适用于非必要内容,例如头像,但需要谨慎。
如果请求与缓存中的任何内容都不匹配,请从网络获取它,将其发送到页面,并同时将其添加到缓存。
如果您对一系列 URL(例如头像)执行此操作,则需要注意不要过度膨胀您来源的存储空间。如果用户需要回收磁盘空间,您不希望成为主要对象。确保您摆脱缓存中不再需要的项目。
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.open('mysite-dynamic').then(function (cache) {
return cache.match(event.request).then(function (response) {
return (
response ||
fetch(event.request).then(function (response) {
cache.put(event.request, response.clone());
return response;
})
);
});
}),
);
});
为了实现高效的内存使用,您只能读取一次响应/请求的正文。上面的代码使用 .clone()
来创建可以单独读取的其他副本。
在 trained-to-thrill 中,我使用它来缓存 Flickr 图像。
过时内容重新验证

理想用途:频繁更新的资源,其中拥有最新版本并非必不可少。头像可以归为此类。
如果有可用的缓存版本,请使用它,但获取更新以供下次使用。
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.open('mysite-dynamic').then(function (cache) {
return cache.match(event.request).then(function (response) {
var fetchPromise = fetch(event.request).then(function (networkResponse) {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
return response || fetchPromise;
});
}),
);
});
这与 HTTP 的 stale-while-revalidate 非常相似。
推送消息时

Push API 是构建在 Service Worker 之上的另一项功能。这允许 Service Worker 响应来自操作系统消息服务的消息而被唤醒。即使在用户没有打开您网站的标签页的情况下也会发生这种情况。只有 Service Worker 会被唤醒。您从页面请求执行此操作的权限,并且系统将提示用户。
理想用途:与通知相关的内容,例如聊天消息、突发新闻或电子邮件。同样适用于不经常更改但受益于即时同步的内容,例如待办事项列表更新或日历更改。
常见的最终结果是一个通知,当点击该通知时,会打开/聚焦相关页面,但在此之前更新缓存非常重要。用户在收到推送消息时显然在线,但当他们最终与通知交互时,他们可能不在线,因此使此内容可离线使用非常重要。
此代码在显示通知之前更新缓存
self.addEventListener('push', function (event) {
if (event.data.text() == 'new-email') {
event.waitUntil(
caches
.open('mysite-dynamic')
.then(function (cache) {
return fetch('/inbox.json').then(function (response) {
cache.put('/inbox.json', response.clone());
return response.json();
});
})
.then(function (emails) {
registration.showNotification('New email', {
body: 'From ' + emails[0].from.name,
tag: 'new-email',
});
}),
);
}
});
self.addEventListener('notificationclick', function (event) {
if (event.notification.tag == 'new-email') {
// Assume that all of the resources needed to render
// /inbox/ have previously been cached, e.g. as part
// of the install handler.
new WindowClient('/inbox/');
}
});
后台同步时

后台同步 是构建在 Service Worker 之上的另一项功能。它允许您请求后台数据同步作为一次性同步,或按(极其启发式的)间隔同步。即使在用户没有打开您网站的标签页的情况下也会发生这种情况。只有 Service Worker 会被唤醒。您从页面请求执行此操作的权限,并且系统将提示用户。
理想用途:非紧急更新,尤其是那些发生得如此频繁以至于每次更新都推送消息对于用户来说过于频繁的更新,例如社交时间线或新闻文章。
self.addEventListener('sync', function (event) {
if (event.id == 'update-leaderboard') {
event.waitUntil(
caches.open('mygame-dynamic').then(function (cache) {
return cache.add('/leaderboard.json');
}),
);
}
});
缓存持久性
您的来源被赋予一定的可用空间来执行其想要的操作。该可用空间在所有来源存储之间共享:(本地)存储、IndexedDB、File System Access,当然还有 Caches。
您获得的数量未指定。它会因设备和存储条件而异。您可以通过以下方式找到您拥有的数量
if (navigator.storage && navigator.storage.estimate) {
const quota = await navigator.storage.estimate();
// quota.usage -> Number of bytes used.
// quota.quota -> Maximum number of bytes available.
const percentageUsed = (quota.usage / quota.quota) * 100;
console.log(`You've used ${percentageUsed}% of the available storage.`);
const remaining = quota.quota - quota.usage;
console.log(`You can write up to ${remaining} more bytes.`);
}
但是,与所有浏览器存储一样,如果设备承受存储压力,浏览器可以随意丢弃您的数据。不幸的是,浏览器无法区分您想要不惜一切代价保留的电影,以及您不太关心的游戏。
为了解决这个问题,请使用 StorageManager 接口
// From a page:
navigator.storage.persist()
.then(function(persisted) {
if (persisted) {
// Hurrah, your data is here to stay!
} else {
// So sad, your data may get chucked. Sorry.
});
当然,用户必须授予权限。为此,请使用 Permissions API。
让用户参与此流程非常重要,因为我们现在可以期望他们控制删除。如果他们的设备承受存储压力,并且清除非必要数据无法解决问题,则用户可以判断要保留和删除哪些项目。
为了使其正常工作,它需要操作系统将“持久”来源视为等同于平台特定应用程序在其存储使用情况细分中的地位,而不是将浏览器报告为单个项目。
服务建议——响应请求
无论您进行多少缓存,Service Worker 都不会使用缓存,除非您告诉它何时以及如何使用。以下是一些处理请求的模式
仅缓存

理想用途:您认为对特定“版本”的网站是静态的任何内容。您应该已在安装事件中缓存了这些内容,因此您可以依赖它们的存在。
self.addEventListener('fetch', function (event) {
// If a match isn't found in the cache, the response
// will look like a connection error
event.respondWith(caches.match(event.request));
});
……尽管您通常不需要专门处理这种情况,但缓存,回退到网络涵盖了它。
仅网络

理想用途:没有离线等效项的内容,例如分析 Ping、非 GET 请求。
self.addEventListener('fetch', function (event) {
event.respondWith(fetch(event.request));
// or simply don't call event.respondWith, which
// will result in default browser behavior
});
……尽管您通常不需要专门处理这种情况,但缓存,回退到网络涵盖了它。
缓存,回退到网络

理想用途:构建离线优先。在这种情况下,这是您处理大多数请求的方式。其他模式将是基于传入请求的例外。
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
return response || fetch(event.request);
}),
);
});
这为您提供了缓存中内容的“仅缓存”行为,以及任何未缓存内容(包括所有非 GET 请求,因为它们无法缓存)的“仅网络”行为。
缓存和网络竞争

理想用途:在磁盘访问速度较慢的设备上追求性能的小型资源。
对于旧硬盘驱动器、病毒扫描程序和更快的互联网连接的某些组合,从网络获取资源可能比访问磁盘更快。但是,当用户设备上已有内容时访问网络可能会浪费数据,因此请记住这一点。
// Promise.race is no good to us because it rejects if
// a promise rejects before fulfilling. Let's make a proper
// race function:
function promiseAny(promises) {
return new Promise((resolve, reject) => {
// make sure promises are all promises
promises = promises.map((p) => Promise.resolve(p));
// resolve this promise as soon as one resolves
promises.forEach((p) => p.then(resolve));
// reject if all promises reject
promises.reduce((a, b) => a.catch(() => b)).catch(() => reject(Error('All failed')));
});
}
self.addEventListener('fetch', function (event) {
event.respondWith(promiseAny([caches.match(event.request), fetch(event.request)]));
});
网络回退到缓存

理想用途:网站“版本”之外频繁更新的资源的快速修复。例如,文章、头像、社交媒体时间线和游戏排行榜。
这意味着您为在线用户提供最新内容,但离线用户获得较旧的缓存版本。如果网络请求成功,您很可能想要更新缓存条目。
但是,此方法存在缺陷。如果用户具有间歇性或缓慢的连接,他们必须等待网络失败,然后才能获得设备上已有的完全可以接受的内容。这可能需要很长时间,并且会给用户带来令人沮丧的体验。有关更好的解决方案,请参阅下一个模式缓存,然后网络。
self.addEventListener('fetch', function (event) {
event.respondWith(
fetch(event.request).catch(function () {
return caches.match(event.request);
}),
);
});
缓存,然后网络

理想用途:频繁更新的内容。例如,文章、社交媒体时间线和游戏。排行榜。
这需要页面发出两个请求,一个请求到缓存,另一个请求到网络。想法是先显示缓存的数据,然后在网络数据到达时/如果到达则更新页面。
有时,您可以在新数据到达时直接替换当前数据(例如,游戏排行榜),但这可能会对较大的内容造成干扰。基本上,不要“消失”用户可能正在阅读或交互的内容。
Twitter 在旧内容上方添加新内容,并调整滚动位置,以便用户不会被打断。这是可能的,因为 Twitter 主要保留了内容的近似线性顺序。我为 trained-to-thrill 复制了这种模式,以便尽快将内容显示在屏幕上,同时在最新内容到达后立即显示。
页面中的代码
var networkDataReceived = false;
startSpinner();
// fetch fresh data
var networkUpdate = fetch('/data.json')
.then(function (response) {
return response.json();
})
.then(function (data) {
networkDataReceived = true;
updatePage(data);
});
// fetch cached data
caches
.match('/data.json')
.then(function (response) {
if (!response) throw Error('No data');
return response.json();
})
.then(function (data) {
// don't overwrite newer network data
if (!networkDataReceived) {
updatePage(data);
}
})
.catch(function () {
// we didn't get cached data, the network is our last hope:
return networkUpdate;
})
.catch(showErrorMessage)
.then(stopSpinner);
Service Worker 中的代码
您应该始终访问网络并在进行过程中更新缓存。
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.open('mysite-dynamic').then(function (cache) {
return fetch(event.request).then(function (response) {
cache.put(event.request, response.clone());
return response;
});
}),
);
});
在 trained-to-thrill 中,我通过使用 XHR 而不是 fetch,并滥用 Accept 标头来告诉 Service Worker 从哪里获取结果(页面代码,Service Worker 代码)。
通用回退

如果您无法从缓存和/或网络提供某些内容,您可能希望提供通用回退。
理想用途:辅助图像,例如头像、失败的 POST 请求以及“离线时不可用”。页面。
self.addEventListener('fetch', function (event) {
event.respondWith(
// Try the cache
caches
.match(event.request)
.then(function (response) {
// Fall back to network
return response || fetch(event.request);
})
.catch(function () {
// If both fail, show a generic fallback:
return caches.match('/offline.html');
// However, in reality you'd have many different
// fallbacks, depending on URL and headers.
// Eg, a fallback silhouette image for avatars.
}),
);
});
您回退到的项目很可能是安装依赖项。
如果您的页面正在发布电子邮件,则您的 Service Worker 可能会回退到将电子邮件存储在 IndexedDB“发件箱”中,并通过让页面知道发送失败但数据已成功保留来响应。
Service Worker 端模板

理想用途:无法缓存其服务器响应的页面。
在服务器上呈现页面可以加快速度,但这可能意味着包含缓存中可能没有意义的状态数据,例如“以…身份登录”。如果您的页面由 Service Worker 控制,您可以选择改为请求 JSON 数据以及模板,并改为呈现该数据。
importScripts('templating-engine.js');
self.addEventListener('fetch', function (event) {
var requestURL = new URL(event.request.url);
event.respondWith(
Promise.all([
caches.match('/article-template.html').then(function (response) {
return response.text();
}),
caches.match(requestURL.path + '.json').then(function (response) {
return response.json();
}),
]).then(function (responses) {
var template = responses[0];
var data = responses[1];
return new Response(renderTemplate(template, data), {
headers: {
'Content-Type': 'text/html',
},
});
}),
);
});
组合在一起
您不限于这些方法之一。实际上,您可能会根据请求 URL 使用其中的许多方法。例如,trained-to-thrill 使用
- 安装时缓存,用于静态 UI 和行为
- 网络响应时缓存,用于 Flickr 图像和数据
- 从缓存提取,回退到网络,用于大多数请求
- 从缓存提取,然后网络,用于 Flickr 搜索结果
只需查看请求并决定要执行的操作
self.addEventListener('fetch', function (event) {
// Parse the URL:
var requestURL = new URL(event.request.url);
// Handle requests to a particular host specifically
if (requestURL.hostname == 'api.example.com') {
event.respondWith(/* some combination of patterns */);
return;
}
// Routing for local URLs
if (requestURL.origin == location.origin) {
// Handle article URLs
if (/^\/article\//.test(requestURL.pathname)) {
event.respondWith(/* some other combination of patterns */);
return;
}
if (/\.webp$/.test(requestURL.pathname)) {
event.respondWith(/* some other combination of patterns */);
return;
}
if (request.method == 'POST') {
event.respondWith(/* some other combination of patterns */);
return;
}
if (/cheese/.test(requestURL.pathname)) {
event.respondWith(
new Response('Flagrant cheese error', {
status: 512,
}),
);
return;
}
}
// A sensible default pattern
event.respondWith(
caches.match(event.request).then(function (response) {
return response || fetch(event.request);
}),
);
});
……您明白了。
鸣谢
……感谢这些可爱的图标
- Code by buzzyrobot
- Calendar by Scott Lewis
- Network by Ben Rizzo
- SD by Thomas Le Bas
- CPU by iconsmind.com
- Trash by trasnik
- Notification by @daosme
- Layout by Mister Pixel
- Cloud by P.J. Onori
并感谢 Jeff Posnick 在我点击“发布”之前发现了许多令人震惊的错误。
延伸阅读
- Service Workers—简介
- Service Worker 准备好了吗?—跟踪主要浏览器中的实现状态
- JavaScript Promise—简介 - Promise 指南