Connect with us

Technology

Construct a Safe Desktop App with Electron Forge and React – SitePoint


On this article, we’ll create a easy desktop utility utilizing Electron and React. Will probably be a small textual content editor known as “scratchpad” that mechanically saves adjustments as you sort, just like FromScratch. We’ll take note of making the appliance safe through the use of Electron Forge, the up-to-date construct software supplied by the Electron crew.

Electron Forge is “an entire software for creating, publishing, and putting in fashionable Electron functions”. It gives a handy growth atmosphere, in addition to configuring all the pieces wanted for constructing the appliance for a number of platforms (although we gained’t contact on that on this article).

We’ll assume you recognize what Electron and React are, although you don’t must know these to comply with together with the article.

You’ll find the code for the completed utility on GitHub.

Setup

This tutorial assumes that you’ve got Node put in in your machine. If that’s not the case, please head over to the official obtain web page and seize the proper binaries to your system, or use a model supervisor reminiscent of nvm. We’ll additionally assume a working set up of Git.

Two necessary phrases I’ll use under are “foremost” and “renderer”. Electron functions are “managed” by a Node.js JavaScript file. This file is known as the “foremost” course of, and it’s chargeable for something operating-system associated, and for creating browser home windows. These browser home windows run Chromium, and are known as the “renderer” a part of Electron, as a result of it’s the half that truly renders one thing to the display.

Now let’s start by establishing a brand new challenge. Since we wish to use Electron Forge and React, we’ll head over to the Forge web site and have a look at the information for integrating React.

First off, we have to arrange Electron Forge with the webpack template. Right here’s how we are able to do this in a single terminal command:

$ npx create-electron-app scratchpad --template=webpack

Working that command will take a short while because it units up and configures all the pieces from Git to webpack to a bundle.json file. When that’s accomplished and we cd into that listing, that is what we see:

➜  scratchpad git:(grasp) ls
node_modules
bundle.json
src
webpack.foremost.config.js
webpack.renderer.config.js
webpack.guidelines.js

We’ll skip over the node_modules and bundle.json, and earlier than we peek into the src folder, let’s go over the webpack recordsdata, since there are three. That’s as a result of Electron truly runs two JavaScript recordsdata: one for the Node.js half, known as “foremost”, which is the place it created browser home windows and communicates with the remainder of the working system, and the Chromium half known as “renderer”, which is the half that truly exhibits up in your display.

The third webpack file — webpack.guidelines.js — is the place any shared configuration between Node.js and Chromium is about to keep away from duplication.

Okay, now it’s time to look into the src folder:

➜  src git:(grasp) ls
index.css
index.html
foremost.js
renderer.js

Not too overwhelming: an HTML and CSS file, and a JavaScript file for each the principle, and the renderer. That’s wanting good. We’ll open these up afterward within the article.

Including React

Configuring webpack could be fairly daunting, so fortunately we are able to largely comply with the information to integrating React into Electron. We’ll start by putting in all of the dependencies we’d like.

First, the devDependencies:

npm set up --save-dev @babel/core @babel/preset-react babel-loader

Adopted by React and React-dom as common dependencies:

npm set up --save react react-dom

With all of the dependencies put in, we have to educate webpack to assist JSX. We will do this in both webpack.renderer.js or webpack.guidelines.js, however we’ll comply with the information and add the next loader into webpack.guidelines.js:

module.exports = [
  ...
  {
    test: /.jsx?$/,
    use: {
      loader: 'babel-loader',
      options: {
        exclude: /node_modules/,
        presets: ['@babel/preset-react']
      }
    }
  },
];

Okay, that ought to work. Let’s rapidly take a look at it by opening up src/renderer.js and changing its contents with the next:

import './app.jsx';
import './index.css';

Then create a brand new file src/app.jsx and add within the following:

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(<h2>Hi there from React in Electron!</h2>, doc.physique);

We will take a look at if that works by operating npm begin within the console. If it opens a window that claims “Hi there from React in Electron!”, all the pieces is sweet to go.

You may need observed that the devtools are open when the window exhibits. That’s due to this line within the foremost.js file:

mainWindow.webContents.openDevTools();

It’s nice to depart this for now, as it’ll come in useful whereas we work. We’ll get to foremost.js afterward within the article as we configure its safety and different settings.

As for the error and the warnings within the console, we are able to safely ignore them. Mounting a React element on doc.physique can certainly be problematic with third-party code interfering with it, however we’re not a web site and don’t run any code that’s not ours. Electron offers us a warning as effectively, however we’ll take care of that later.

Constructing Our Performance

As a reminder, we’re going to construct a small scratchpad: just a little utility that saves something we sort as we sort it.

To start out, we’ll add CodeMirror and react-codemirror so we get an easy-to-use editor:

npm set up --save react-codemirror codemirror

Let’s arrange CodeMirror. First, we have to open up src/renderer.js and import and require some CSS. CodeMirror ships with a few completely different themes, so decide one you want, however for this text we’ll use the Materials theme. Your renderer.js ought to now appear like this:

import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/materials.css';
import './app.jsx';
import './index.css';

Be aware how we import our personal recordsdata after the CodeMirror CSS. We do that so we are able to extra simply override the default styling later.

Then in our app.jsx file we’re going to import our CodeMirror element as follows:

import CodeMirror from 'react-codemirror';

Create a brand new React element in app.jsx that provides CodeMirror:

const ScratchPad = () => {
  const choices = {
    theme: "materials"
  };

  const updateScratchpad = newValue => {
    console.log(newValue)
  }

  return <CodeMirror
    worth="Hi there from CodeMirror in React in Electron"
    onChange={updateScratchpad}
    choices={choices} />;
}

Additionally change the render perform to load our ScratchPad element:

ReactDOM.render(<ScratchPad />, doc.physique);

After we begin the app now, we must always see a textual content editor with the textual content “Hi there from CodeMirror in React in Electron”. As we sort into it, the updates will present in our console.

What we additionally see is that there’s a white border, and that our editor doesn’t truly fill the entire window, so let’s do one thing about that. Whereas we’re doing that, we’ll do some housekeeping in our index.html and index.css recordsdata.

First, in index.html, let’s take away all the pieces contained in the physique aspect, since we don’t want it anyway. Then we’ll change the title to “Scratchpad”, in order that the title bar gained’t say “Hi there World!” because the app masses.

We’ll additionally add a Content material-Safety-Coverage. What which means is an excessive amount of to take care of on this article (MDN has a very good introduction, but it surely’s basically a option to forestall third-party code from doing issues we don’t wish to occur. Right here, we inform it to solely permit scripts from our origin (file) and nothing else.

All in all, our index.html can be very empty and can appear like this:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Scratchpad</title>
    <meta http-equiv="Content material-Safety-Coverage" content material="script-src 'self';">
  </head>
  <physique></physique>
</html>

Now let’s transfer to index.css. We will take away all the pieces that’s in there now, and change it with this:

html, physique {
  place: relative;
  width:100vw;
  peak:100vh;
  margin:0;
  background: #263238;
}

.ReactCodeMirror,
.CodeMirror {
  place: absolute;
  peak: 100vh;
  inset: 0;
}

This does a few issues:

  • It removes the margin that the physique aspect has by default.
  • It makes the CodeMirror aspect the identical peak and width because the window itself.
  • It provides the identical background colour to the physique aspect so it blends properly.

Discover how we use inset, which is a shorthand CSS property for the highest, proper, backside and left values. Since we all know that our app is all the time going to run in Chromium model 89, we are able to use fashionable CSS with out worrying about assist!

So that is fairly good: we now have an utility that we are able to begin up and that lets us sort into it. Candy!

Besides, after we shut the appliance and restart it once more, all the pieces’s gone once more. We wish to write to the file system in order that our textual content is saved, and we wish to do this as safely as doable. For that, we’ll now shift our focus to the foremost.js file.

Now, you may need additionally observed that although we added a background colour to the html and physique parts, the window continues to be white whereas we load the appliance. That’s as a result of it takes just a few milliseconds to load in our index.css file. To enhance how this appears to be like, we are able to configure the browser window to have a particular background colour after we create it. So let’s go to our foremost.js file and add a background colour. Change your mainWindow so it appears to be like like this:

const mainWindow = new BrowserWindow({
  width: 800,
  peak: 600,
  backgroundColor: "#263238",
});

And now once you begin, the flash of white must be gone!

Saving our scratchpad on disk

Once I defined Electron earlier on this article, I made it just a little less complicated than it’s. Whereas Electron has a foremost and a renderer course of, lately there’s truly been a 3rd context, which is the preload script.

The concept behind the preload script is that it acts as a bridge between the principle (which may entry all of the Node.js APIs) and the renderer (which ought to undoubtedly not!). In our preload script we are able to add features that may speak to the principle course of, after which expose them to the renderer course of in such a manner that it doesn’t affect the safety of our utility.

So let’s get an outline of what we wish to do:

  • When the consumer makes a change, we wish to reserve it to the disk.
  • When the appliance launches, we wish to load again in that saved content material from disk, and ensure it exhibits in our CodeMirror editor.

First, we’ll write code that lets us load and retailer content material to disk in our foremost.js file. That file already imports Node’s path module, however we additionally must import fs to do issues with the file system. Add this to the highest of the file:

const fs = require('fs');

Then, we’ll want to decide on a location for our saved textual content file. Right here, we’re going to make use of the appData folder, which is an mechanically created place to your app to retailer data. You will get it with the app.getPath characteristic, so let’s add a filename variable to the foremost.js file proper earlier than the createWindow perform:

const filename = `${app.getPath('userData')}/content material.txt`;

After that, we’re going to want two features: one to learn the file and one to retailer the file. We’ll name them loadContent and saveContent, and right here’s what they appear like:

const loadContent = async () => {
  return fs.existsSync(filename) ? fs.readFileSync(filename, 'utf8') : '';
}

const saveContent = async (content material) => {
  fs.writeFileSync(filename, content material, 'utf8');
}

They’re each one-liners utilizing the built-in fs strategies. For loadContent, we first must verify if the file already exists (because it gained’t be there the primary time we launch it!) and if it doesn’t, we are able to return an empty string.

saveContent is even less complicated: when it’s known as, we name writeFile with the filename, the content material, and we be sure that it’s saved as UTF8.

Now that we now have these features, we have to hook them up. And the best way to speak these is thru IPC, Inter Course of Communication. Let’s set that up subsequent.

Organising IPC

First, we have to import ipcMain from Electron, so be sure that your require('Electron') line in foremost.js appears to be like like this:

const { app, BrowserWindow, ipcMain } = require('electron');

IPC permits you to ship messages from the renderer to foremost (and the opposite manner round). Proper under the saveContent perform, add the next:

ipcMain.on("saveContent", (e, content material) =>{
  saveContent(content material);
});

After we obtain a saveContent message from the renderer, we name the saveContent perform with the content material we bought. Fairly easy. However how can we name that perform? That’s the place issues get just a little sophisticated.

We don’t need the renderer file to have entry to all of this, as a result of that will be very unsafe. We have to add in an middleman that may speak with the foremost.js file and the renderer file. That’s what a preload script can do.

Let’s create that preload.js file within the src listing, and hyperlink it in our mainWindow like so:

const mainWindow = new BrowserWindow({
  width: 800,
  peak: 600,
  backgroundColor: "#263238",
  webPreferences: {
    preload: path.be a part of(__dirname, 'preload.js'),
  }
});

Then in our preload script we’ll add the next code:

const { ipcRenderer, contextBridge } = require("electron");

contextBridge.exposeInMainWorld(
  'scratchpad',
  {
    saveContent: (content material) => ipcRenderer.ship('saveContent', content material)
  }
)

contextBridge.exposeInMainWorld lets us add a perform saveContent in our renderer.js file with out making the entire of Electron and Node accessible. That manner, the renderer solely is aware of about saveContent with out understanding how, or the place, the content material is saved. The primary argument, “scratchpad”, is the worldwide variable that saveContent can be accessible in. To name it in our React app, we do window.scratchpad.saveContent(content material);.

Let’s do this now. We open our app.jsx file and replace the updateScratchpad perform like this:

const updateScratchpad = newValue => {
  window.scratchpad.saveContent(newValue);
};

That’s it. Now each change we make is written to disk. However after we shut and reopen the appliance, it’s empty once more. We have to load within the content material after we first begin as effectively.

Load the content material after we open the app

We’ve already written the loadContent perform in foremost.js, so let’s hook that as much as our UI. We used IPC ship and on for saving the content material, since we didn’t must get a response, however now we have to get the file from disk and ship it to the renderer. For that, we’ll use the IPC invoke and deal with features. invoke returns a promise that will get resolved with regardless of the deal with perform returns.

We’ll start with writing the handler in our foremost.js file, proper under the saveContent handler:

ipcMain.deal with("loadContent", (e) => {
  return loadContent();
});

In our preload.js file, we’ll invoke this perform and expose it to our React code. To our exporeInMainWorld listing of properties we add a second one known as content material:

contextBridge.exposeInMainWorld(
  'scratchpad',
  {
    saveContent: (content material) => ipcRenderer.ship('saveContent', content material),
    content material: ipcRenderer.invoke("loadContent"),
  }
);

In our app.jsx we are able to get that with window.scratchpad.content material, however that’s a promise, so we have to await it earlier than loading. To try this, we wrap the ReactDOM renderer in an async IFFE like so:

(async () => {
  const content material = await window.scratchpad.content material;
  ReactDOM.render(<ScratchPad textual content={content material} />, doc.physique);
})();

We additionally replace our ScratchPad element to make use of the textual content prop as our beginning worth:

const ScratchPad = ({textual content}) => {
  const choices = {
    theme: "materials"
  };

  const updateScratchpad = newValue => {
    window.scratchpad.saveContent(newValue);
  };

  return (
    <CodeMirror
      worth={textual content}
      onChange={updateScratchpad}
      choices={choices}
    />
  );
};

There you have got it: we’ve efficiently built-in Electron and React and created a small utility that customers can sort in, and that’s mechanically saved, with out giving our scratchpad any entry to the file system that we don’t wish to give it.

We’re accomplished, proper? Properly, there’s just a few issues we are able to do to make it look just a little bit extra “app” like.

“Quicker” Loading

You may need observed that, once you open the app, it takes just a few moments earlier than the textual content is seen. That doesn’t look nice, so it will be higher to attend for the app to have loaded, and solely then present it. It will make the entire app really feel sooner, because you gained’t be an inactive window.

First, we add present: false to our new BrowserWindow invocation, and add a listener to the ready-to-show occasion. There we present and focus our created window:

const mainWindow = new BrowserWindow({
  width: 800,
  peak: 600,
  backgroundColor: "#263238",
  present: false,
  webPreferences: {
    preload: path.be a part of(__dirname, 'preload.js'),
  }
});

mainWindow.as soon as('ready-to-show', () => {
  mainWindow.present();
  mainWindow.focus();
});

Whereas we’re within the foremost.js file, we’ll additionally take away the openDevTools name, since we don’t wish to present that to customers:

mainWindow.webContents.openDevTools();

After we now begin the appliance, the app window exhibits with the content material already there. A lot better!

Constructing and Putting in the Utility

Now that the appliance is finished, we are able to construct it. Electron Forge already has created a command for this. Run npm run make and Forge will construct an app and installer to your present working system and place it within the “out” folder, all prepared so that you can set up whether or not its an .exe, .dmg or .deb.

In the event you’re on Linux and get an error about rpmbuild, set up the “rpm” bundle, for instance with sudo apt set up rpm on Ubuntu. In the event you don’t wish to make an rpm installer, you can even take away the “@electron-forge/maker-rpm” block from the makers in your bundle.json.

It will miss some important issues like code signing, notarization and auto updates, however we’ll depart these for a later article.

It is a actually minimal instance of integrating Electron and React. There’s rather more we are able to do with the appliance itself. Listed below are some concepts so that you can discover:

  • Add a cool desktop icon.
  • Create darkish and lightweight mode assist based mostly on the working system settings, both with media queries or through the use of the nativeTheme api supplied by Electron.
  • Add shortcuts with one thing like mousetrap.js or with Electron’s menu accelerators and globalShortcuts.
  • Retailer and restore the dimensions and place of the window.
  • Sync with a server as a substitute of a file on disk.

And don’t overlook, yow will discover the completed utility on GitHub.

Click to comment

Leave a Reply

Your email address will not be published. Required fields are marked *