7.使用service workers来缓存天气预报数据
为你的数据选择一个正确的缓存策略是非常重要的,其也取决于你app将要展示的数据类型。例如,时间敏感型数据像天气或者股票应该尽可能的新,同时头像图片或者文章内容更新频率就可以不那么高。
对我们的app来说,先访问缓存再请求网络策略是很理想的。它能够尽快的获取数据并呈现到屏幕,然后更新那些一旦网络返回的最新数据。与网络优先然后再访问缓存的策略相比,用户不需要等到fetch请求超时才拿得到缓存的数据。
缓存优先再请求网络的策略意味着我们搞定两个异步请求,一个是缓存一个是网络。我们的网络请求不需要太多改变,但是我们需要去修改service worker来响应返回前先缓存响应。
在普通情况下,缓存的数据会几乎立刻返回给app通过最新的数据给它用。然后,当网络请求返回,app将使用从网络获取的最新数据来更新。
拦截网络请求并缓存响应
我们需要修改service worker来拦截请求天气API并它们的响应到缓存中,因此我们可以在之后轻松的访问它们。在先缓存再网络的策略中,我们希望网络响应是数据源,总是提供给我们最新的信息。如果不能够做到,失败也没事,因为我们早已在我们的app中去获取了最新的缓存数据。
在service worker中,让我们添加一个dataCacheName让我们能够区分应用数据和应用壳。当应用壳更新了并且老的缓存也更新了,我们的数据将保持纯净,准备好一次超级快速的加载。记住,如果你的数据格式在将来改变了,你将需要一个方法来处理它并确保应用壳和内容保持同步。
添加如下行到你的service-worker.js文件的头部:
var dataCacheName = 'weatherData-v1';
接下来,更新activate事件处理器,以便它不会在当它清除应用壳缓存的时候删除数据缓存。
if (key !== cacheName && key !== dataCacheName) {
最后,更新fetch事件处理器,来处理将对数据API的请求从其他请求区分开。
self.addEventListener('fetch', function(e) {
console.log('[Service Worker] Fetch', e.request.url);
var dataUrl = 'https://query.yahooapis.com/v1/public/yql';
if (e.request.url.indexOf(dataUrl) > -1) {
/*
* When the request URL contains dataUrl, the app is asking for fresh
* weather data. In this case, the service worker always goes to the
* network and then caches the response. This is called the "Cache then
* network" strategy:
* https://jakearchibald.com/2014/offline-cookbook/#cache-then-network
*/
e.respondWith(
caches.open(dataCacheName).then(function(cache) {
return fetch(e.request).then(function(response){
cache.put(e.request.url, response.clone());
return response;
});
})
);
} else {
/*
* The app is asking for app shell files. In this scenario the app uses the
* "Cache, falling back to the network" offline strategy:
* https://jakearchibald.com/2014/offline-cookbook/#cache-falling-back-to-network
*/
e.respondWith(
caches.match(e.request).then(function(response) {
return response || fetch(e.request);
})
);
}
});
以上代码拦截请求并检查是否URL以天气API地址开头。如果是,我们将使用fetch来请求。一旦响应返回,我们的代码打开缓存,克隆响应,并存储到缓存中,并且最终返回响应到原始的请求者那里。
我们的app尚不会在离线下工作。我们将为应用壳实现缓存并从缓存中去取数据,但是即使我们缓存数据,app目前也不会去检查缓存中是否有任何天气数据。
制造请求
正如之前提到过的,app需要解决两个异步请求,一个是缓存一个是网络。app使用window下的caches对象来缓存读取最新的数据。对于渐进增强来说,这是一个很好的例子,因为caches对象可能并不会再所有浏览器总都支持,如果不支持,网络请求也应该继续工作。
为了做到这点,我们需要:
- 检查在全局window对象下的caches对象是否可用;
从缓存中拿取请求数据;
如果网络请求仍然没有完成,就使用缓存的数据来更新app
- 从服务器拿取请求数据;
为之后的快速访问存储数据
- 以服务器获取的新数据来更新app
从缓存中获取数据
接下来,我们需要检查caches对象是否存在并从中请求最新的数据。在app.getForecast()方法中找到TODO add cache logic here注释。并且在注释下添加如下代码。
if ('caches' in window) {
/*
* Check if the service worker has already cached this city's weather
* data. If the service worker has the data, then display the cached
* data while the app fetches the latest data.
*/
caches.match(url).then(function(response) {
if (response) {
response.json().then(function updateFromCache(json) {
var results = json.query.results;
results.key = key;
results.label = label;
results.created = json.query.created;
app.updateForecastCard(results);
});
}
});
}
我们的天气app现在可以制造两个异步请求来获取数据,一个是从缓存一个是通过XHR。如果在缓存中有数据,他会先返回并极其迅速的(几十毫秒)渲染更新天气卡片直到XHR请求完成。当XHR响应,卡片将以最新的从天气API请求回来的数据更新。
注意缓存请求和XHR请求两者是如果以一个结束调用来更新天气卡片的。以及app是如何直到它是否是展示的最新的数据?这些是从app.updateForecastCard中的如下代码处理的:
var cardLastUpdatedElem = card.querySelector('.card-last-updated');
var cardLastUpdated = cardLastUpdatedElem.textContent;
if (cardLastUpdated) {
cardLastUpdated = new Date(cardLastUpdated);
// Bail if the card has more recent data then the data
if (dataLastUpdated.getTime() < cardLastUpdated.getTime()) {
return;
}
}
一个卡片每一次更新,应用都会在卡片的一个隐藏属性上存储数据的时间戳。app只需检查是否卡片上存在的时间戳比传入函数的数据更新。
测试一下
现在app应该完全拥有了离线功能。存储一些城市然后在app上按下刷新按钮来获取新的天气数据,然后离线并重载页面。
然后去到DevTools中的Application面板下的Cache Storage栏。打开它,你应该看到你的应用壳的名字并且在左手边罗列着所有被缓存的数据。打开数据缓存,应该看到缓存的每个城市数据。