Cache Fetched AJAX Requests Locally: Wrapping the Fetch API
This article is by guest authorPeter Bengtsson. SitePoint guest posts aim to bring you engaging content from prominent writers and speakers of the JavaScript community More from this author
This article demonstrates how you implement a local cache of fetched requestsso that if done repeatedly it reads from session storage instead. The advantage of this is that you don’t need to have custom code for each resource you want cached. Follow along if you want to look really cool at your next JavaScript dinner party,where you can show off various skills of juggling promises,state-of-the-art APIs and local storage. The Fetch APIAt this point you’re hopefully familiar withfetch. It’s a new native API in browsers to replace the old Where it hasn’t been perfectly implemented in all browsers,you can useGitHub’s fetch polyfill(And if you have nothing to do all day,here’s theFetch Standard spec). The Na?ve AlternativeSuppose you know exactly which one resource you need to download and only want to download it once. You could use a global variable as your cache,something like this: let origin = null fetch('https://httpbin.org/get') .then(r => r.json()) .then(information => { origin = information.origin // your client's IP }) // need to delay to make sure the fetch has finished setTimeout(() => { console.log('Your origin is ' + origin) },3000) On CodePen That just relies on a global variable to hold the cached data. The immediate problem is that the cached data goes away if you reload the page or navigate to some new page. Let’s upgrade our first naive solution before we dissect its shortcomings. fetch('https://httpbin.org/get') .then(r => r.json()) .then(info => { sessionStorage.setItem('information',JSON.stringify(info)) }) // need to delay to make sure the fetch has finished setTimeout(() => { let info = JSON.parse(sessionStorage.getItem('information')) console.log('Your origin is ' + info.origin) },3000) On CodePen The first an immediate problem is that The second problem is that this solution is very specific to a particular URL and a particular piece of cached data (key First Implementation – Keeping It SimpleLet’s put a wrapper around So imagine youusedto do this: fetch('https://httpbin.org/get') .then(r => r.json()) .then(issues => { console.log('Your origin is ' + info.origin) }) On CodePen And now you want to wrap that,so that repeated network calls can benefit from a local cache. Let’s simply call it cachedFetch('https://httpbin.org/get') .then(r => r.json()) .then(info => { console.log('Your origin is ' + info.origin) }) The first time that’s run,it needs to resolve the request over the network and store the result in the cache. The second time it should draw directly from the local storage. Let’s start with the code that simply wraps the const cachedFetch = (url,options) => { return fetch(url,options) } On CodePen This works,but is useless,of course. Let’s implement thestoringof the fetched data to start with. const cachedFetch = (url,options) => { // Use the URL as the cache key to sessionStorage let cacheKey = url return fetch(url,options).then(response => { // let's only store in cache if the content-type is // JSON or something non-binary let ct = response.headers.get('Content-Type') if (ct && (ct.match(/application/json/i) || ct.match(/text//i))) { // There is a .json() instead of .text() but // we're going to store it in sessionStorage as // string anyway. // If we don't clone the response,it will be // consumed by the time it's returned. This // way we're being un-intrusive. response.clone().text().then(content => { sessionStorage.setItem(cacheKey,content) }) } return response }) } On CodePen There’s quite a lot going on here. The first promise returned by The most interesting feature is that we have toclonetheResponseobject returned by the first promise. If we don’t do that,we’re injecting ourselves too much and when the final user of the promise tries to call TypeError: Body has already been consumed. The other thing to notice is the carefulness around what the response type is: we only store the response if the status code is Here’s an example of using this: cachedFetch('https://httpbin.org/get') .then(r => r.json()) .then(info => { console.log('Your origin is ' + info.origin) }) cachedFetch('https://httpbin.org/html') .then(r => r.text()) .then(document => { console.log('Document has ' + document.match(/<p>/).length + ' paragraphs') }) cachedFetch('https://httpbin.org/image/png') .then(r => r.blob()) .then(image => { console.log('Image is ' + image.size + ' bytes') }) What’s neat about this solution so far is that it works,without interfering,for both JSONandHTML requests. And when it’s an image,it does not attempt to store that in Second Implementation – Actually Return Cache HitsSo our first implementation just takes care ofstoringthe responses of requests. But if you call the Let’s start with a very basic implementation: const cachedFetch = (url,options) => { // Use the URL as the cache key to sessionStorage let cacheKey = url // START new cache HIT code let cached = sessionStorage.getItem(cacheKey) if (cached !== null) { // it was in sessionStorage! Yay! let response = new Response(new Blob([cached])) return Promise.resolve(response) } // END new cache HIT code return fetch(url,options).then(response => { // let's only store in cache if the content-type is // JSON or something non-binary if (response.status === 200) { let ct = response.headers.get('Content-Type') if (ct && (ct.match(/application/json/i) || ct.match(/text//i))) { // There is a .json() instead of .text() but // we're going to store it in sessionStorage as // string anyway. // If we don't clone the response,it will be // consumed by the time it's returned. This // way we're being un-intrusive. response.clone().text().then(content => { sessionStorage.setItem(cacheKey,content) }) } } return response }) } On CodePen And it just works! To see it in action,openthe CodePen for this codeand once you’re there open your browser’s Network tab in the developer tools. Press the “Run” button (top-right-ish corner of CodePen) a couple of times and you should see that only the image is being repeatedly requested over the network. One thing that is neat about this solution is the lack of “callback spaghetti”. Since the Third Implementation – What About Expiry Times?So far we’ve been using A better solution is to give theusercontrol instead. (The user in this case is the web developer using our For example,in Python (with Flask) >>> from werkzeug.contrib.cache import MemcachedCache >>> cache = MemcachedCache(['127.0.0.1:11211']) >>> cache.set('key','value',10) True >>> cache.get('key') 'value' >>> # waiting 10 seconds ... >>> cache.get('key') >>> Now,neither But before we do that,how is this going to look? How about something like this: // Use a default expiry time,like 5 minutes cachedFetch('https://httpbin.org/get') .then(r => r.json()) .then(info => { console.log('Your origin is ' + info.origin) }) // Instead of passing options to `fetch` we pass an integer which is seconds cachedFetch('https://httpbin.org/get',2 * 60) // 2 min .then(r => r.json()) .then(info => { console.log('Your origin is ' + info.origin) }) // Combined with fetch's options object but called with a custom name let init = { mode: 'same-origin',seconds: 3 * 60 // 3 minutes } cachedFetch('https://httpbin.org/get',init) .then(r => r.json()) .then(info => { console.log('Your origin is ' + info.origin) }) The crucial new thing we’re going to add is that every time we save the response data,wealsorecordwhenwe stored it. But note that now we can also switch to the braver storage of So here’s our final working solution: const cachedFetch = (url,options) => { let expiry = 5 * 60 // 5 min default if (typeof options === 'number') { expiry = options options = undefined } else if (typeof options === 'object') { // I hope you didn't set it to 0 seconds expiry = options.seconds || expiry } // Use the URL as the cache key to sessionStorage let cacheKey = url let cached = localStorage.getItem(cacheKey) let whenCached = localStorage.getItem(cacheKey + ':ts') if (cached !== null && whenCached !== null) { // it was in sessionStorage! Yay! // Even though 'whenCached' is a string,this operation // works because the minus sign converts the // string to an integer and it will work. let age = (Date.now() - whenCached) / 1000 if (age < expiry) { let response = new Response(new Blob([cached])) return Promise.resolve(response) } else { // We need to clean up this old key localStorage.removeItem(cacheKey) localStorage.removeItem(cacheKey + ':ts') } } return fetch(url,it will be // consumed by the time it's returned. This // way we're being un-intrusive. response.clone().text().then(content => { localStorage.setItem(cacheKey,content) localStorage.setItem(cacheKey+':ts',Date.now()) }) } } return response }) } On CodePen Future Implementation – Better,Fancier,CoolerNot only are we avoiding hitting those web APIs excessively,the best part is that So how could we further improve our solution? Dealing with binary responsesOur implementation here doesn’t bother caching non-text things,like images,but there’s no reason it can’t. We would need a bit more code. In particular,we probably want to store more information about theBlob. Every response is a Blob basically. For text and JSON it’s just an array of strings. And the For the curious,to see an extension of our implementation that supports images,check outthis CodePen. Using hashed cache keysAnother potential improvement is to trade space for speed by hashing every URL,which was what we used as a key,to something much smaller. In the examples above we’ve been using just a handful of really small and neat URLs (e.g. A solution to this is to usethis neat algorithmwhich is known to be safe and fast: const hashstr = s => { let hash = 0; if (s.length == 0) return hash; for (let i = 0; i < s.length; i++) { let char = s.charCodeAt(i); hash = ((hash<<5)-hash)+char; hash = hash & hash; // Convert to 32bit integer } return hash; } If you like this,check outthis CodePen. If you inspect the storage in your web console you’ll see keys like ConclusionYou now have a working solution you can stick into your web apps,where perhaps you’re consuming a web API and you know the responses can be pretty well cached for your users. One last thing that might be a natural extension of this prototype is to take it beyond an article and into a real,concrete project,with tests and a (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |