Building an Electron app from scratch (Part 2)
- Part 1: Introducing Electron, starting with npm, creating a browser window and adding Typescript
- Part 2: Creating the app website, adding react, and bundling with webpack
- Part 3: Integrating SASS into the build and consuming a UI toolkit
- Part 4: Using JSON-RPC as an easy way to reuse code from other languages
In the previous part we brought up an electron window and displayed a webpage, but if we’re just going to display something from the internet then our electron app is going to be rather pointless. In the next few parts we’ll start turning this into a real desktop application.
It’s time to look at the other half of electron: the website that’s actually going to implement our UI. Eventually we’ll build a fully-fledged single-page application. For now, let’s just start with some simple HTML and make it more complicated as we go:
We also need to change our electron code to load the file instead of example.com:
window.loadURL(`file://${__dirname}/../website/index.html`);
Since electron is running from out/electron/index.js
, we load index.html
relative to that path. We just need to copy our website from src/website/index.html
to out/website/index.html
. For now we’ll add a quick copy command to our build script in package.json
:
"build": "tsc && cp ./src/website/index.html
./out/website/index.html",
And a quick npm run start
shows off the new content:
Not very impressive, I’ll admit. Let’s make it a bit more interactive:
Now we’ve got a minimal page skeleton in HTML, and we can start writing typescript to turn it into a real interactive application. Note that we’ve created an index.ts
file but we reference index.js
in the html, since our copied html file needs to reference the compiled javascript from the out/
folder.
Building a website with React
For this project we’ll be using React, a javascript library for building rich user interfaces. I’ll try to avoid going into too much detail with React here, since there are plenty of other excellent resources available on the web.
The short version is that React lets us write pure functions which generate an immutable tree of how the application should look at a given point in time, called a virtual DOM. Rather than re-rendering the entire page, React then does the hard work of figuring out how to update the real DOM as efficiently as possible. React also gives us a bunch of tools for splitting our app into independent components and managing complexity, and in general does a pretty good job.
React is very popular right now, but it may be overkill for a lot of more traditional websites. In our case, since the goal is to create a highly-interactive desktop application, we’ll be leaning into React a lot and making full use of the tech available.
There’s a bunch of tweaks we need to make to our build process to get React up and running:
Firstly, JSX is a handy extension to javascript which means we can write out html-like templates for our components. While mixing html and javascript together like this sounds like a bad idea (and often is), it turns out to work quite well in this context. By convention, .js
files containing JSX get renamed to .jsx
, and .ts
files become .tsx
. We also need to transform the JSX into actual javascript, so we’ll change the jsx
setting in tsconfig.json
to 'react'
instead of 'preserve'
.
Secondly, we need to invoke React to actually get it to update the DOM. We do that with the ReactDOM.render(element, contents)
method, where element
is the part of the DOM we want to update (ie our getElementById('app')
variable) and contents
is the JSX we want to replace it with.
We’ll also quickly grab type definitions for react
and react-dom
. These will give us handy type-checking and code completion features, like we saw earlier with loadUrl
in electron:
> npm install --save-dev @types/react @types/react-dom
Finally, we need to pull in the React libraries. For now we’ll just grab them over the CDN, but we’ll be changing that in a moment. Here’s how it all looks:
Bundling javascript files together
We now have a problem: we need to figure out how to ship React along with our app. We’re currently referencing a copy of React from an online CDN, but we don’t really want to be dependent on an online resource for our desktop application to function.
As our app gets more complicated, we’ll also want to split it up into multiple files which reference each other, so we’ll need to figure out how to solve that problem as well.
We’ll use webpack, an all-in-one build tool, to solve these problems. For now we’re going to use it for some fairly basic tasks, but webpack configuration can get notoriously complicated.
The main job webpack will do for us (and the one it’s named after) is to bundle up our module references and package our own code along with React. Remember that module boilerplate that typescript turned our import * as React from "react";
calls into? Now that will actually call into a real module system.
We’ll need a few npm modules to get started:
> npm install --save-dev webpack webpack-cli ts-loader
> npm install --save react react-dom
webpack
and webpack-cli
provide the build tool. We need ts-loader
to integrate with the typescript compiler and make it part of the build process. We’re now pulling react
and react-dom
through npm instead of a hosted CDN.
Unlike some other build tools, where we define a chain of tasks which source code goes through, everything in webpack is configured in one place. Our very first webpack config looks like this:
We’ll replace the call to tsc
in the package.json
build script with webpack --config webpack.website.config.js
and run it. Our generated index.js
now jumps massively in size: it’s too big to show here, but you can see the full thing in this gist. You don’t need to understand every line, but you can sort of see the overall structure and how each library that we depend on has been inlined into a module system. Right at the bottom of the file you can see a string containing our compiled index.tsx
.
Note that the above webpack config only builds the website half of the application. Because the electron process runs in node while the website process runs in a browser context, some details like how the module system works will be different between the two, and so we need separate configs for each. Here’s the equivalent electron config:
Most of the content is the same: the main differences are the target: 'electron-main'
and node: { __dirname: false }
lines. The latter is required to make loading our local index.html
work, while the former affects some module loading settings: for example, our electron import in src/electron/index.ts
gets turned into a normal node require("electron")
call instead of being handled directly by webpack, and electron itself doesn’t get inlined into the bundle.
Currently webpack is only handling our typescript code, but we can also get it to do the index.html
copying for us as well. We could just use a simple file copy plugin, but actually we can get webpack to do something a bit more intelligent with html-webpack-plugin
. After installing the plugin with npm, we add this to our config:
const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = {
// [...]
plugins: [
new HtmlWebpackPlugin({
template: 'src/website/index.html',
}),
],
}
Now webpack will actually read in our index.html
file, automatically insert a script tag which calls the webpack entry point, and then write it to the output folder. This means we don’t have to know about the name of the entrypoint when we write the html, and we don’t need to include an extra copy step in our npm build script.
As a side note, webpack also has a --watch
parameter, so our build:watch
script carries on working as before.
And after all that effort:
In the next few parts we’ll look at adding styling, making our application more complex, and interacting with external capabilities to continue turning this into a real desktop application.