So, for all these examples the assumed set up is a main.js
file that imports a cache.js
file. All the methods dealing with the cache(s) will be inside the cache.js
file and will return a
Promise
. Here is an example of a function inside the main.js
file that calls a function in cache.js
and receives a Promise in return. The code in main.js
uses a then()
to wait for the
completion of the code in cache.js
.
//main.js
import CACHE from './cache.js';
// from inside a method in main.js we will call a method in cache.js
const APP = {
cacheRef: null, //a variable to hold a reference to a cache.
someFunc(){
CACHE.open('my-cache-name')
.then(cache=>{
APP.cacheRef = cache; //save the reference in the variable
});
}
If you need to do multiple things in a row, like open a cache, then delete a file, then get a list of all the files, then build the HTML for that list, then you need to use a chain of .then()
methods with a return of the Promise method inside each then()
.
const APP = {
someFunc(filename) {
CACHE.open('my-cache-name')
.then((cache) => {
APP.cacheRef = cache; //save the reference to the cache
return CACHE.delete(filename);
})
.then(() => {
//now the delete is complete,
//get the list from the cache passed in
return CACHE.getFiles(APP.cacheRef);
})
.then((fileList) => {
//fileList is the array of files
//build the HTML
return APP.buildList(fileList);
})
.then(() => {
//HTML is built... need to update anything else?
})
.catch((err) => {
//handle an error from any step
});
},
};
If all we need to do is open a cache and keep a reference to that cache for later use...
const CACHE = {
open(cacheName) {
return caches.open(cacheName);
},
};
If you want a list of all the available caches...
const CACHE = {
listCaches() {
return caches.keys();
},
};
Then back in the main JS file we get an array that we can loop through.
//main.js
CACHE.listCaches().then((list) => {
//list is an array that you can loop through
});
If you need to delete one or more caches we can use Promise.all()
to delete an array of names. This is a less common thing to do. Typically, we do this in a Service Worker when we want to get rid of
old versions of caches.
We pass the name (or names) of cache(s) to keep to our function. It will return a Promise that lets us know when it is complete.
const CACHE = {
deleteCache(toKeep) {
return CACHE.listCaches().then((keys) => {
//keys is the array of all the cache names
return Promise.all(keys.filter((key) => key != toKeep).map((name) => name.delete()));
});
},
};
This function could be written in two ways. First, you can pass a reference to the cache that contains the files. Second, you could pass the name of the cache that holds the files
const CACHE = {
getFileList(cache) {
//pass in the cache where the files are located
//version 1 - by cache reference
return cache.keys();
},
getFileList(cacheName) {
//if you pass in the name of the cache instead, then you would call CACHE.open() first
//version 2 - by cache name
return CACHE.open(cacheName).then((cache) => {
return cache.keys();
});
},
};
Back in the main JS file we get an array of all the Request
objects from the cache. Each of those Request objects has a url
property which can be used to create a URL
object. From the URL
object you can extract any part of the url that you want - origin, hostname, pathname, port, protocol, query string, hash, etc.
//main.js
const APP = {
cacheRef: null,
init() {
CACHE.open('my-cache-name').then((cache) => {
APP.cacheRef = cache;
});
},
getFiles() {
CACHE.getFileList(APP.cacheRef).then((fileList) => {
//fileList is an array of each Request in the Cache
fileList.forEach((req) => {
let url = new URL(req.url);
console.log(url.pathname);
console.log(url.origin);
console.log(url.search);
console.log(url.port);
console.log(url.hash);
});
});
},
};
To delete a file from a cache you need to have the file name or a Request containing the file name, plus a way to reference the cache. To reference the cache you need a cache name or a variable holding the reference to the cache. For this example we will pass in the reference to the cache.
You could alternatively pass in a Request object or URL object instead of a filename.
const CACHE = {
deleteFile(filename, cache) {
return cache.delete(filename);
},
};
Your function to add a file to a cache could accept a filename, a URL, or a Request object. The best approach is to have a Request
object. This way you can define things like the HTTP method,
whether it is cors
or no-cors
request, and whether or not credentials should be included.
The file to be saved needs to be wrapped in a Response
object that also gets passed to the CACHE function.
//in main.js
let filename = crypto.randomUUID() + '.json';
let request = new Request(filename, {
method: 'get',
credentials: omit,
});
let data = JSON.stringify({ id: 123, some: 'info', type: 'object' });
let file = new File([data], filename, { type: 'application/json' });
let response = new Response(file, { status: 200, statusText: 'all good' });
CACHE.saveFile(APP.cacheRef, request, response)
.then(() => {
//this function runs after the file is saved.
})
.catch((err) => {
//handles failure to save file in cache.
});
The third thing that gets passed to the function is a reference to the cache.
//in cache.js
const CACHE = {
saveFile(cache, req, resp) {
return cache.put(req, resp);
},
};
When a cache saves files, it does so with key value pairs. The Request object is the key and the Response object is the value.
The Response object is a container that holds a File. Depending on the type of file there is a different response method that we have to use to get the contents.
Whether it is a Response
object coming from a Cache
or a Response
object coming from a fetch
, it is still just a Response
object.
In our cache function we need a filename but we can uses caches
instead of a reference to a single cache.
In the main Javascript file we will be given a Promise containing the contents of a file OR an Error.
//main.js
let filename = 'style.css';
CACHE.getText(filename)
.then((txt) => {
//txt is the text from the css file
})
.catch((err) => {
//Error about no match
});
To get the contents of a text file, which includes .txt
, .html
, or .css
, we use the .text()
method.
const CACHE = {
getText(filename) {
return caches.match(filename).then((cacheResponse) => {
//cacheResponse is the Response object or null
if (cacheResponse) return cacheResponse.text();
throw new Error('No match for text file in the cache');
});
},
};
To get an Object built from the contents of a .json
text file we would use the .json()
method on the Response object that is returned by the match()
method.
const CACHE = {
getJSON(filename) {
return caches.match(filename).then((cacheResponse) => {
//cacheResponse is the Response object or null
if (cacheResponse) return cacheResponse.json();
throw new Error('No match for json file in the cache');
});
},
};
When you want an image (or any media file) from a cache we will use the blob()
method.
The result from the blob()
method is actual just a reference to the location in memory where the binary data from inside the image file is saved.
const CACHE = {
getImage(filename) {
return caches.match(filename).then((cacheResponse) => {
//cacheResponse is the Response object or null
if (cacheResponse) return cacheResponse.blob();
throw new Error('No match for media file in the cache');
});
},
};
Then back in the main JavaScript file when we receive the blob reference variable, we need to use the URL.createObjectURL()
method which will create a URL that points to the location in memory. This
newly created URL can be used as the src
for any <img>
element.
//main.js
let filename = 'myavatar.png';
CACHE.getImage(filename)
.then((blobRef) => {
//blobRef is the pointer to the location in memory of the blob
let url = URL.createObjectURL(blobRef);
let img = document.getElementById('someImage');
img.src = url;
})
.catch((err) => {
//error about no file match in cache
});