We have learned how to use the CustomEvent
interface in a previous post.
How can we create a progress indicator using the same JavaScript code for both browser and terminal (using Node.js)? For this we can build a fetch
wrapper with a progress event using the CustomEvent interface, which is compatible with both environments.
📣 The CustomEvent
interface was added in Node.js v18.7.0 as an experimental API, and it's exposed on global
using the --experimental-global-customevent
flag.
Implementing our event
We need to extend the EventTarget
interface to dispatch events from our custom class so the clients can subscribe to our events.
class Http extends EventTarget {
…
async get(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(response.statusText);
}
const contentLength = this._getContentLength(response);
const self = this;
const res = new Response(new ReadableStream({
async start(controller) {
const reader = response.body.getReader();
let loaded = 0;
try {
while (true) {
const {done, value} = await reader.read();
if (done) {
break;
}
loaded += value.byteLength;
if (contentLength) {
self.dispatchEvent(new CustomEvent('progress', {detail: {contentLength, loaded}}));
}
controller.enqueue(value);
}
controller.close();
} catch (err) {
controller.error(err);
}
}
}));
return res.blob();
}
}
export default Http;
We wrapped the ReadableStream
instance of the body
property into a custom implementation to notify the read progress to the listeners of the progress
event. We should also read()
all the content of the response until the done
flag indicates that we've reached the end of the stream.
Using our progress event in the terminal
Let's import the Http
class and add an event listener for the progress
event. For this example we're going to use a server with download speed up to 30kbps.
const exec = async () => {
const { default: Http } = await import('./http.mjs');
const http = new Http();
const listener = e => console.log(e.detail);
http.addEventListener('progress', listener);
await http.get('https://fetch-progress.anthum.com/30kbps/images/sunrise-baseline.jpg');
http.removeEventListener('progress', listener);
}
exec();
💡 The listener should be removed to avoid memory leaks in our server. 😉
🧠 We need to use the dynamic import()
to import ES modules into CommonJS code.
To run this code, we should include the --experimental-global-customevent
flag; otherwise the CustomEvent
class will be undefined
.
node --experimental-global-customevent index.js
Using our progress event in the browser
Let's create an index.html
and import our JavaScript module using the following code:
<script type="module">
import Http from './http.mjs';
const http = new Http();
const listener = e => console.log(e.detail);
http.addEventListener('progress', listener);
await http.get('https://fetch-progress.anthum.com/30kbps/images/sunrise-baseline.jpg');
http.removeEventListener('progress', listener);
</script>
We can run our example locally with the following command:
npx http-server
Now we can navigate to http://localhost:8080
and check the console output.
Conclusion
With the EventTarget
interface we can create reusable code detached from our UI that can be connected to either HTML elements or the terminal to inform progress to our users.
If we don't want to use an experimental API behind the flag in our server we can use the EventEmitter
class in Node.js.
You can check the full code example in https://github.com/navarroaxel/fetch-progress.
For this post, I have adapted the fetch-basic
example from https://github.com/AnthumChris/fetch-progress-indicators by @anthumchris.
Open source rocks. 🤘