我最近一直在尝试使用 ServiceWorker,所以当 Chris 让我写一篇关于它的文章时,我感到非常兴奋。ServiceWorker 是继 Ajax 之后最具影响力的现代 Web 技术。它是一个存在于浏览器内部的 API,位于您的网页和应用程序服务器之间。一旦安装并激活,ServiceWorker 就可以以编程方式确定如何响应来自您源的资源请求,即使浏览器处于离线状态也是如此。ServiceWorker 可用于支持所谓的“优先离线”网络。
ServiceWorker 是一项渐进式技术,在本文中,我将向您展示如何获取一个网站并使其可供使用现代浏览器的用户离线访问,同时不会影响使用不支持浏览器的用户。
这是一个无声的 26 秒视频,展示了支持浏览器(Chrome)离线后最终演示站点仍然可以工作的情况
如果您只想查看代码,我们为此构建了一个 简单离线站点 存储库。您可以查看整个 CodePen 项目,它甚至是一个 完整的演示网站。
浏览器支持
如今,ServiceWorker 在 Google Chrome、Opera 和 Firefox(在配置标志后面)中获得了浏览器支持。微软 可能会很快开始开发它。Apple 的 Safari 尚未发布官方消息。
Jake Archibald 有一个 页面跟踪所有 ServiceWorker 相关技术的支持情况。
此浏览器支持数据来自 Caniuse,其中包含更多详细信息。数字表示浏览器在该版本及更高版本中支持该功能。
桌面
Chrome | Firefox | IE | Edge | Safari |
---|---|---|---|---|
45 | 44 | 否 | 17 | 11.1 |
移动/平板电脑
Android Chrome | Android Firefox | Android | iOS Safari |
---|---|---|---|
127 | 127 | 127 | 11.3-11.4 |
鉴于您可以以渐进增强的方式实现这些功能(不会影响不支持的浏览器),这是一个领先于竞争对手的绝佳机会。那些得到支持的人将非常感谢它。
在开始之前,我应该提醒您注意一些需要考虑的事项。
仅限安全连接
您应该知道,在使用 ServiceWorker 时有一些硬性要求。首先也是最重要的是,**您的站点需要通过安全连接提供服务**。如果您仍然通过 HTTP 提供站点服务,这可能是实施 HTTPS 的一个好理由。

您可以使用 CloudFlare 等 CDN 代理安全地提供流量。请记住查找并修复混合内容警告,因为某些浏览器可能会警告您的客户您的网站不安全,否则。
- 我写了一个教程,可以帮助您 为您的站点设置 CloudFlare
- 您可能希望利用 LetsEncrypt.org 获取免费的 TLS 证书
虽然 HTTP/2 的规范本身并不强制执行加密连接,但浏览器打算仅通过 HTTPS 实施 HTTP/2 和类似技术。另一方面,ServiceWorker 规范 建议通过 HTTPS 实现浏览器。浏览器也暗示过将通过未加密连接提供的站点标记为不安全。搜索引擎会对未加密的结果进行处罚。
“仅限 HTTPS”是浏览器表达“这很重要,您应该这样做”的方式。
Promise
的 API
基于 Web 浏览器 API 实现的未来是 Promise
为主。例如,fetch
API 在 XMLHttpRequest
的基础上添加了基于 Promise
的语法糖。ServiceWorker 偶尔会使用 fetch
,但还有工作程序注册、缓存和消息传递,所有这些都是基于 Promise 的。
- 我写了一个教程,可以帮助您 开始使用 Promise
- 在我的博客上还有一个 用要点概括的 ES6 概述
- 我还有一个工具可以 帮助您可视化 Promise,如果您更喜欢视觉学习方式。

无论您是否喜欢 Promise,它们都会继续存在,因此您最好习惯它们。
注册您的第一个 ServiceWorker
我与 Chris 合作,对如何使用 ServiceWorker 做了尽可能简单的实用演示。他实现了一个简单的网站(静态 HTML、CSS、JavaScript 和图像),并让我添加离线支持。我觉得这是一个很好的机会来展示向现有网站添加离线功能是多么容易和不显眼。
如果您想跳到最后,请查看 演示站点在 GitHub 上的此提交。
第一步是注册 ServiceWorker。我们不会盲目地尝试注册,而是会检测 ServiceWorker 是否可用。
if ('serviceWorker' in navigator) {
}
以下代码片段演示了我们如何安装 ServiceWorker。传递给 .register
的 JavaScript 资源将在 ServiceWorker 的上下文中执行。请注意,注册返回一个 Promise
,以便您可以跟踪 ServiceWorker 注册是否成功。我在日志语句前面添加了 CLIENT
:,以便我更容易从视觉上区分日志语句是来自网页还是 ServiceWorker 脚本。
// ServiceWorker is a progressive technology. Ignore unsupported browsers
if ('serviceWorker' in navigator) {
console.log('CLIENT: service worker registration in progress.');
navigator.serviceWorker.register('/service-worker.js').then(function() {
console.log('CLIENT: service worker registration complete.');
}, function() {
console.log('CLIENT: service worker registration failure.');
});
} else {
console.log('CLIENT: service worker is not supported.');
}
到 service-worker.js
文件的端点非常重要。如果脚本是从例如 /js/service-worker.js
提供服务的,那么 ServiceWorker 只能拦截 /js/
上下文中的请求,但它对 /other
等资源一无所知。这通常是一个问题,因为您通常会在 /js/
、/public/
、/assets/
或类似的“目录”中限定您的 JavaScript 文件范围,而在大多数情况下,您希望从域根目录提供 ServiceWorker 脚本。
事实上,这是您需要对 Web 应用程序代码进行的唯一更改,前提是您已经实施了 HTTPS。此时,支持的浏览器将发出对 /service-worker.js
的请求,并尝试安装工作程序。
那么,您应该如何构建 service-worker.js
文件呢?
组装 ServiceWorker
ServiceWorker 是事件驱动的,**您的代码应该旨在是无状态的**。这是因为当 ServiceWorker 未被使用时,它会被关闭,从而丢失所有状态。您无法控制这一点,因此最好避免任何长期依赖内存中的状态。
下面,我列出了您必须在 ServiceWorker 中处理的最值得注意的事件。
- 当 ServiceWorker 首次获取时,会触发
install
事件。这是您有机会使用基本资源为 ServiceWorker 缓存做好准备的机会,即使用户离线时也应提供这些资源。 - 每当请求源自您的 ServiceWorker 范围时,都会触发
fetch
事件,并且您将有机会拦截请求并立即响应,而无需访问网络。 - 安装成功后,会触发
activate
事件。您可以使用它逐步淘汰旧版本的工作程序。我们将查看一个基本示例,在该示例中,我们删除了陈旧的缓存条目。
让我们回顾每个事件,并查看如何处理它们的示例。
安装您的 ServiceWorker
版本号在更新 Worker 逻辑时非常有用,它允许你在激活步骤期间移除过时的缓存条目,我们稍后会看到这一点。在创建缓存存储时,我们将使用以下版本号作为前缀。
var version = 'v1::';
你可以使用addEventListener
为install
事件注册一个事件处理程序。使用event.waitUntil
会在提供的p
Promise 上阻塞安装过程。如果 Promise 被拒绝(例如,由于某个资源下载失败),则服务工作者将不会被安装。在这里,你可以利用从使用caches.open(name)
打开缓存返回的 Promise,然后将其映射到cache.addAll(resources)
,这会下载并存储提供的资源的响应。
self.addEventListener("install", function(event) {
console.log('WORKER: install event in progress.');
event.waitUntil(
/* The caches built-in is a promise-based API that helps you cache responses,
as well as finding and deleting them.
*/
caches
/* You can open a cache by name, and this method returns a promise. We use
a versioned cache name here so that we can remove old cache entries in
one fell swoop later, when phasing out an older service worker.
*/
.open(version + 'fundamentals')
.then(function(cache) {
/* After the cache is opened, we can fill it with the offline fundamentals.
The method below will add all resources we've indicated to the cache,
after making HTTP requests for each of them.
*/
return cache.addAll([
'/',
'/css/global.css',
'/js/global.js'
]);
})
.then(function() {
console.log('WORKER: install completed');
})
);
});
一旦安装步骤成功,activate
事件就会触发。这有助于我们淘汰旧版本的 ServiceWorker,我们稍后会详细介绍。现在,让我们关注一下fetch
事件,它会更有趣一些。
拦截 Fetch 请求
无论何时由该服务工作者控制的页面请求资源,fetch
事件都会触发。这不仅限于fetch
或XMLHttpRequest
。相反,它甚至包含首次加载 HTML 页面的请求,以及 JS 和 CSS 资源、字体、任何图像等。还要注意,针对其他来源发出的请求也会被 ServiceWorker 的fetch
处理程序捕获。例如,针对i.imgur.com
(一个流行的图片托管网站的 CDN)发出的请求也会被我们的服务工作者捕获,只要该请求源自由工作者控制的客户端之一(例如浏览器标签页)。
就像install
一样,我们可以通过将 Promise 传递给event.respondWith(p)
来阻塞fetch
事件,当 Promise 完成时,工作者将使用它进行响应,而不是执行转到网络的默认操作。我们可以使用caches.match
查找缓存的响应,并返回这些响应,而不是转到网络。
如注释中所述,这里我们使用的是“最终新鲜”缓存模式,其中我们返回存储在缓存中的任何内容,但无论如何始终尝试再次从网络中获取资源,以保持缓存更新。如果我们提供给用户的响应已过期,则他们在下次请求该资源时将获得新的响应。如果网络请求失败,它将尝试通过尝试提供硬编码的Response
来恢复。
self.addEventListener("fetch", function(event) {
console.log('WORKER: fetch event in progress.');
/* We should only cache GET requests, and deal with the rest of method in the
client-side, by handling failed POST,PUT,PATCH,etc. requests.
*/
if (event.request.method !== 'GET') {
/* If we don't block the event as shown below, then the request will go to
the network as usual.
*/
console.log('WORKER: fetch event ignored.', event.request.method, event.request.url);
return;
}
/* Similar to event.waitUntil in that it blocks the fetch event on a promise.
Fulfillment result will be used as the response, and rejection will end in a
HTTP response indicating failure.
*/
event.respondWith(
caches
/* This method returns a promise that resolves to a cache entry matching
the request. Once the promise is settled, we can then provide a response
to the fetch request.
*/
.match(event.request)
.then(function(cached) {
/* Even if the response is in our cache, we go to the network as well.
This pattern is known for producing "eventually fresh" responses,
where we return cached responses immediately, and meanwhile pull
a network response and store that in the cache.
Read more:
https://ponyfoo.com/articles/progressive-networking-serviceworker
*/
var networked = fetch(event.request)
// We handle the network request with success and failure scenarios.
.then(fetchedFromNetwork, unableToResolve)
// We should catch errors on the fetchedFromNetwork handler as well.
.catch(unableToResolve);
/* We return the cached response immediately if there is one, and fall
back to waiting on the network as usual.
*/
console.log('WORKER: fetch event', cached ? '(cached)' : '(network)', event.request.url);
return cached || networked;
function fetchedFromNetwork(response) {
/* We copy the response before replying to the network request.
This is the response that will be stored on the ServiceWorker cache.
*/
var cacheCopy = response.clone();
console.log('WORKER: fetch response from network.', event.request.url);
caches
// We open a cache to store the response for this request.
.open(version + 'pages')
.then(function add(cache) {
/* We store the response for this request. It'll later become
available to caches.match(event.request) calls, when looking
for cached responses.
*/
cache.put(event.request, cacheCopy);
})
.then(function() {
console.log('WORKER: fetch response stored in cache.', event.request.url);
});
// Return the response so that the promise is settled in fulfillment.
return response;
}
/* When this method is called, it means we were unable to produce a response
from either the cache or the network. This is our opportunity to produce
a meaningful response even when all else fails. It's the last chance, so
you probably want to display a "Service Unavailable" view or a generic
error response.
*/
function unableToResolve () {
/* There's a couple of things we can do here.
- Test the Accept header and then return one of the `offlineFundamentals`
e.g: `return caches.match('/some/cached/image.png')`
- You should also consider the origin. It's easier to decide what
"unavailable" means for requests against your origins than for requests
against a third party, such as an ad provider
- Generate a Response programmaticaly, as shown below, and return that
*/
console.log('WORKER: fetch request failed in both cache and network.');
/* Here we're creating a response programmatically. The first parameter is the
response body, and the second one defines the options for the response.
*/
return new Response('<h1>Service Unavailable</h1>', {
status: 503,
statusText: 'Service Unavailable',
headers: new Headers({
'Content-Type': 'text/html'
})
});
}
})
);
});
还有几种其他的策略,其中一些我在我博客上关于 ServiceWorker 策略的文章中进行了讨论。
如承诺的那样,让我们看看你可以用来淘汰旧版本 ServiceWorker 脚本的代码。
淘汰旧版本的 ServiceWorker
activate
事件在服务工作者成功安装后触发。在淘汰旧版本的 ServiceWorker 时,它最有用,因为此时你知道新的工作者已正确安装。在此示例中,我们删除了与我们刚刚安装完成的工作者的version
不匹配的旧缓存。
self.addEventListener("activate", function(event) {
/* Just like with the install event, event.waitUntil blocks activate on a promise.
Activation will fail unless the promise is fulfilled.
*/
console.log('WORKER: activate event in progress.');
event.waitUntil(
caches
/* This method returns a promise which will resolve to an array of available
cache keys.
*/
.keys()
.then(function (keys) {
// We return a promise that settles when all outdated caches are deleted.
return Promise.all(
keys
.filter(function (key) {
// Filter by keys that don't start with the latest version prefix.
return !key.startsWith(version);
})
.map(function (key) {
/* Return a promise that's fulfilled
when each outdated cache is deleted.
*/
return caches.delete(key);
})
);
})
.then(function() {
console.log('WORKER: activate completed.');
})
);
});
提醒:我们为此构建了一个简单的离线站点存储库。你可以在CodePen 项目中查看整个内容,它甚至是一个完整的演示网站。
有点跑题,但这篇文章中的代码非常冗长,而且由于提供的代码视图宽度太窄,无法阅读。
至于这项技术本身?绝对棒极了。能够编写本质上是客户端代理服务器的东西非常有用。
我在阅读时也想到了同样的事情。我使用了 Stylish 浏览器插件隐藏了 Chris 的侧边栏 -
@namespace url(http://www.w3.org/1999/xhtml);
@-moz-document domain(“css-tricks.com”) {
.entry-unrelated { display: none !important; }
.blog-posts.grid-2-3 { width: 100% !important; }
}
@Neal 我喜欢这种 CAN DO 的态度!
嗨,Chris,
如何添加一个切换按钮,让我们能够将主要内容放大 100% 以查看类似这样的较大的代码块。开发者工具没问题,但如果将其作为网站功能的一部分会很棒。
CSS-Tricks 可以使用一些 CSS 样式在悬停时使代码块可扩展:https://www.youtube.com/watch?v=w79Uze8sTrU
@Šime:width: -moz-max-content! 太棒了!比魔法数字好多了。而且我想它只会如果需要变宽才会这样做。
当我将 Chrome 47.0.2526.49 beta(64 位)的网络节流设置为离线时,似乎无法让服务工作者提供离线站点。工作者已注册,并且在检查后,它似乎正在运行,但 Chrome 只是返回“无法连接到 Internet”的消息。
这是我得到的
但我偶尔看到过一些不一致的结果,以及在强制刷新而不是普通刷新时出现不同的结果。
嗨,Chris,
我也似乎无法使其工作。与 Johnny 遇到同样的问题。我在 Chrome 46.0.2490.86(64 位)和 Chrome 48.0.2563.0 canary(64 位)上尝试过。
我不确定还有什么其他细节需要提供,但请告诉我,我将很乐意提供更多信息。:)
谢谢!
嘿,对于它值不值得,这里的教程有效,只是演示站点无效。:)
Polymer 使这个用例变得非常容易,使用该元素。我大约花了 5 分钟时间就创建了一个 100% 离线的简单站点。
<platinum-sw-register>
元素*这大约是使用任何库在没有库的情况下实现 ServiceWorker 所需的时间,并且一旦你掌握了基础知识,你就可以在任何地方实现它(而不仅仅是使用 Polymer 的站点)。
Safari 缺乏“热情”是我唯一阻止我进一步尝试 Service Workers 的事情。
从 Apple 的角度来看,不通过允许轻松创建支持离线的 Web 应用来自讨苦吃是有道理的,当然,可能还有其他原因。这对 Web 开发人员来说确实很糟糕。
但是,即使 Internet Explorer 也正在考虑支持 Service Workers,现在可能是在没有 Safari 的情况下继续前进的时候了。