A young developer, new to the Tao of the client-side, comes to a Master of the way, and speaks thusly: “Oh Master, our application nears completion; and lo, cat pics can be drawn upon, and captions fixated thereto, for the creation of humour and the bounteous enjoyment of our users.”
“This is good,” responded the Master.
“But now, I and the acolyte of the server-side have come unto a disagreement, for I wish to render the image to be saved on the client, and by means cunning, reflect the image from the server back unto the grateful user when downloading is desired; but he who develops the server says this is wasteful, and bids me send the minimum data to him, so that the image can be rendered on the server, and then served to the user – but to me this is folly, for do we not then have dependencies in the server on image libraries which we could otherwise do without? And so we are at an impasse.”
The Master shook his head at the folly of youth. “You may tell the acolyte of the server that his services shall not be needed in this instance,” he told the young developer. “The shortest journey is one that ends where it begins.”
And the young developer was enlightened.
In times past, offering content generated in the browser for download by the user was a difficult proposition – one needed to rely on plugins like Flash (boo! hiss!), or on sending the content up to the server to be sent back to the user with the appropriate headers to trigger a download, incurring the cost of the bandwidth up and down in the process.
It’s still not a straightforward prospect if you need to support older browsers – even IE 11 will require you to dip your toes into the shark-infested pool of browser-specific extensions like msSaveBlob; and then there’s aborted efforts like the FileSystem API.
However, if you’ve managed to make a reliance on only modern/evergreen browsers stick for your project, a couple of HTML5 APIs have your content downloading needs covered – the Download Attribute and Blob URLs.
Blob URLs
So what are Blob URLs? In short, they’re a way of referencing a Blob in the browser that allows us to interact with it like it was a remote file reference – for instance, we can assign it as the value of the src
attribute of an img
tag.
Of course, Blob URLs are only useful if you can use Blobs, so you can implicitly expect any browser supporting the former to support the latter as well. This make getting a blob url a three part process, each of which we’ll be discussing today:
- Get the blob;
- Create the object url for the blob;
- After we’re finished with the url, revoke it.
In our scenario above, getting a Blob is trivial – the HTMLCanvasElement interface offers us a convenient toBlob function that we can call asynchronously to get the contents of the canvas as a Blob.
Let’s say we had a bunch of comma-seperated values that we wanted to give to the user instead, however. In that case, we can just construct a new Blob from the string, like so:
// Assuming we have our CSV in a string variable named 'csv'.
var blob = new Blob(csv, {type: 'text/csv'});
Easy, right? We could also do this with arbitrary binary data, passing in a typed array:
var blob = new Blob([the_typed_array], {type: 'application/octet-stream'});
This could be generating save files for games, user-specific executables, etc.
Now that we have our blob one way or the other, creating the object URL is just as easy:
var blobURL = URL.createObjectURL(blob);
On the topic of point 3, above, we could just rely on the garbage collection of the browser – the lifetime of our object urls are tied to the document. However, it’s good practice to revoke the urls yourself – otherwise blobs that could otherwise be garbage collected may stick around, unecessarily consuming memory. Also keep in mind that each object url is unique – if you call createObjectURL
against the same blob three times, you get three different URLs you’ll need to revoke separately.
On the other hand, if you’re using the object url as, for example, the src
of an img
, you don’t want to revoke said URL until you remove the img from the DOM, otherwise you’ll end up with a broken img tag.
URL.revokeObjectURL(blobURL);
Anchor Download Attribute
For a while, it seemed like browser vendors were set to give us a more comprehensive access to the file system, or some sandboxed portion thereof. That withered on the vine, but it didn’t take with it the need to sometimes download generated content – and so we have the anchor download attribute. It’s actually a pleasantly intuitive overloading of the anchor element – classically, we might point to a file on a server with the anchor element, expecting that the user would click on it and thereby trigger the download – assuming the server sent the appropriate headers.
In this case, we can indicate that we want the client to download whatever the anchor is pointing to, even if the content-disposition
headers we would normally require aren’t sent, and we can specify a default name for the resulting file – and combining that with object urls lets us inform the browser that we want it to download the client-side Blob that the anchor is pointing to.
So, assuming we have a blobURL
from the process above:
var anchor = document.createElement('a');
anchor.href = blobURL;
anchor.download = 'name_of_file.ext';
If we wanted to show this anchor to the user for them to click on, we could simply add it wherever appropriate. If, however, we wanted to trigger a download automatically, this too is fairly straightforward:
anchor.style.display = "none";
document.body.appendChild(anchor);
anchor.click();
Note that we still need to add the anchor to the body. Because this triggers the blocking download logic in the browser, we can then add a little further logic in a timeout to remove the anchor (and, if we’re done with the url, revoke it):
setTimeout(function(){
URL.revokeObjectURL(url);
document.body.removeChild(anchor);
}, 1);
And now, the user should be presented with a download of the client-side generated content, with the specified name, without needing any plugins or bounces off the server. Beautiful.
Header image via Wikimedia commons