首页 > 代码库 > 使用 Service Workers 来预缓存应用外壳
使用 Service Workers 来预缓存应用外壳
Progressive Web Apps 是快速且可安装的,这意味着它能在在线、离线、断断续续或者缓慢的网络环境下使用。为了实现这个目标,我们需要使用一个 service worker 来缓存应用外壳,以保证它能始终迅速可用且可靠。
如果你对 service workers 不熟悉,你可以通过阅读 介绍 Service Workers 来了解关于它能做什么,它的生命周期是如何工作的等等知识。
service workers 提供的是一种应该被理解为渐进增强的特性,这些特性仅仅作用于支持service workers 的浏览器。比如,使用 service workers 你可以缓存应用外壳和你的应用所需的数据,所以这些数据在离线的环境下依然可以获得。如果浏览器不支持 service workers ,支持离线的 代码没有工作,用户也能得到一个基本的用户体验。使用特性检测来渐渐增强有一些小的开销,它不会在老旧的不支持 service workers 的浏览器中产生破坏性影响。
注册 service worker
为了让应用离线工作,要做的第一件事是注册一个 service worker,一段允许在后台运行的脚本,不需要 用户打开 web 页面,也不需要其他交互。
这只需要简单两步:
- 创建一个 JavaScript 文件作为 service worker
- 告诉浏览器注册这个 JavaScript 文件为 service worker
第一步,在你的应用根目录下创建一个空文件叫做 service-worker.js
。这个 service-worker.js
文件必须放在跟目录,因为 service workers 的作用范围是根据其在目录结构中的位置决定的。
接下来,我们需要检查浏览器是否支持 service workers,如果支持,就注册 service worker,将下面代码添加至app.js
中。
if(‘serviceWorker‘ in navigator) {
navigator.serviceWorker
.register(‘/service-worker.js‘)
.then(function() { console.log(‘Service Worker Registered‘); });
}
缓存站点的资源
当 service worker 被注册以后,当用户首次访问页面的时候一个 install
事件会被触发。在这个事件的回调函数中,我们能够缓存所有的应用需要再次用到的资源。
当 service worker 被激活后,它应该打开缓存对象并将应用外壳需要的资源存储进去。将下面这些代码加入你的service-worker.js
(你可以在your-first-pwapp-master/work
中找到) :
var cacheName = ‘weatherPWA-step-6-1‘;
var filesToCache = [];
self.addEventListener(‘install‘, function(e) {
console.log(‘[ServiceWorker] Install‘);
e.waitUntil(
caches.open(cacheName).then(function(cache) {
console.log(‘[ServiceWorker] Caching app shell‘);
return cache.addAll(filesToCache);
})
);
});
首先,我们需要提供一个缓存的名字并利用 caches.open()
打开 cache 对象。提供的缓存名允许我们给 缓存的文件添加版本,或者将数据分开,以至于我们能够轻松地升级数据而不影响其他的缓存。
一旦缓存被打开,我们可以调用 cache.addAll()
并传入一个 url 列表,然后加载这些资源并将响应添加至缓存。不幸的是 cache.addAll()
是原子操作,如果某个文件缓存失败了,那么整个缓存就会失败!
好的。让我们开始熟悉如何使用DevTools并学习如何使用DevTools来调试service workers。在刷新你的网页前,开启DevTools,从 Application 的面板中打开 Service Worker 的窗格。它应该是这样的:
当你看到这样的空白页,这意味着当前打开的页面没有已经被注册的Service Worker。
现在,重新加载页面。Service Worker的窗格应该是这样的:
当你看到这样的信息,这意味着页面有个Service Worker正在运行。
现在让我们来示范你在使用Service Worker时可能会遇到的问题。为了演示, 我们将把service-worker.js里的install 的事件监听器的下面添加在activate 的事件监听器。
self.addEventListener(‘activate‘, function(e) {
console.log(‘[ServiceWorker] Activate‘);
});
当 service worker 开始启动时,这将会发射activate事件。
打开DevTools并刷新网页,切换到应用程序面板的Service Worker窗格,在已被激活的Service Worker中单击inspect。理论上,控制台将会出现[ServiceWorker] Activate的信息,但这并没有发生。现在回去Service Worker窗格,你会发现到新的Service Worker是在“等待”状态。
简单来说,旧的Service Worker将会继续控制该网页直到标签被关闭。因此,你可以关闭再重新打开该网页或者点击 skipWaiting 的按钮,但一个长期的解决方案是在DevTools中的Service Worker窗格启用 Update on Reload 。当那个复选框被选择后,当每次页面重新加载,Service Worker将会强制更新
启用 update on reload 复选框并重新加载页面以确认新的Service Worker被激活。
Note: 您可能会在应用程序面板里的Service Worker窗格中看到类似于下面的错误信息,但你可以放心的忽略那个错误信息。
Ok, 现在让我们来完成activate 的事件处理函数的代码以更新缓存。
self.addEventListener(‘activate‘, function(e) {
console.log(‘[ServiceWorker] Activate‘);
e.waitUntil(
caches.keys().then(function(keyList) {
return Promise.all(keyList.map(function(key) {
console.log(‘[ServiceWorker] Removing old cache‘, key);
if (key !== cacheName) {
return caches.delete(key);
}
}));
})
);
});
确保在每次修改了 service worker 后修改 cacheName,这能确保你永远能够从缓存中获得到最新版本的文件。过一段时间清理一下缓存删除掉没用的数据也是很重要的。
最后,让我们更新一下 app shell 需要的缓存的文件列表。在这个数组中,我们需要包括所有我们的应用需要的文件,其中包括图片、JavaScript以及样式表等等。
var filesToCache = [
‘/‘,
‘/index.html‘,
‘/scripts/app.js‘,
‘/styles/inline.css‘,
‘/images/clear.png‘,
‘/images/cloudy-scattered-showers.png‘,
‘/images/cloudy.png‘,
‘/images/fog.png‘,
‘/images/ic_add_white_24px.svg‘,
‘/images/ic_refresh_white_24px.svg‘,
‘/images/partly-cloudy.png‘,
‘/images/rain.png‘,
‘/images/scattered-showers.png‘,
‘/images/sleet.png‘,
‘/images/snow.png‘,
‘/images/thunderstorm.png‘,
‘/images/wind.png‘
];
我么的应用目前还不能离线工作。我们缓存了 app shell 的组件,但是我们仍然需要从本地缓存中加载它们。
从缓存中加载 app sheel
Service workers 可以截获 Progressive Web App 发起的请求并从缓存中返回响应。这意味着我们能够 决定如何来处理这些请求,以及决定哪些网络响应能够成为我们的缓存。
比如:
self.addEventListener(‘fetch‘, function(event) {
// Do something interesting with the fetch here
});
让我们来从缓存中加载 app shell。将下面代码加入 service-worker.js 中:
self.addEventListener(‘fetch‘, function(e) {
console.log(‘[ServiceWorker] Fetch‘, e.request.url);
e.respondWith(
caches.match(e.request).then(function(response) {
return response || fetch(e.request);
})
);
});
从内至外,caches.match() 从网络请求触发的 fetch 事件中得到请求内容,并判断请求的资源是 否存在于缓存中。然后以缓存中的内容作为响应,或者使用 fetch 函数来加载资源(如果缓存中没有该资源)。 response 最后通过 e.respondWith() 返回给 web 页面。
测试
你的应用程序现在可以在离线下使用了! 让我们来试试吧!
先刷新那个网页, 然后去DevTools里的 Cache Storage 窗格中的 Application 面板上。展开该部分,你应该会在左边看到您的app shell缓存的名称。当你点击你的appshell缓存,你将会看到所有已经被缓存的资源。
现在,让我们测试离线模式。回去DevTools中的 Service Worker 窗格,启用 Offline 的复选框。启用之后,你将会在 Network 窗格的旁边看到一个黄色的警告图标。这表示您处于离线状态。
刷新网页,然后你会发现你的网页仍然可以正常操作!
下一步骤是修改该应用程序和service worker的逻辑,让气象数据能够被缓存,并能在应用程序处于离线状态,将最新的缓存数据显示出来。
Tip: 如果你要清除所有保存的数据(localStoarge,IndexedDB的数据,缓存文件),并删除任何的service worker,你可以在DevTools中的Application 面板里的Clear storage清除。
当心边缘问题
之前提到过,这段代码 一定不要用在生产环境下 ,因为有很多没有处理的边界情况。
缓存依赖于每次修改内容后更新缓存名称
比如缓存方法需要你在每次改变内容后更新缓存的名字。否则,缓存不会被更新,旧的内容会一直被缓存返回。 所以,请确保每次修改你的项目后更新缓存名称。
每次修改后所有资源都需要被重新下载
另一个缺点是当一个文件被修改后,整个缓存都需要被重新下载。这意味着即使你修改了一个简单的拼写错误 也会让整个缓存重新下载。这不太高效。
浏览器的缓存可能阻碍 service worker 的缓存的更新
另外一个重要的警告。首次安装时请求的资源是直接经由 HTTPS 的,这个时候浏览器不会返回缓存的资源, 除此之外,浏览器可能返回旧的缓存资源,这导致 service worker 的缓存不会得到 更新。
在生产环境中当下 cache-first 策略
我们的应用使用了优先缓存的策略,这导致所有后续请求都会从缓存中返回而不询问网络。优先缓存的策略是 很容易实现的,但也会为未来带来诸多挑战。一旦主页和注册的 service worker 被缓存下来,将会很难 去修改 service worker 的配置(因为配置依赖于它的位置),你会发现你部署的站点很难被升级。
我该如何避免这些边缘问题
我们该如何避免这些边缘问题呢? 使用一个库,比如 sw-precache, 它对资源何时过期提供了 精细的控制,能够确保请求直接经由网络,并且帮你处理了所有棘手的问题。
实时测试 service workers 提示
调试 service workers 是一件有调整性的事情,当涉及到缓存后,当你期望缓存更新,但实际上它并没有的时候,事情更是变得像一场恶梦。在 service worker 典型的生命周期和你的代码之间,你很快就会受挫。但幸运的是,这里有一些工具可以让你的生活更加简单。
其他的提示:
一旦 service worker 被注销(unregistered)。它会继续作用直到浏览器关闭。
如果你的应用打开了多个窗口,新的 service worker 不会工作,直到所有的窗口都进行了刷新,使用了 新的 service worker。
注销一个 service worker 不会清空缓存,所以如果缓存名没有修改,你可能继续获得到旧的数据。
如果一个 service worker 已经存在,而且另外一个新的 service worker 已经注册了,这个新的 service worker 不会接管控制权,知道该页面重新刷新后,除非你使用立刻控制的方式。
使用 Service Workers 来预缓存应用外壳