Chapter 2. Your first Electron application

This chapter covers

  • Structuring and setting up an Electron application
  • Generating a package.json, and configuring it to work with Electron in development
  • Including a prebuilt version of Electron for your platform in your project
  • Configuring your package.json to start up your main process
  • Creating renderer processes from your main process
  • Taking advantage of Electron’s relaxed sandboxing restrictions to build functionality that normally would not be possible inside of the browser
  • Using Electron’s built-in modules to side-step some common issues

In chapter 1, we discussed what Electron is at a high level. That said, this book is called Electron in Action, right? In this chapter, we learn the ropes of Electron by setting up and building a simple application from the ground up to manage a list of bookmarks. The application will take advantage of features available only in the most modern browsers.

In that high-level discussion from the previous chapter, I mentioned that Electron is a runtime like Node. That’s still true, but I want to revisit that point for a moment. Electron is not a framework—it does not provide any scaffolding or have strong rules about how you structure your application or name your files. Those choices are left up to us, the developers. On the bright side, it also doesn’t enforce any conventions, and we have less conceptual boilerplate information to discuss before getting our hands dirty.

2.1. Building a bookmark list application

Let’s start by building a simple and somewhat naive Electron application to reinforce everything we’ve covered. Our application accepts URLs. When the user provides a URL, we fetch the title of the page that the URL refers to and save it in our application’s localStorage. Finally, we display all the links in the application. You can find the completed source code for this chapter on GitHub (https://github.com/electron-in-action/bookmarker).

Along the way, we uncover some of the advantages of building an application in Electron, such as the ability to bypass the need for a server and use cutting-edge web APIs that do not have wide support across all the browsers but are implemented in modern versions of Chromium. Figure 2.1 is a wireframe of the application we build in this chapter.

Figure 2.1. A wireframe of the application we build in this chapter

When users add the URL of a website that they would like to save to the list below the input fields, the application sends a request to the website to fetch the markup. After it successfully receives the markup, the application pulls the title of the website and appends both the title and URL to the list of websites, which is stored in the browser’s localStorage. When the application starts, it reads from localStorage and restores the list. We add a button with a command to clear localStorage in case anything goes wrong. Because this simple application is designed to help you get comfortable with Electron, we won’t implement advanced moves, such as removing individual websites from the list.

2.1.1. Structuring the Electron application

How you choose to structure your application is up to your team or the individual working on the application. Many developers take slightly different approaches. Looking at some of the more established Electron applications, we can discern common patterns and make decisions on how we’d like to approach our applications in this book.

For our purposes, let’s agree upon a file structure for the remainder of this book. We have an app directory where we store all of our application code. We also have a package.json that will store a list of dependencies, metadata about our application, and scripts and declare where Electron should look for our main process. After we install our dependencies, we end up with a node_modules directory that Electron creates on our behalf, but we won’t include that in the initial setup.

As far as files are concerned, let’s start with two files in our app: main.js and renderer.js. These are purposely simple filenames so we can track the two types of processes. The start of all the applications that we build in this book roughly follows the directory structure shown in figure 2.2. (If you’re running macOS, you can install the tree command using brew install tree.)

Figure 2.2. The file tree structure for our first Electron application

Make a directory called “bookmarker,” and navigate to it. You can create this structure quickly by running the following two commands from the command line. You will generate a package.json file later using npm init.

mkdir app
touch app/main.js app/renderer.js app/style.css app/index.html

Electron doesn’t require this structure, but it is inspired by some of the best practices established by other Electron applications. Atom keeps all of the application code in an app directory and all of its stylesheets and other assets such as images in a static directory. LevelUI has an index.js and a client.js on the top level and keeps all the dependent files in an src directory and stylesheets in a styles directory. Yoda keeps all of its files—including the file that loads the rest of the application—in an src directory. app, src, and lib are common names for the folder that holds the majority of the application’s code, and styles, static, and assets are common names for the directory that holds the static assets used in the application.

2.1.2. package.json

The package.json manifest is used in many—if not most—Node projects. This manifest contains important information about the project. It lists metadata such as the name of the author as well as their email address, which license the project is released under, the location of the project’s git repository, and where to file issues. It also defines scripts for common tasks such as running the test suite or—pertinent to our needs—building the application. The package.json file also lists all of the dependencies used to run and develop the application.

In theory, you could potentially have a Node project that does not have a package.json. But Electron relies on this file and its main property to figure out where to start when it loads or builds your application.

npm, the package manager that ships with Node, comes with a helpful tool for generating package.json. From the “bookmarker” directory you created earlier, run npm init. If you leave a prompt blank, npm uses whatever is in the parentheses after the colon as the default answer. Your answers should look something like figure 2.3, with the exception of the author’s name, of course.

Figure 2.3. npm init provides a series of prompts and sets up a package.json file

Of note is the main entry in the sample package.json. Here, you can see that I set it to point to ./app/main.js, based on how we set up the application. You can point to any file you want. The main file we’re going to use happens to be called main.js, but it could be named anything (e.g., sandwich.js, index.js, app.js).

2.1.3. Downloading and installing Electron in our project

We have the basic structure of our application set up, but Electron is nowhere to be found. Building Electron from source takes a while and can be tedious. We rely on prebuilt versions of Electron for each platform (macOS, Windows, and Linux) and both architectures (32- and 64-bit). We install Electron using npm.

npm allows us to install binaries globally or locally to each project. Installing Electron globally seems convenient, but it can cause trouble down the road if we have multiple applications using different versions of Electron. We’re better off specifying and installing a unique version of Electron for each project we work on.

Downloading and installing Electron is easy. Run the following command from inside the project directory where you ran npm init previously:

npm install electron–-save

This command will download and install Electron in your project’s node_modules directory. (It will also create the directory if you don’t already have one.) The --save flag adds it to the list of dependencies in our package.json. This means that if someone downloads the project and runs npm install, they will get electron by default.

A word on electron-prebuilt

As you acclimate yourself to the world of Electron, you may see blog posts, documentation, and even earlier versions of this book that refer to the electron-prebuilt package instead of electron. In the past, the former was the preferred way to install a precompiled version of Electron for your operating system. The latter is the new preferred way. As of early 2017, electron-prebuilt is no longer supported.

npm also lets you define shortcuts for running common scripts in your package.json. When you run a script defined in your package.json, npm automatically adds node_modules to the path. This means that it will use the locally installed version of Electron by default. Let’s add a start script to our package.json.

Listing 2.1. Adding a start script to package.json
{
  "name": "bookmarker",
  "version": "1.0.0",
  "description": "Our very first Electron application",
  "main": "./app/main.js",
  "scripts": {
    "start": "electron .",                                     1
    "test": "echo "Error: no test specified" && exit 1"
  },
  "author": "Steve Kinney",
  "license": "ISC",
  "dependencies": {
    "electron": "^2.0.4"
  }
}

  • 1 What npm will run when we use npm start.

Now when we run npm start, npm uses our locally installed version of electron to start the Electron application. You’ll notice that not much seems to happen. You should see the following code in your terminal application:

> [email protected] start /Users/stevekinney/Projects/bookmarker
> electron .

You’ll also see a new application in your dock or task bar—the Electron application we just set up—as shown in figure 2.4. Right now, it’s called simply “Electron,” and it uses Electron’s default application icon. In later chapters, we’ll see how we can customize these properties, but the default is good enough for now. All of our implementation files are completely blank. As a result, there isn’t a lot going on with this application, but it exists and starts up correctly. We count that as a win for the time being. Closing all windows of the application on Windows or selecting Quit from the application menu terminates the process. Alternatively, you can press Control-C in the Windows Command prompt or Terminal to quit the application. Pressing Command-Period terminates a process on macOS.

Figure 2.4. The application in the dock isn’t just any Electron application; it’s the Electron application we just set up.

2.2. Working with the main process

Now that we have an Electron application, it would be cool if we could actually get it to do something. If you recall from chapter 1, Electron starts with a main process that can create one or more renderer processes. We start by writing code in main.js to get our application off the ground.

To work with Electron, we need to import the electron library. Electron comes with a number of useful modules that we use throughout this book. The first—and arguably, most important—is the app module.

Listing 2.2. Adding a basic main process: ./app/main.js
const {app} = require('electron');

app.on('ready', () => {                   1
  console.log('Hello from Electron');
});

  • 1 Called as soon as the application has fully launched.

app is a module that handles the lifecycle and configuration of our application. We can use it to quit, hide, and show the application as well as get and set the application’s properties. The app module also runs events—including before-quit, window -all-closed, browser-window-blur, and browser-window-focus—when the application enters different states.

We cannot work with our application until it has completely started up and is ready to go. Luckily, app fires a ready event. This means we need to wait patiently and listen for the application to start the ready event before we do anything. In the previous code, we logged into the console, which is something we could easily do without Electron, but this code highlights how to listen for the ready event.

2.3. Creating a renderer process

Our main process is a lot like any other Node process. It has access to all of Node’s built-in libraries as well as a special set of modules provided by Electron, which we explore over the course of this book. But, like any other Node process, our main process does not have a DOM (Document Object Model) and cannot render a UI. The main process is responsible for interacting with the operating system, managing state, and coordinating with all the other processes in our application. It is not in charge of rendering HTML and CSS. That’s the job of the renderer processes. One of the primary reasons we signed up for this whole Electron adventure is that we wanted to create a GUI for Node processes.

The main process can create multiple renderer processes using the Browser-Window module. Each BrowserWindow is a separate and unique renderer process that includes a DOM, access to the Chromium web APIs, and the Node built-in module. We can access the BrowserWindow module the same way we got our hands on the app module.

Listing 2.3. Requiring the BrowserWindow module: ./app/main.js
const {app, BrowserWindow} = require('electron');

You may have noticed that the BrowserWindow module starts with a capital letter. According to standard JavaScript convention, this usually means that we call it as a constructor with the new keyword. We can use this constructor to create as many renderer processes as we like or our computer can handle. When the application is ready, we create a BrowserWindow instance. Let’s update our code as follows.

Listing 2.4. Creating a BrowserWindow: ./app/main.js
const {app, BrowserWindow} = require('electron');

let mainWindow = null;                      1

app.on('ready', () => {
  console.log('Hello from Electron.');
  mainWindow = new BrowserWindow();         2
});

  • 1 Creates a variable in the top-level scope for the main window of our application
  • 2 When the application is ready, creates a browser window, and assigns it to the variable created in the top-level scope

We declared mainWindow outside the ready event listener. JavaScript uses function scope. If we declared mainWindow inside the event listener, mainWindow would be eligible for garbage collection because the function assigned to the ready event has run to completion. If garbage is collected, our window would mysteriously disappear. If we run this code, we see a humble little window displayed in the center of our screen, as shown in figure 2.5.

Figure 2.5. An empty BrowserWindow without an HTML document loaded

It’s a window, but it’s not much to look at. The next step is to load an HTML page into that BrowserWindow instance we created. All BrowserWindow instances have a web-Contents property, which has several useful features, such as loading an HTML file into the renderer process’s window, sending messages from the main process to the renderer process, printing the page to either PDF or a printer, and much more. Right now, our biggest concern is loading content into that boring window we just created.

We need an HTML page to load, so create an index.html in the app directory of your project. Let’s add the following content to the HTML page to make it a valid document.

Listing 2.5. Creating index.html: ./app/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy"
          content="
            default-src 'self';
            script-src 'self' 'unsafe-inline';
            connect-src *
          "
>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Bookmarker</title>
</head>
<body>

<h1>Hello from Electron</h1>

</body>

</html>

It’s simple, but it gets the job done and gives a good foundation on which to build. We add the following to app/main.js to tell the renderer process to load this HTML document inside of the window we created earlier.

Listing 2.6. Loading an HTML document into the main window: ./app/main.js
app.on('ready', () => {
  console.log('Hello from Electron.');
  mainWindow = new BrowserWindow();
  mainWindow.webContents.loadFile('index.html');        1
});

  • 1 Tells the browser window to load an HTML file located in the same directory as the main process

We use the file:// protocol and the __dirname variable, which is globally available in Node. __dirname is the full path to the directory where our Node process is being executed. In my case, __dirname expands to /Users/stevekinney/Projects/bookmarker/ app. It’s like typing pwd in macOS and Linux or chdir in Windows.

Now, we can use npm start to start our application and watch it load our new HTML file. If all goes well, you should see something resembling figure 2.6.

Figure 2.6. A BrowserWindow with a simple HTML document loaded

2.3.1. Loading code from the renderer process

From the HTML file loaded by the renderer process, we can load any other files we might need just like we would in a traditional browser-based web application—namely, <script> and <link> tags.

What makes Electron different from what we’re used to in the browser is that we have access to all of Node—even from what we would normally consider “the client.” This means that we can use require or even Node-only objects and variables like __dirname or the process module. At the same time, we have all the browser APIs available as well. The division between what we can do only on the client and what we can do only on the server begins to fade away.

Let’s look at this in action. __dirname is not available in the traditional browser environment, and document or alert are not available in Node. But in Electron we can seamlessly use them together. Let’s add a button to the page.

Listing 2.7. Adding a button to an HTML document: ./app/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy"
      content="
            default-src 'self';
            script-src 'self' 'unsafe-inline';
            connect-src *
          "
>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Bookmarker</title>
</head>
<body>

<h1>Hello from Electron</h1>

<p>                                                     1
<button class="alert">Current Directory</button>
</p>

</body>

</html>

  • 1 This is our new button.

Now that we have our button, let’s add an event listener that alerts us to the current directory from which our application is running.

Listing 2.8. Adding script with Node.js global in the browser context: ./app/index.html
<script>
  const button = document.querySelector('.alert');

  button.addEventListener('click', () => {
    alert(__dirname);                        1
  });
</script>

  • 1 When the button is clicked, uses a browser alert to display a Node global variable

alert() is available only in the browser. __dirname is available only in Node. When we click the button, we are treated to Node and Chromium working together in sweet, sweet harmony, as shown in figure 2.7.

Figure 2.7. The BrowserWindow executing JavaScript from the context of the renderer process.

2.3.2. Requiring files in the renderer process

Writing code in our HTML file clearly works, but it’s probably not hard to imagine a situation where our code might grow to the point where this method is no longer feasible. We can add script tags with src attributes to reference other files, but this also becomes cumbersome quickly.

This is where web development gets tricky. Although modules were added to the ECMAScript specification, no browsers currently have a working implementation of a module system. On the client, this is the point where we might consider some kind of build tool like Browserify (http://browserify.org) or the module bundler, webpack, and possibly a task runner like Gulp or Grunt.

We can use Node’s module system with no additional configuration. Let’s move all of the code from inside those <script> tags to our—currently empty—app/renderer.js file. Now we can replace the contents inside of the <script> tags with just a reference to renderer.js.

Listing 2.9. Loading JavaScript from renderer.js: ./app/index.html
<script>
  require('./renderer');       1
</script>

  • 1 Uses Node’s require function to load additional JavaScript modules into the renderer process

If we start up our application, you’ll see that its functionality hasn’t changed. Everything still works as it should. That rarely happens in software development. Let’s briefly savor that feeling before moving on.

2.3.3. Adding styles in the renderer process

Few surprises occur when we reference stylesheets in our Electron applications. Later, we talk about using Sass and Less with Electron. Adding a stylesheet in an Electron application isn’t much different than it would be with a traditional web application. That said, a few nuances are worth talking about.

Let’s start by adding a style.css file to our app directory. We add the following content to that style.css.

Listing 2.10. Adding basic styles: ./app/style.css
html {
  box-sizing: border-box;
}

*, *:before, *:after {
  box-sizing: inherit;
}

body, input {
  font: menu;          1
}

  • 1 Uses the default system font for the operating system the page is running on

That last declaration might look a little unfamiliar. It is unique to Chromium and allows us to use the system font in CSS. This ability is important to make our application fit in with its native siblings. On macOS, it’s the only way to use San Francisco, the system font that ships with El Capitan 10.11 and later.

We should consider one other important distinction when working with CSS inside of our Electron applications. Our applications will run only in the version of Chromium that we ship with the application. We don’t have to worry about cross-browser support or legacy compatibility. As mentioned in chapter 1, Electron ships with a relatively recent version of Chromium. This means we can freely use technologies like flexbox and CSS variables.

We reference our new stylesheet just like we would in the traditional browser environment, then add the following to the <head> section of index.html. I’ll include the HTML tag for linking to a stylesheet—because, in my 20 years as a web developer, I still can never remember how to do it on the first try.

Listing 2.11. Referencing a stylesheet in the HTML document: ./app/index.html
<link rel="stylesheet" href="style.css" type="text/css">

2.4. Implementing the UI

We start by updating our index.html with the markup that we need for the UI.

Listing 2.12. Adding the markup for the UI of the application: ./app/index.html
<h1>Bookmarker</h1>

<div class="error-message"></div>

<section class="add-new-link">
  <form class="new-link-form">
    <input type="url" class="new-link-url" placeholder="URL"size="100"
     required>
    <input type="submit" class="new-link-submit" value="Submit" disabled>
  </form>
</section>

<section class="links"></section>

<section class="controls">
  <button class="clear-storage">Clear Storage</button>
</section>

We have a section for adding a new link, a section for displaying all of our wonderful links, and a button for clearing all links and starting over. The <script> tag in your application should be just as we left it earlier in this chapter, but just in case it isn’t, here is what it should look like at this point:

<script>
  require('./renderer');
</script>

With our markup in place, we can now turn our attention to the functionality. Let’s clear away anything we might have in app/renderer.js and start fresh. Throughout our time together, we’re going to need to work with a few of the elements we added to the markup, so let’s start by querying for those selectors and caching them into variables. Add the following to app/renderer.js.

Listing 2.13. Caching DOM element selectors: ./app/renderer.js
const linksSection = document.querySelector('.links');
const errorMessage = document.querySelector('.error-message');
const newLinkForm = document.querySelector('.new-link-form');
const newLinkUrl = document.querySelector('.new-link-url');
const newLinkSubmit = document.querySelector('.new-link-submit');
const clearStorageButton = document.querySelector('.clear-storage');

If you look back at listing 2.12, you’ll notice that we set the input element’s type attribute to "url" in the markup. Chromium will mark the field as invalid if the contents do not match a valid URL pattern. We can style valid and invalid states of the element and even check its state using JavaScript. Unfortunately, we don’t have access to the built-in error message popups in Chrome or Firefox. Those popups are not part of the Chromium content module and—as a result—not part of Electron. For now, we disable the start button by default and then check to see if we have a valid URL pattern every time the user types a letter into the URL field.

If the user has provided a valid URL, then we flip the switch on that submit button and allow them to submit the URL. Let’s add this code to app/renderer.js.

Listing 2.14. Adding an event listener to enable the submit button: ./app/renderer.js
newLinkUrl.addEventListener('keyup', () => {
  newLinkSubmit.disabled = !newLinkUrl.validity.valid;        1
});

  • 1 When a user types in the input field, this uses Chromium’s ValidityState API to determine if the input is valid. If so, removes the disabled attribute from the submit button.

Now is also a good time to add a small helper function to clear out the contents of the URL field. In a perfect world, we call this whenever we’ve successfully stored the link.

Listing 2.15. Adding a helper function to clear out form input: ./app/renderer.js
const clearForm= () => {
  newLinkUrl.value = null;        1
};

  • 1 Clears the value of the new link input field by setting its value to null.

When the user submits a link, we want the browser to make a request for that URL and then take the response body, parse it, find the title element, get the text from that title element, store the title and URL of the bookmark in localStorage, and then—finally—update the page with the bookmark.

2.4.1. Making cross-origin requests in Electron

You may or may not feel some of the hairs on the back of your neck begin to stand at attention. You might even be thinking to yourself, “There is no way that this plan will work. You can’t make requests to third-party servers. The browser doesn’t allow this.”

Normally, you’d be right. In a traditional browser-based application, you’re not allowed to have your client-side code make requests to other servers. Typically, your client-side code makes a request to your server which in turn proxies the request to the third-party server. When it hears back, it proxies the response back to the client. We discussed some of the reasoning behind this in chapter 1.

Electron has all the abilities of a Node server along with all the bells and whistles of a browser. This means that we’re free to make cross-origin requests without the need for a server to get in the way.

Another perk of writing this application in Electron is that we’re able to use the up-and-coming Fetch API to make requests to remote servers. The Fetch API spares us the hassle of setting up XMLHttpRequests by hand and gives a nice, promise-based interface for working with our requests. As of this writing, Fetch has limited support among the major browsers. That said, it has full support in the current version of Chromium, which means we can use it.

We add an event listener to the form to spring into action whenever the form has been submitted. We don’t have a server, so we need to be sure to prevent the default action of making a request. We do this by preventing the default action. We also cache the value of the URL input field for future use.

Listing 2.16. Adding an event listener to the submit button: ./app/renderer.js
newLinkForm.addEventListener('submit', (event) => {
  event.preventDefault();                             1

  const url = newLinkUrl.value;                       2

  // More code to come...
});

  • 1 Tells Chromium not to trigger an HTTP request, the default action for form submissions
  • 2 Grabs the URL in the new link input field. We’ll need this value shortly.

The Fetch API is available as a globally available fetch variable. Fetching a URL returns a promise object, which will be fulfilled when the browser has completed fetching the remote resource. With this promise object, we could handle the response differently depending on if we decided to fetch a webpage, an image, or some other kind of content. In this case, we’re fetching a webpage, so we convert the response to text. We start with the following code inside our event listener.

Listing 2.17. Using the Fetch API to request a remote resource: ./app/renderer.js
fetch(url)                                1
  .then(response => response.text());     2

  • 1 Uses the Fetch API to fetch the content of the provided URL.
  • 2 Parses the response as plain text

Promises are chainable. We can take the return value of the previous promise and tack on another call to then. Additionally, response.text() itself returns a promise. Our next step will be to take the big block of markup that we received and parse it to traverse it and find the <title> element.

2.4.2. Parsing responses

Chromium provides a parser that will do this for us, but we need to instantiate it. At the top of app/renderer.js, we create an instance of DOMParser and store it for later use.

Listing 2.18. Instantiating a DOMParser: ./app/renderer.js
const parser = new DOMParser();         1

  • 1 Creates a DOMParser instance. We’ll use this after fetching the text contents of the provided URL.

Let’s set up a pair of helper functions that parse the response and find the title for us.

Listing 2.19. Adding functions for parsing response and finding the title: ./app/renderer.js
const parseResponse = (text) => {
  return parser.parseFromString(text, 'text/html');      1
}

const findTitle = (nodes) =>{
  return nodes.querySelector('title').innerText;         2
}

  • 1 Takes the string of HTML from the URL and parses it into a DOM tree.
  • 2 Traverses the DOM tree to find the <title> node.

We can now add those two steps to our promise chain.

Listing 2.20. Parsing response and finding the title when fetching a page: ./app/renderer.js
fetch(url)
  .then(response => response.text())
  .then(parseResponse)
  .then(findTitle);

At this point, the code in app/renderer.js looks like this.

Listing 2.21. Current contents of app/renderer.js
const parser = new DOMParser();

const linksSection = document.querySelector('.links');
const errorMessage = document.querySelector('.error-message');
const newLinkForm = document.querySelector('.new-link-form');
const newLinkUrl = document.querySelector('.new-link-url');
const newLinkSubmit = document.querySelector('.new-link-submit');
const clearStorageButton = document.querySelector('.clear-storage');

newLinkUrl.addEventListener('keyup', () => {
  newLinkSubmit.disabled = !newLinkUrl.validity.valid;
});

newLinkForm.addEventListener('submit', (event) => {
  event.preventDefault();

  const url = newLinkUrl.value;

  fetch(url)
    .then(response => response.text())
    .then(parseResponse)
    .then(findTitle)
});

const clearForm = () => {
  newLinkUrl.value = null;
}

const parseResponse = (text) => {
  return parser.parseFromString(text, 'text/html');
}

const findTitle = (nodes) => {
  return nodes.querySelector('title').innerText;
}

2.4.3. Storing responses with web storage APIs

localStorage is a simple key/value store that is built into the browser and persists between sessions. You can store simple data types like strings and numbers under an arbitrary key. Let’s set up another helper function that will make a simple object out of the title and URL, convert it into a string using the built-in JSON library, and then store it using the URL as the key.

Listing 2.22. Creating a function to persist links in local storage: ./app/renderer.js
const storeLink = (title, url) => {
  localStorage.setItem(url, JSON.stringify({ title: title, url: url }));
};

Our new storeLink function needs the title as well as the URL to get its job done, but the previous promise returns only the title. We use an arrow function to wrap our call to storeLink in an anonymous function that has access to the url variable in scope. If that is successful, we clear the form as well.

Listing 2.23. Storing a link and clearing the form upon fetching remote resource: ./app/renderer.js
fetch(url)
    .then(response => response.text())
    .then(parseResponse)
    .then(findTitle)
    .then(title => storeLink(title, url))           1
    .then(clearForm);

  • 1 Stores the title and URL into localStorage.

2.4.4. Displaying request results

Storing the links is not enough. We also want to display them to the user. This means that we need to create the functionality to go through all the links that we stored, turn them into DOM nodes, and then add them to the page.

Let’s start with the ability to get all the links out of localStorage. If you recall, localStorage is a key/value storage. We can use Object.keys to get all the keys out of an object. We have to give ourselves another helper function to get all the links out of localStorage. This isn’t a huge deal because we needed to convert them from strings back into real objects anyway. Let’s define a getLinks function.

Listing 2.24. Creating a function for getting links from local storage: ./app/renderer.js
const getLinks = () => {
  return Object.keys(localStorage)                                  1
               .map(key => JSON.parse(localStorage.getItem(key)));  2
}

  • 1 Gets an array of all the keys currently stored in localStorage
  • 2 For each key, gets its value and parses it from JSON into a JavaScript object

Next, we take these simple objects and convert them into markup so that we can add them to the DOM later. We create a simple convertToElement helper that can take care of this as well. It’s important to mention that our convertToElement function is a bit naive and does not try to sanitize user input. In theory, your application is vulnerable to script-injection attacks. It’s a bit outside of the scope of this chapter, so we do just the bare minimum to render these links onto the page. I’ll leave it as an exercise to the reader to secure this feature.

Listing 2.25. Creating a function for creating DOM nodes from link data: ./app/renderer.js
const convertToElement = (link) => {
  return `
<div class="link">
<h3>${link.title}</h3>
<p>
<a href="${link.url}">${link.url}</a>
</p>
</div>
`;
};

Finally, we create a renderLinks() function that calls getLinks, concatenates them, converts the collection using convertToElement(), and then replaces the linksSection element on the page.

Listing 2.26. Creating a function to render all links and add them to the DOM: ./app/renderer.js
const renderLinks = () => {
  const linkElements = getLinks().map(convertToElement).join('');    1
  linksSection.innerHTML = linkElements;                             2
};

  • 1 Converts all the links to HTML elements and combines them
  • 2 Replaces the contents of the links section with the combined link elements

We can now add now add this final step to our promise chain.

Listing 2.27. Rendering links after fetching a remote resource: ./app/renderer.js
fetch(url)
  .then(response => response.text())
  .then(parseResponse)
  .then(findTitle)
  .then(title => storeLink(title, url))
  .then(clearForm)
  .then(renderLinks);

We also render all of the links when the page initially loads simply by calling renderLinks() at the top-level scope.

Listing 2.28. Loading and rendering links: ./app/render.js
renderLinks();       1

  • 1 Calls the renderLinks() function we created earlier as soon as the page loads

One of the advantages of using promises in coordination with breaking out functionality into named helper functions is that it’s very clear what our code is doing as it works through fetching the external webpage, parsing it, storing the result, and re-rendering the list of links.

The final thing we need to complete all of the functionality for our simple application is to wire up the Clear Storage button. We call the clear method on localStorage and then empty the list in linksSection.

Listing 2.29. Wiring the Clear Storage button: ./app/renderer.js
clearStorageButton.addEventListener('click', () => {
  localStorage.clear();                                 1
  linksSection.innerHTML = '';                          2
});

  • 1 Empties all the links from localStorage
  • 2 Removes the links from the UI

With the Clear Storage button in place, it seems we have most of the functionality in place. Our application now looks something like figure 2.8. At this point, our code for our renderer process should look like listing 2.30.

Figure 2.8. The complete Bookmarker application

Listing 2.30. Renderering process to fetch, store, and render links: ./app/renderer.js
const parser = new DOMParser();

const linksSection = document.querySelector('.links');
const errorMessage = document.querySelector('.error-message');
const newLinkForm = document.querySelector('.new-link-form');
const newLinkUrl = document.querySelector('.new-link-url');
const newLinkSubmit = document.querySelector('.new-link-submit');
const clearStorageButton = document.querySelector('.clear-storage');

newLinkUrl.addEventListener('keyup', () => {
  newLinkSubmit.disabled = !newLinkUrl.validity.valid;
});

newLinkForm.addEventListener('submit', (event) => {
  event.preventDefault();

  const url = newLinkUrl.value;

  fetch(url)
    .then(response => response.text())
    .then(parseResponse)
    .then(findTitle)
    .then(title => storeLink(title, url))
    .then(clearForm)
    .then(renderLinks);
});

clearStorageButton.addEventListener('click', () => {
  localStorage.clear();
  linksSection.innerHTML = '';
});

const clearForm = () => {
  newLinkUrl.value = null;
}

const parseResponse = (text) => {
  return parser.parseFromString(text, 'text/html');
}

const findTitle = (nodes) => {
  return nodes.querySelector('title').innerText;
}

const storeLink = (title, url) => {
  localStorage.setItem(url, JSON.stringify({ title: title, url: url }));
}

const getLinks = () => {
  return Object.keys(localStorage)
               .map(key => JSON.parse(localStorage.getItem(key)));
}

const convertToElement = (link) => {
  return `<div class="link"><h3>${link.title}</h3>
<p><a href="${link.url}">${link.url}</a></p></div>`;
}

const renderLinks = () => {
  const linkElements = getLinks().map(convertToElement).join('');
  linksSection.innerHTML = linkElements;
}

renderLinks();

2.4.5. The unhappy path

So far, everything appears to work. Our application fetches the title from the external webpage, stores the link locally, renders the links on the page, and clears them from the page when we ask it to.

But what happens if something goes wrong? What happens if we give it an invalid link? What happens if the request times out? We’ll handle the two most likely cases: when the user provides a URL that passed the validation check on the input field but is not in fact valid, and when the URL is valid but the server returns a 400- or 500-level error.

The first thing we add is the ability to handle any error. Promise chains support a catch method, which is called into action in the event of an uncaught error. We define another helper method in this event.

Listing 2.31. Displaying an error message: ./app/renderer.js
const handleError = (error, url) => {
  errorMessage.innerHTML = `
There was an issue adding "${url}": ${error.message}
  `.trim();                                                  1
  setTimeout(() => errorMessage.innerText = null, 5000);     2
}

  • 1 Sets the contents of the error message element if fetching a link fails
  • 2 Clears the error message after 5 seconds

We can add that to the chain. We use another anonymous function to pass along the URL with our error message. This is primarily for providing better error messages. It’s not necessary if you don’t want to include the URL in the error message.

Listing 2.32. Catching errors when fetching, parsing, and rendering links: ./app/renderer.js
fetch(url)
  .then(response => response.text())
  .then(parseResponse)
  .then(findTitle)
  .then(title => storeLink(title, url))
  .then(clearForm)
  .then(renderLinks)
  .catch(error => handleError(error, url));        1

  • 1 If any promise in this chain rejects or throws an error, catches the error and displays it in the UI

We also add a step early on to the chain that checks to see if the request was successful. If so, it passes the request along to the next promise in the chain. If it was not successful, then we throw an error, which circumvents the rest of the promises in the chain and skips directly to the handleError() step. There is an edge case here that I didn’t handle: the promise returned from the Fetch API rejects outright if it cannot establish a network connection. I leave that as an exercise to the reader to handle because we have a lot to cover in this book and a limited number of pages to do it in. response.ok will be false if its status code is in the 400- or 500-range.

Listing 2.33. Validating responses from remote servers: ./app/renderer.js
const validateResponse = (response) => {
  if (response.ok) { return response; }                  1
  throw new Error(`Status code of ${response.status}
     ${response.statusText}`);                           2
}

  • 1 If the response was successful, passes it along to the next promise.
  • 2 Throws an error if the request received a 400- or 500-series response.

This code passes the response object along if there is nothing wrong. But if there is something wrong, it throws an error, which is caught by handleError() and dealt with accordingly.

Listing 2.34. Adding validateResponse() to the chain: ./app/renderer.js
fetch(url)
  .then(validateResponse)
  .then(response => response.text())
  .then(parseResponse)
  .then(findTitle)
  .then(title => storeLink(title, url))
  .then(clearForm)
  .then(renderLinks)
  .catch(error => handleError(error, url));

2.4.6. An unexpected bug

We’re not out of the woods yet—we also have an issue in the event that everything goes well. What happens if we click one of the links in our application? Perhaps unsurprisingly, it goes to that link. The Chromium part of our Electron application thinks that it is a web browser, and so it does what web browsers do best—it goes to the page.

Except our application is not really a web browser. It lacks important things like a Back button or a location bar. If we click any of the links in our application, we’re pretty much stuck there. Our only option is to kill the application and start all over.

The solution is to open the links in a real browser. But this raises the question, which browser? How can we tell what the user has set as their default browser? We certainly don’t want to take any lucky guesses because we don’t know what browsers the user has installed and no one likes seeing the wrong application start opening just because they clicked a link.

Electron ships with the shell module, which provides some functions related to high-level desktop integration. The shell module can ask the user’s operating system what browser they prefer and pass the URL to that browser to open. Let’s start by pulling in Electron and storing a reference to its shell module at the top of app/renderer.js.

Listing 2.35. Requiring Electron’s shell module: ./app/renderer.js
const {shell} = require('electron');

We can use JavaScript to determine which URLs we want to handle in our application and which ones we want to pass along to the default browser. In our simple application, the distinction is easy. We want all of the links to open in the default browser. Links are being added and removed in this application, so we set an event listener on the linksSection element and allow click events to bubble up. If the target element has an href attribute, we prevent the default action and pass the URL to the default browser instead.

Listing 2.36. Opening links in the user’s default browser: /app/renderer.js
linksSection.addEventListener('click', (event) => {
  if (event.target.href) {                           1
    event.preventDefault();                          2
    shell.openExternal(event.target.href);           3
  }
});

  • 1 Checks to see if the element that was clicked was a link by looking for an href attribute
  • 2 If it was a link, don’t open it normally.
  • 3 Uses Electron’s shell module to open a link in the user’s default browser

With that relatively simple change, our code behaves as expected. Clicking a link will open that page in the user’s default browser. We have a simple—yet fully functional—desktop application.

Our finished code should look something like the following code example. You may have your functions in a different order.

Listing 2.37. Completed application: ./app/renderer.js
const {shell} = require('electron');

const parser = new DOMParser();

const linksSection = document.querySelector('.links');
const errorMessage = document.querySelector('.error-message');
const newLinkForm = document.querySelector('.new-link-form');
const newLinkUrl = document.querySelector('.new-link-url');
const newLinkSubmit = document.querySelector('.new-link-submit');
const clearStorageButton = document.querySelector('.clear-storage');

newLinkUrl.addEventListener('keyup', () => {
  newLinkSubmit.disabled = !newLinkUrl.validity.valid;
});

newLinkForm.addEventListener('submit', (event) => {
  event.preventDefault();

  const url = newLinkUrl.value;

  fetch(url)
    .then(response => response.text())
    .then(parseResponse)
    .then(findTitle)
    .then(title => storeLink(title, url))
    .then(clearForm)
    .then(renderLinks)
    .catch(error => handleError(error, url));
});

clearStorageButton.addEventListener('click', () => {
  localStorage.clear();
  linksSection.innerHTML = '';
});

linksSection.addEventListener('click', (event) => {
  if (event.target.href) {
    event.preventDefault();
    shell.openExternal(event.target.href);
  }
});

const clearForm = () => {
  newLinkUrl.value = null;
};

const parseResponse = (text) => {
  return parser.parseFromString(text, 'text/html');
};

const findTitle = (nodes) => {
  return nodes.querySelector('title').innerText;
};

const storeLink = (title, url) => {
  localStorage.setItem(url, JSON.stringify({ title: title, url: url }));
};

const getLinks = () => {
  return Object.keys(localStorage)
               .map(key => JSON.parse(localStorage.getItem(key)));
};

const convertToElement = (link) => {
  return `<div class="link"><h3>${link.title}</h3>
<p><a href="${link.url}">${link.url}</a></p></div>`;
};

const renderLinks = () => {
  const linkElements = getLinks().map(convertToElement).join('');
  linksSection.innerHTML = linkElements;
};

const handleError = (error, url) => {
  errorMessage.innerHTML = `
    There was an issue adding "${url}": ${error.message}
  `.trim();
  setTimeout(() => errorMessage.innerText = null, 5000);
};

const validateResponse = (response) => {
  if (response.ok) { return response; }
  throw new Error(`Status code of ${response.status}
     ${response.statusText}`);
}

renderLinks();

Summary

  • Electron does not recommend or enforce a particular project structure.
  • Electron uses npm’s package.json manifest to determine what file it should load as the main process.
  • We can generate a package.json from a boilerplate by using npm init.
  • We typically install Electron locally in each project we work on. This allows us to have project-specific versions of Electron.
  • We can use require(’electron’) in Electron applications to access Electron-specific modules and functionality.
  • The app module manages the lifecycle of our Electron application.
  • The main process cannot render a UI.
  • We can create renderer processes from the main process using the BrowserWindow module.
  • Electron allows us to make requests from a third-party server directly from the browser without an intermediary server. Traditional web applications are not permitted to do this.
  • Storing data in localStorage will allow it to persist when we quit and reopen the application.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.231.155