“离线”如今是一个热门话题,尤其是在许多 Web 应用也希望作为移动应用运行的情况下。最初的离线助手 API,即 Application Cache API(也称为“appcache”),存在许多问题,其中许多可以在 Jake Archibald 的 Application Cache is a Douchebag 文章中找到。appcache 的问题包括
- 即使用户在线,文件也从缓存中提供。
- 没有动态性:appcache 文件只是一个要缓存的文件列表。
- 可以 缓存 .appcache 文件本身,这会导致更新问题。
- 其他注意事项.
如今,开发人员可以使用一个新的 API 来确保他们的 Web 应用正常工作:Service Worker API。Service Worker API 允许开发人员使用 JavaScript 管理哪些内容进入缓存以供离线使用。
介绍 Service Worker 食谱
为了向您介绍 Service Worker API,我们将使用 Mozilla 新的 Service Worker 食谱中的示例!食谱是 Service Worker 在现代 Web 应用中使用的实用示例的集合。我们将在这个由三部分组成的系列中介绍 Service Worker
- Service Worker 的离线食谱(今天的文章)
- 不仅仅是 appcache 的服务
- 向大众推送 Web 更新
当然,除了启用离线功能外,此 API 还具有其他优势,例如性能,但我希望首先介绍用于离线的基本 Service Worker 策略。
我们所说的“离线”是什么意思?
离线不仅意味着用户没有互联网连接,也可能意味着用户网络连接不稳定。本质上,“离线”意味着用户没有可靠的连接,我们都遇到过这种情况!
食谱:离线状态
离线状态食谱说明了如何使用 Service Worker 缓存已知的资产列表,然后通知用户他们现在可以离线使用该应用。应用本身非常简单:单击按钮时显示随机图像。让我们看看实现这一目标的相关组件。
Service Worker
我们将首先查看 service-worker.js
文件,以了解我们正在缓存什么。我们将缓存要显示的随机图像,以及显示页面和关键 JavaScript 资源,并将其存储在名为 dependencies-cache
的缓存中。
var CACHE_NAME = 'dependencies-cache';
// Files required to make this app work offline
var REQUIRED_FILES = [
'random-1.png',
'random-2.png',
'random-3.png',
'random-4.png',
'random-5.png',
'random-6.png',
'style.css',
'index.html',
'/', // Separate URL than index.html!
'index.js',
'app.js'
];
Service Worker 的 install
事件将打开缓存并使用 addAll
指示 Service Worker 缓存我们指定的文件。
self.addEventListener('install', function(event) {
// Perform install step: loading each required file into cache
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
// Add all offline dependencies to the cache
return cache.addAll(REQUIRED_FILES);
})
.then(function() {
// At this point everything has been cached
return self.skipWaiting();
})
);
});
Service Worker 的 fetch
事件会在页面发出的每个请求时触发。fetch
事件还允许您提供与实际请求不同的内容。但是,出于离线内容的目的,我们的 fetch
监听器将非常简单:如果文件已缓存,则从缓存中返回;否则,从服务器检索文件。
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// Cache hit - return the response from the cached version
if (response) {
return response;
}
// Not in cache - return the result from the live server
// `fetch` is essentially a "fallback"
return fetch(event.request);
}
)
);
});
此 service-worker.js
文件的最后一部分是 activate
事件监听器,我们立即声明 Service Worker,以便用户无需刷新页面即可激活 Service Worker。当先前版本的 Service Worker(如果有)已被替换,并且更新后的 Service Worker 开始控制范围时,会触发 activate
事件。
self.addEventListener('activate', function(event) {
// Calling claim() to force a "controllerchange" event on navigator.serviceWorker
event.waitUntil(self.clients.claim());
});
本质上,我们不希望用户需要刷新页面才能启动 Service Worker——我们希望 Service Worker 在初始页面加载时激活。
Service Worker 注册
创建了简单的 Service Worker 后,就可以注册 Service Worker 了。
// Register the ServiceWorker
navigator.serviceWorker.register('service-worker.js', {
scope: '.'
}).then(function(registration) {
// The service worker has been registered!
});
请记住,此食谱的目标是在需要时通知用户已缓存必要文件。为此,我们需要监听 Service Worker 的 state
。当 state
变为 activated
时,我们知道基本文件已缓存,我们的应用已准备好离线使用,我们可以通知用户。
// Listen for claiming of our ServiceWorker
navigator.serviceWorker.addEventListener('controllerchange', function(event) {
// Listen for changes in the state of our ServiceWorker
navigator.serviceWorker.controller.addEventListener('statechange', function() {
// If the ServiceWorker becomes "activated", let the user know they can go offline!
if (this.state === 'activated') {
// Show the "You may now use offline" notification
document.getElementById('offlineNotification').classList.remove('hidden');
}
});
});
测试注册并验证应用是否可以离线使用,只需使用此食谱即可!此食谱提供了一个按钮,通过更改图像的 src
属性来加载随机图像。
// This file is required to make the "app" work offline
document.querySelector('#randomButton').addEventListener('click', function() {
var image = document.querySelector('#logoImage');
var currentIndex = Number(image.src.match('random-([0-9])')[1]);
var newIndex = getRandomNumber();
// Ensure that we receive a different image than the current
while (newIndex === currentIndex) {
newIndex = getRandomNumber();
}
image.src = 'random-' + newIndex + '.png';
function getRandomNumber() {
return Math.floor(Math.random() * 6) + 1;
}
});
更改图像的 src
会触发对该图像的网络请求,但由于我们已通过 Service Worker 缓存了该图像,因此无需发出网络请求。
此食谱涵盖了最简单的离线情况:缓存所需的静态文件以供离线使用。
食谱:离线回退
此食谱遵循另一个简单的用例:通过 AJAX 获取页面,但如果请求失败,则使用另一个缓存的 HTML 资源(offline.html
)进行响应。
Service Worker
Service Worker 的 install
步骤获取 offline.html
文件并将其放入名为 offline
的缓存中。
self.addEventListener('install', function(event) {
// Put `offline.html` page into cache
var offlineRequest = new Request('offline.html');
event.waitUntil(
fetch(offlineRequest).then(function(response) {
return caches.open('offline').then(function(cache) {
return cache.put(offlineRequest, response);
});
})
);
});
如果该请求失败,Service Worker 不会注册,因为 Promise 被拒绝。
fetch
监听器侦听对页面的请求,并在失败时使用我们在事件注册期间缓存的 offline.html
文件进行响应。
self.addEventListener('fetch', function(event) {
// Only fall back for HTML documents.
var request = event.request;
// && request.headers.get('accept').includes('text/html')
if (request.method === 'GET') {
// `fetch()` will use the cache when possible, to this examples
// depends on cache-busting URL parameter to avoid the cache.
event.respondWith(
fetch(request).catch(function(error) {
// `fetch()` throws an exception when the server is unreachable but not
// for valid HTTP responses, even `4xx` or `5xx` range.
return caches.open('offline').then(function(cache) {
return cache.match('offline.html');
});
})
);
}
// Any other handlers come here. Without calls to `event.respondWith()` the
// request will be handled without the ServiceWorker.
});
请注意,我们使用 catch
来检测请求是否失败,因此我们应该使用 offline.html
内容进行响应。
Service Worker 注册
Service Worker 只需注册一次。此示例演示了如何通过检查 navigator.serviceWorker.controller
属性的存在来绕过注册;如果 controller
属性不存在,我们将继续注册 Service Worker。
if (navigator.serviceWorker.controller) {
// A ServiceWorker controls the site on load and therefor can handle offline
// fallbacks.
console.log('DEBUG: serviceWorker.controller is truthy');
debug(navigator.serviceWorker.controller.scriptURL + ' (onload)', 'controller');
}
else {
// Register the ServiceWorker
console.log('DEBUG: serviceWorker.controller is falsy');
navigator.serviceWorker.register('service-worker.js', {
scope: './'
}).then(function(reg) {
debug(reg.scope, 'register');
});
}
确认 Service Worker 已注册后,您可以通过单击“刷新”链接测试此食谱(并触发新的页面请求):(然后使用缓存清除参数触发页面刷新)
// The refresh link needs a cache-busting URL parameter
document.querySelector('#refresh').search = Date.now();
为用户提供离线消息,而不是允许浏览器显示其自身(有时很丑陋)的消息,是与用户保持对话的一种极佳方式,告知他们应用在离线时不可用!
离线!
Service Worker 已将离线体验和控制带入了一个强大的新领域。今天,您可以在 Chrome 和 Firefox 开发者版 中使用 Service Worker API。许多网站现在都在使用 Service Worker,您可以通过在 Firefox 开发者版中访问 about:serviceworkers
来自行查看;您将看到已访问网站安装的 Service Worker 列表!
Service Worker 食谱充满了优秀的实用食谱,并且我们还会继续添加更多食谱。请关注本系列的下一篇文章,不仅仅是 appcache 的服务,您将在其中了解如何将 Service Worker API 用于不仅仅是离线目的。
关于 David Walsh
David Walsh 是 Mozilla 的 Web 开发人员,主要从事 Mozilla 开发者网络的工作。他还是一位会议演讲者、博主和 Web 爱好者。
2 条评论