Cinan's world

GNU/Linux & free software, howtos, web development, scripts and other geek stuff

Integrate Native Node.js Modules Into an Electron App (1/2)

tl;dr

  • when you probably really need Electron
  • how to integrate Webpack with Electron
  • develop (and test) browser and Electron app
  • Electron vs. system-wide node.js

OMG, another Electron freak!

Yeah yeah, I hear you saying Why don’t you learn Swift/C#/C++/…, Electron is so much memory intensive etc. I know you’re there, Electron haters. Well, in my opinion it’s super convenient to develop web application and have a possibility to run it inside Electron (with a great advantage—access to underlying node.js). Personally, it’s been thrilling to communicate with node.js from Electron (I’m kind of passionate developer). Sure, you can feel no Electron app is native, but that’s what trade-off is about.

When you may need Electron

A simple rule: if your project has a node.js dependency (meaning, the dependency is working only in node.js environment, not in a browser), you need Electron. What Electron basically does is running your Javascript in Chromium with node.js environment. Imagine you can do your usual front-end stuff and also you can control for example serial port (or USB port) peripherals in the same fashion, in the same project, even in the same file.

Goal

I’ll show you how to communicate with serialport device from your Electron app. First, we’ll create a project and install dependencies. Then Webpack needs to be configured. After this step we can finally run the app. The demo project will be fully functional in Electron and will gracefully degrade in a browser.

Prerequisites and project structure

Install node.js with npm and Electron. MacOS and Linux is OK, Windows should be supported too.

1
2
3
4
5
6
7
8
9
10
11
12
├── app.js
├── app.test.js
├── build
│   └── ...
├── index.electron.js
├── index.html
├── node_modules
│   └── ...
├── package.json
├── release-builds
│   └── ...
└── webpack.config.js

Actual app code is inside app.js; we will write some tests in app.test.js; built bundle inside build directory; Electron main file is index.electron.js; our webpage index file is index.html; package.json contains list of dependencies and scripts; into release-builds we will pack whole Electron ready for distribution (in the next article); and finally webpack.config.js serves as Webpack config file.

Paste this into package.json and run npm install:

package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "name": "electron-tutorial",
  "main": "index.electron.js",
  "scripts": {
    "build": "webpack",
    "electron": "electron .",
    "test": "jest"
  },
  "dependencies": {
    "electron": "^1.8.2",
    "electron-rebuild": "^1.7.3",
    "jest": "^22.4.0",
    "serialport": "^6.0.5",
    "webpack": "^3.11.0"
  }
}

Let’s start with Webpack

Paste this into webpack.config.js:

webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const webpack = require('webpack');
const path = require('path');

const platform = process.env.PLATFORM;

module.exports = {
  entry: {
    app: path.join(__dirname, 'app.js')
  },
  output: {
    path: path.join(__dirname, 'build'),
    filename: '[name].js',
    publicPath: 'build/',
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env.IS_ELECTRON': JSON.stringify(platform === 'electron'),
    })
  ],
  target: platform === 'electron' ? 'electron-renderer' : 'web',
  externals: {
    bindings: 'require("bindings")' // fixes warnings during build
  }
};

You probably already know Webpack. What’s critical in configuration below is target option.

You need to set target to electron-renderer if you’re building code for Electron. Long story short—this setting will enable node.js and browser environment in your bundle.

Also notice process.env.IS_ELECTRON definition. This way we can easily execute parts of javascript in Electron only.

Develop

app.js, index.html and index.electron.js code looks like this:

app.js
1
2
3
4
5
6
7
8
if (process.env.IS_ELECTRON) {
  const serialport = require('serialport');
  // will show connected serialport devices if any
  serialport.list().then(list => console.log(list));
  alert('Electron detected');
} else {
  alert('Browser detected');
}

Notice the if clause; the code inside the clause will be executed only if the bundle was built for Electron environment.

index.html
1
2
3
4
5
6
7
8
9
10
11
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Node.js native modules</title>
  <script src="build/app.js"></script>
</head>
<body>
  Running.
</body>
</html>
index.electron.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const { app, BrowserWindow } = require('electron');

let mainWindow;

function createWindow() {
  mainWindow = new BrowserWindow();

  mainWindow.loadURL(`file:///${__dirname}/index.html`);
  mainWindow.webContents.openDevTools();

  mainWindow.on('closed', () => {
    mainWindow = null;
  });
}

app.on('ready', createWindow);

app.on('window-all-closed', () => {
  app.quit();
});

app.on('activate', () => {
  if (mainWindow === null) {
    createWindow();
  }
});

There’s nothing special in index.html. index.electron.js is (almost) a vanilla Electron configuration.

All right, run Webpack with npm run build and open index.html in your browser (e.g. file:///Users/joedoe/electron/index.html). You should see something like on the screenshot below.

Browser screenshot

Now run Webpack in Electron mode: PLATFORM=electron npm run build. Open the index.html file with Electron: npm run electron.

Cannot find native module

Most native node.js modules are loaded via node-bindings library. The library tries to find native module in build directory.

We need to copy serialport.node to build/. If you already have a prebuilt module in node_modules/serialport/build/Release/serialport.node just copy it: cp node_modules/serialport/build/Release/serialport.node build/.

If the module is missing build it with ./node_modules/.bin/electron-rebuild -e node_modules/electron and copy to build/ directory.

Try Electron again with previous npm command. Now you’re actually scanned your serial ports thanks to node.js native module!

Native module in action

Living with two node.js environments

There’s a chance you’ll eventually see an error message The module ... was compiled against a different Node.js version using NODE_MODULE_VERSION X. This version of Node.js requires NODE_MODULE_VERSION Y. In that case just run the ./node_modules/.bin/electron-rebuild -e node_modules/electron command. The reason why it might happen is Electron node.js usually slightly differs from the system-wide node.js version. In other words, Electron environment doesn’t depend on system-wide node.js.

My strategy is to keep native modules built against Electron in build/ directory and (if needed, because of tests e.g.) modules built against system-wide node.js in node_modules (use npm rebuild command).

Testing

Let’s write a simple test which actually require() serialport dependency:

1
2
3
4
test('i can require serialport', () => {
  const serialport = require('serialport');
  expect(serialport).toBeDefined();
});

This will load native module in node_modules/serialport/build/Release/serialport.node and the test will pass. Run tests with npm run test.

What’s next

We can develop the javascript app (although very primitive) for both browser and Electron. In the next article we’ll pack Electron environment with javascript bundle into a ready-for-distribution executable package.

Comments