Browse code

New article: Integrate native Node.js modules into an Electron app (1/2)

Cinan Rakosnik authored on 22/02/2018 at 20:35:09
Showing 5 changed files
... ...
@@ -4,7 +4,7 @@
4 4
 
5 5
 url: http://blog.cinan.sk
6 6
 title: Cinan's world
7
-keywords: blog, linux, web developer, servers, configuration, geek, nerd, cinan
7
+keywords: blog, linux, web developer, frontend, javascript, servers, configuration, geek, nerd, cinan
8 8
 author: cinan
9 9
 simple_search: https://google.com/search
10 10
 description: GNU/Linux & free software, howtos, web development, scripts and other geek stuff
11 11
new file mode 100644
... ...
@@ -0,0 +1,214 @@
0
+---
1
+layout: post
2
+title: "Integrate native Node.js modules into an Electron app (1/2)"
3
+date: 2018-02-22 21:12
4
+comments: true
5
+categories: [javascript]
6
+cover: /images/cover/avatar.png
7
+keywords: 
8
+description: 
9
+---
10
+
11
+### tl;dr
12
+
13
+- when you probably really need Electron
14
+- how to integrate Webpack with Electron
15
+- develop (and test) browser and Electron app
16
+- Electron vs. system-wide node.js
17
+
18
+### OMG, another Electron freak!
19
+
20
+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](https://electronjs.org/) 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.
21
+
22
+# When you may need Electron
23
+
24
+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.
25
+
26
+# Goal 
27
+
28
+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](https://github.com/webpack/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.
29
+
30
+<!-- more -->
31
+
32
+# Prerequisites and project structure
33
+
34
+Install [node.js](https://nodejs.org) with npm and [Electron](https://electronjs.org/). MacOS and Linux is OK, Windows should be supported too.
35
+
36
+```
37
+├── app.js
38
+├── app.test.js
39
+├── build
40
+│   └── ...
41
+├── index.electron.js
42
+├── index.html
43
+├── node_modules
44
+│   └── ...
45
+├── package.json
46
+├── release-builds
47
+│   └── ...
48
+└── webpack.config.js
49
+```
50
+
51
+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.
52
+
53
+Paste this into `package.json` and run `npm install`:
54
+
55
+{% codeblock package.json lang:json %}
56
+{
57
+  "name": "electron-tutorial",
58
+  "main": "index.electron.js",
59
+  "scripts": {
60
+    "build": "webpack",
61
+    "electron": "electron .",
62
+    "test": "jest"
63
+  },
64
+  "dependencies": {
65
+    "electron": "^1.8.2",
66
+    "electron-rebuild": "^1.7.3",
67
+    "jest": "^22.4.0",
68
+    "serialport": "^6.0.5",
69
+    "webpack": "^3.11.0"
70
+  }
71
+}
72
+{% endcodeblock %}
73
+
74
+# Let's start with Webpack
75
+
76
+Paste this into `webpack.config.js`:
77
+
78
+{% codeblock webpack.config.js lang:javascript %}
79
+const webpack = require('webpack');
80
+const path = require('path');
81
+
82
+const platform = process.env.PLATFORM;
83
+
84
+module.exports = {
85
+  entry: {
86
+    app: path.join(__dirname, 'app.js')
87
+  },
88
+  output: {
89
+    path: path.join(__dirname, 'build'),
90
+    filename: '[name].js',
91
+    publicPath: 'build/',
92
+  },
93
+  plugins: [
94
+    new webpack.DefinePlugin({
95
+      'process.env.IS_ELECTRON': JSON.stringify(platform === 'electron'),
96
+    })
97
+  ],
98
+  target: platform === 'electron' ? 'electron-renderer' : 'web',
99
+  externals: {
100
+    bindings: 'require("bindings")' // fixes warnings during build
101
+  }
102
+};
103
+{% endcodeblock %}
104
+
105
+You probably already know Webpack. What's critical in configuration below is `target` option.
106
+
107
+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.
108
+
109
+Also notice `process.env.IS_ELECTRON` definition. This way we can easily execute parts of javascript in Electron only.
110
+
111
+# Develop
112
+
113
+`app.js`, `index.html` and `index.electron.js` code looks like this:
114
+
115
+{% codeblock app.js lang:javascript %}
116
+if (process.env.IS_ELECTRON) {
117
+  const serialport = require('serialport');
118
+  // will show connected serialport devices if any
119
+  serialport.list().then(list => console.log(list));
120
+  alert('Electron detected');
121
+} else {
122
+  alert('Browser detected');
123
+}
124
+{% endcodeblock %}
125
+
126
+Notice the `if` clause; the code inside the clause will be executed only if the bundle was built for Electron environment.
127
+
128
+{% codeblock index.html lang:html %}{% raw %}
129
+<!doctype html>
130
+<html lang="en">
131
+<head>
132
+  <meta charset="UTF-8">
133
+  <title>Node.js native modules</title>
134
+  <script src="build/app.js"></script>
135
+</head>
136
+<body>
137
+  Running.
138
+</body>
139
+</html>
140
+{% endraw %}{% endcodeblock %}
141
+
142
+{% codeblock index.electron.js lang:javascript %}
143
+const { app, BrowserWindow } = require('electron');
144
+
145
+let mainWindow;
146
+
147
+function createWindow() {
148
+  mainWindow = new BrowserWindow();
149
+
150
+  mainWindow.loadURL(`file:///${__dirname}/index.html`);
151
+  mainWindow.webContents.openDevTools();
152
+
153
+  mainWindow.on('closed', () => {
154
+    mainWindow = null;
155
+  });
156
+}
157
+
158
+app.on('ready', createWindow);
159
+
160
+app.on('window-all-closed', () => {
161
+  app.quit();
162
+});
163
+
164
+app.on('activate', () => {
165
+  if (mainWindow === null) {
166
+    createWindow();
167
+  }
168
+});
169
+{% endcodeblock %}
170
+
171
+There's nothing special in `index.html`. `index.electron.js` is (almost) a vanilla Electron configuration.
172
+
173
+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.
174
+
175
+{% imgcap /images/electron1.png Browser screenshot %}
176
+
177
+Now run Webpack in Electron mode: `PLATFORM=electron npm run build`. Open the `index.html` file with Electron: `npm run electron`. 
178
+
179
+{% imgcap /images/electron2.png Cannot find native module %}
180
+
181
+Most native node.js modules are loaded via [node-bindings](https://github.com/TooTallNate/node-bindings) library. The library tries to find native module in `build` directory.
182
+
183
+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/`.
184
+
185
+If the module is missing build it with `./node_modules/.bin/electron-rebuild -e node_modules/electron` and copy to `build/` directory. 
186
+
187
+Try Electron again with previous npm command. Now you're actually scanned your serial ports thanks to node.js native module!
188
+
189
+{% imgcap /images/electron3.png Native module in action %}
190
+
191
+# Living with two node.js environments
192
+
193
+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
194
+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.
195
+
196
+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).
197
+
198
+# Testing
199
+
200
+Let's write a simple test which actually `require()` serialport dependency:
201
+
202
+```js
203
+test('i can require serialport', () => {
204
+  const serialport = require('serialport');
205
+  expect(serialport).toBeDefined();
206
+});
207
+```
208
+
209
+This will load native module in `node_modules/serialport/build/Release/serialport.node` and the test will pass. Run tests with `npm run test`.
210
+
211
+# What's next
212
+
213
+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.
0 214
\ No newline at end of file
1 215
new file mode 100644
2 216
Binary files /dev/null and b/source/images/electron1.png differ
3 217
new file mode 100644
4 218
Binary files /dev/null and b/source/images/electron2.png differ
5 219
new file mode 100644
6 220
Binary files /dev/null and b/source/images/electron3.png differ