Reputation: 901
Here is a code to preload files and save them to browser's cache using HTTP requests in JavaScript. The problem is I can't find a solution to track errors in preloadMe
function without your kind help:)
I know we can add onerror
to preloadOne
and it works fine:
xhr.onerror = function() {
console.log('onerror happend');
};
But I wonder how can we do this in preloadMe
function like oncomplete
, onfetched
and onprogress
. something like this:
preload.onerror = items => {
console.log('onerror happend'); // onerror here
}
Any comments or modifications are extremely appreciated...
Here is my code so far:
(function(global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Preload = factory());
}(this, (function() {
'use strict';
function preloadOne(url, done) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'blob';
xhr.onprogress = event => {
if (!event.lengthComputable) return false
let item = this.getItemByUrl(event.target.responseURL);
item.completion = parseInt((event.loaded / event.total) * 100);
item.downloaded = event.loaded;
item.total = event.total;
this.updateProgressBar(item);
};
xhr.onload = event => {
let type = event.target.response.type;
let blob = new Blob([event.target.response], {
type: type
});
let url = URL.createObjectURL(blob);
let responseURL = event.target.responseURL;
let item = this.getItemByUrl(responseURL);
item.blobUrl = url;
item.fileName = responseURL.substring(responseURL.lastIndexOf('/') + 1);
item.type = type;
item.size = blob.size;
done(item);
};
xhr.send();
}
function updateProgressBar(item) {
var sumCompletion = 0;
var maxCompletion = this.status.length * 100;
for (var itemStatus of this.status) {
if (itemStatus.completion) {
sumCompletion += itemStatus.completion;
}
}
var totalCompletion = parseInt((sumCompletion / maxCompletion) * 100);
if (!isNaN(totalCompletion)) {
this.onprogress({
progress: totalCompletion,
item: item
});
}
}
function getItemByUrl(rawUrl) {
for (var item of this.status) {
if (item.url == rawUrl) return item
}
}
function fetch(list) {
return new Promise((resolve, reject) => {
this.loaded = list.length;
for (let item of list) {
this.onerror(item); // onerror here
this.status.push({
url: item
});
this.preloadOne(item, item => {
this.onfetched(item);
this.loaded--;
if (this.loaded == 0) {
this.oncomplete(this.status);
resolve(this.status);
}
});
}
});
}
function Preload() {
return {
status: [],
loaded: false,
onprogress: () => {},
oncomplete: () => {},
onfetched: () => {},
onerror: () => {}, // onerror here
fetch,
updateProgressBar,
preloadOne,
getItemByUrl
}
}
return Preload;
})));
//preload here
preloadMe();
function preloadMe() {
const preload = Preload();
preload.fetch([
'https://round-arm-authority.000webhostapp.com/Ultimate%20Video%20Hack/videos/vid1.mp4'
]).then(items => {
// use either a promise or 'oncomplete'
console.log(items);
});
preload.oncomplete = items => {
console.log(items);
}
preload.onerror = items => {
console.log('onerror happend'); // onerror here
}
preload.onprogress = event => {
console.log(event.progress + '%');
}
preload.onfetched = item => {
console.log(item);
}
};
Upvotes: 1
Views: 798
Reputation: 7464
There are a couple of options. You could add the error callback as an argument, much as you've done with the done
callback.
If you wanted to make the error callback optional, you'd do something like this:
function preloadOne(url, done, error) {
const xhr = new XML HttpRequest();
// ...
if(typeof error === 'function') xhr.onerror = error;
// ...
}
In some ways this might be preferable because you could register the error handler before calling xhr.send()
. (Although I don't know if it matters, I'm not sure how the event loop works in this regard.)
The other way would be to return xhr
at the end of your preloadOne
function, like this:
function preloadOne(url, done) {
const xhr = new XMLHttpRequest();
// ...
xhr.send();
return xhr;
}
Then you could easily add your own event handler after the fact:
var preload = preloadOne('https://my.site.com/path', x=> {
console.log('done');
});
preload.onerror = function(event) {
console.log('error');
};
return xhr
solution.After doing some research, I believe it is possible to lose error events if you add the event listener after the xhr.send()
. This is based on the MDN event loop page, specifically the Adding messages
section, which says:
In web browsers, messages are added anytime an event occurs and there is an event listener attached to it. If there is no listener, the event is lost.
So while the messages don't get handled until the JavaScript stack is empty, the queuing of a message for an XHR error could happen at just about any time. Or attempted queuing, if the handler isn't attached yet.
However if you really want to return the xhr
and ensure the opportunity for event handlers to be attached, I believe there is a way to do it, by changing preoloadOne
to put the xhr.send
inside of a setTimeout
.
setTimeout(()=> xhr.send(), 0);
This will throw the send
into the event queue, which means it has to wait for the JavaScript call stack to empty and then the next cycle of the event loop. And by the time the call stack empties, any error event handlers should be attached.
Upvotes: 1