Create an Electron App with a Loading Screen
Overview
We’ve written quite a few Electron apps over the past few years for clients who have public-facing displays and interactive screens in their museums and lobbies. One thing which Interactive Knowledge enables our clients to do is manage the user experience with a content management system (CMS). This gives our clients a certain level of flexibility to keep content fresh and changing across weeks, months and years of the app’s lifetime.
With the introduction of a CMS comes a challenge. We prefer to keep a copy of all data locally so that experiences driven by large data audio and video files aren't delayed on the user’s end. Therefore we need to create a loading script to grab updated content when the app loads and before any user is allowed to see the experience. This is where we employ a loading screen and Electron gives us all the tools we need to implement one.
We will work our way, step-by-step, up to the concept of Node.js EventEmitters — so let’s get started with a simple Electron app which demonstrates Electron showing a loading screen and then proceeding to the main view when we are ready. I’ve used Electron’s Writing Your First Electron App to help us get started. Use their instructions to help set up a basic Electron app.
BrowserWindow loadFile
const { app, BrowserWindow } = require('electron')
const createMainWindow = () => new BrowserWindow()
app.on('ready', () => {
const window = createMainWindow()
window.loadFile('loading.html')
setTimeout(() => window.loadFile('index.html'), 3000)
})
In this first example I’m highlighting how you can use loadFile() to handle changes in your Electron App. In this simple example your loading page (loading.html) will be displayed as soon as the app is ready. Then after a timeout period of 3000ms, the index.html page will be displayed. This is the simplest way to switch pages. Except we haven’t yet solved the initial problem: how do we know when to show index.html? How will we know when our content is finished loading?
EventEmitter
Node.js is packaged with an EventEmitter class which allows you to define your own events. Our goal here is to create an event which will trigger the application to close the loading window and load the main index content. Here is an updated iteration of the the first example, except that after a timeout period we will emit an event to trigger a page change.
const { app, BrowserWindow } = require('electron')
const EventEmitter = require('events')
const loadingEvents = new EventEmitter()
const createMainWindow = () => new BrowserWindow()
app.on('ready', () => {
const window = createMainWindow()
window.loadFile('loading.html')
// Our loadingEvents object listens for 'finished'
loadingEvents.on('finished', () => {
window.loadFile('index.html')
})
setTimeout(() => loadingEvents.emit('finished'), 3000)
})
At the beginning of this app we are pulling in a new class, EventEmitter, from the Node.js events package. Our loadingEvents object is now able to listen for (.on) and emit (.emit) custom events that we define in our application. You may be familiar with eventListeners in the browser or, in this example, the Electron app is utilizing an event listener to fire a function when everything is ‘ready.’
In this app we’ve defined a ‘finished’ event. When the loadingEvents emits ‘finished’ from the same object, this listener will trigger our Electron app to show index.html. I am still utilizing setTimeout function to delay a reaction for a specific number of milliseconds. Let’s see what the code looks like when we are actually downloading content.
Download the Content and Callback the Event
Let’s put this together, let’s create a function that downloads some data from an internet, CMS source.
const download = url => {
const file = fs.createWriteStream('big-file.jpg');
http.get(url, function(response) {
response.pipe(file)
file.on('finish', function() {
file.close(() => loadingEvents.emit('finished'))
})
}).on('error', function(err) {
fs.unlink(dest)
})
}
The download function gets a remote URL and creates a local file called big-file.jpg. As the Electron app gets the data it pipes it into the file. When the app is finished, the file closes and you’ll see we specify a callback which will be triggered when the file is finished writing. There we put our loadingEvents. Now we know that index.html will not be shown until our file is finished downloading. Now we have a dynamic loading screen!
const http = require('https')
const fs = require('fs')
const { app, BrowserWindow } = require('electron')
const EventEmitter = require('events')
const loadingEvents = new EventEmitter()
const createMainWindow = () => new BrowserWindow()
app.on('ready', () => {
const window = createMainWindow()
window.loadFile('loading.html')
// Our loadingEvents object listens for 'finished'
loadingEvents.on('finished', () => {
window.loadFile('index.html')
})
download('https://512pixels.net/downloads/macos-wallpapers/10-15-Day.jpg')
})
const download = url => {
const file = fs.createWriteStream('big-file.jpg');
http.get(url, function(response) {
response.pipe(file)
file.on('finish', function() {
file.close(() => loadingEvents.emit('finished'))
})
}).on('error', function(err) {
fs.unlink(dest)
})
}
Track and Report the Progress
At this point I have demonstrated all you need to dynamically download data to your app and then display the main screen. However, what if we want to give the user an indication of the app’s progress? That can easily be achieved by using Electron’s BrowserWindow to send data from the Electron process to the browser window. What we will do is create a second event that delivers the percentage progress download to the Loading page and we will write some simple Javascript to handle that info.
const http = require('https')
const fs = require('fs')
const { app, BrowserWindow } = require('electron')
const EventEmitter = require('events')
const loadingEvents = new EventEmitter()
const createMainWindow = () => new BrowserWindow({
webPreferences: {
nodeIntegration: true
}
})
app.on('ready', () => {
const window = createMainWindow()
window.loadFile('loading.html')
// Our loadingEvents object listens for 'finished'
loadingEvents.on('finished', () => {
window.loadFile('index.html')
})
loadingEvents.on('progress', percentage => {
window.webContents.send('progress', percentage)
})
download(
'https://512pixels.net/downloads/macos-wallpapers/10-15-Day.jpg'
)
})
const download = (url, closeCallback) => {
const file = fs.createWriteStream('big-file.jpg');
http.get(url, function(response) {
let total = 0;
response.on('data', (c) => {
total += c.length
loadingEvents.emit('progress', total/response.headers['content-length'])
})
response.pipe(file)
file.on('finish', function() {
file.close(() => loadingEvents.emit('finished'))
}).on('error', function(err) {
fs.unlink(dest)
})
}
There is a little bit more going on here. Firstly, I’ve enabled nodeIntegration when I create my BrowserWindow so that I may access Electron’s IpcRenderer from the page Javascript. Secondly, you’ll see that I’ve created a second event ‘progress’ that sends information about the download progress to the window object. Finally, I’ve modified the http.get download function to track the total size of the file and keep track of how much data has been downloaded. In this section I create my percentage.
On my loading screen, I write a bit of Javascript:
require('electron').ipcRenderer.on('progress', (event, message) => {
const percent = Math.floor(message * 100)
window.document.getElementById('percentage').innerHTML = percent
})
And what I get is a dynamic page that keeps track of the percentage I’ve downloaded.
Check out the source code to see the entire demo in action.
Our expertise does not end here. Want to see how to code sign your Electron app when you are ready to distribute it to many platforms? Have questions about building your own app? Contact us!