Service Worker 是一种在 Web 平台上运行的脚本,它能够拦截和处理网络请求、管理应用程序的缓存、实现离线访问等功能。使用 service worker,你可以将 app 设置为首先使用缓存资源,从而即使在离线状态,也可以提供默认的体验,然后从网络获取更多数据(通常称为“离线优先”)。这已经在原生 app 中可用,这是经常选择原生 app,而不是选择 web app 的主要原因之一。
if 代码块进行特性检测测试,以确保在尝试注册 service worker 之前,该特性是被支持的。接着,我们使用 ServiceWorkerContainer.register() 函数来注册站点的 service worker。service worker 代码只是一个驻留在我们的 app 内的一个 JavaScript 文件(注意,这个文件的 URL 是相对于源(origin)的,而不是相对于引用它的那个 JS 文件)。scope 参数是可选的,并且可以用来指定你想要 service worker 控制的子作用域。在这个例子中,我们指定了 ‘/’,其表示 app 的源(origin)下的所有内容。如果你留空的话,它的默认值也是这个,但是我们在这里指定它是为了更明确的阐述我们的目的。
注意只能用 localhost 或者 127.0.0.1 以及 https 才能使用 service work
const registerServiceWorker = async () => {
if ("serviceWorker" in navigator) {
try {
const registration = await navigator.serviceWorker.register("/sw.js", {
scope: "/",
});
if (registration.installing) {
console.log("正在安装 Service worker");
} else if (registration.waiting) {
console.log("已安装 Service worker installed");
} else if (registration.active) {
console.log("激活 Service worker");
}
} catch (error) {
console.error(`注册失败:${error}`);
}
}
};
registerServiceWorker();
这里我们新增了一个 install 事件监听器去监听 service worker(这里指的是 self),接着在事件上调用 ExtendableEvent.waitUntil() 方法——这会确保 Service Worker 不会在 waitUntil() 里面的代码执行完毕之前安装完成。
在 addResourcesToCache() 内,我们使用了 caches.open() 方法来创建了叫做 v1 的新缓存,这将会是我们的站点资源缓存的第 1 个版本。然后我们会在创建的缓存示例中调用 addAll() 函数,它的参数采用一个 URL 数组,指向你想要缓存的所有资源。其中,URL 是相对于 worker 的 location (en-US)。
如果 promise 被拒绝,安装就会失败,这个 worker 不会做任何事情。这也是可以的,因为你可以修复你的代码,在下次注册的时候再次进行尝试。
当安装成功完成之后,service worker 就会激活。在你的 service worker 第一次完成安装/激活时,这并没有什么用。但是当 service worker 更新(稍后查看更新你的 service worker 部分)的时候,就不太一样了。
const addResourcesToCache = async (resources) => {
const cache = await caches.open('v1');
await cache.addAll(resources);
};
self.addEventListener('install', (event) => {
event.waitUntil(
addResourcesToCache(['/', '/index.html', '/style.css', '/app.js'])
);
});
在我们的 install callback 中,我们需要执行以下步骤:
当 service worker 被安装成功并且用户浏览了另一个页面或者刷新了当前的页面,service worker 将开始接收到 fetch 事件。
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
// Cache hit - return response
if (response) {
return response;
}
return fetch(event.request);
})
);
});
上面的代码里我们定义了 fetch 事件,在 event.respondWith() 里,我们传入了一个由 caches.match 产生的 promise.caches.match 查找 reques t 中被 service worker 缓存命中的 response。
如果我们有一个命中的 response,我们返回被缓存的值,否则我们返回一个实时从网络请求 fetch 的结果。这是一个非常简单的例子,使用所有在 install 步骤下被缓存的资源。
如果我们想要增量地缓存新的请求,我们可以通过处理 fetch 请求的 response 并且添加它们到缓存中来实现,例如:
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// 缓存命中 - 返回响应
if (response) {
return response;
}
// 重要:克隆请求。
// 请求是一个流,只能被使用一次。
// 由于我们通过缓存消耗了一次,而浏览器获取用了一次,我们需要克隆响应。
var fetchRequest = event.request.clone();
return fetch(fetchRequest).then(
function(response) {
// 检查是否收到有效的 response
if(!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 重要提示:克隆响应。
// 响应是一个流,因为我们希望浏览器使用响应以及消耗响应的缓存,我们需要克隆它以便我们有两个流
var responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});
代码里我们所做事情包括:
添加一个 callback 到 fetch 请求的 .then 方法中
保证 response 的类型是 basic,这表示请求本身是同源的,非同源(即跨域)的请求也不能被缓存。
如果我们通过了检查,clone 这个请求。这么做的原因是如果 response 是一个 Stream,那么它的 body 只能被读取一次,所以我们得将它克隆出来,一份发给浏览器,一份发给缓存。
你的 service worker 总有需要更新的那一天。当那一天到来的时候,你需要按照如下步骤来更新:
更新你的 service worker 的 JavaScript 文件。当用户浏览你的网站,浏览器尝试在后台下载 service worker 的脚本文件。只要服务器上的文件和本地文件有一个字节不同,它们就被判定为需要更新。
更新后的 service worker 将开始运作,install event 被重新触发。
在这个时间节点上,当前页面生效的依然是老版本的 service worker,新的 servicer worker 将进入”waiting”状态。
当前页面被关闭之后,老的 service worker 进程被杀死,新的 servicer worker 正式生效。
一旦新的 service worker 生效,它的 activate 事件被触发。
代码更新后,通常需要在 activate 的 callback 中执行一个管理 cache 的操作。因为你会需要清除掉之前旧的数据。我们在 activate 而不是 install 的时候执行这个操作是因为如果我们在 install 的时候立马执行它,那么依然在运行的旧版本的数据就坏了。
之前我们只使用了一个缓存,叫做 my-site-cache-v1,其实我们也可以使用多个缓存的,例如一个给页面使用,一个给 blog 的内容提交使用。这意味着,在 install 步骤里,我们可以创建两个缓存,pages-cache-v1 和 blog-posts-cache-v1,在 activite 步骤里,我们可以删除旧的 my-site-cache-v1。
下面的代码能够循环所有的缓存,删除掉所有不在白名单中的缓存。
self.addEventListener('activate', function(event) {
var cacheWhitelist = ['pages-cache-v1', 'blog-posts-cache-v1'];
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
});