├── .gitignore
├── README.md
├── docs
└── guide.md
├── index.html
├── main.js
├── package.json
├── src
└── entry.js
├── static
└── sass
│ └── main.scss
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # NPM packages
2 | node_modules
3 |
4 | # General ignored folder
5 | ignore
6 |
7 | # Webpack build directory
8 | build
9 |
10 | # Packaged app
11 | bin
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Electron React Base App
2 | -----------------------
3 |
4 | Starting point for an Electron + React + Sass application.
--------------------------------------------------------------------------------
/docs/guide.md:
--------------------------------------------------------------------------------
1 | # Building apps with Electron, React and Sass
2 |
3 | Creating desktop applications with web technologies has always been a little dream of mine. With GitHub's Electron project, it's now easier than ever. Even so, there are a few gotchas along the way. In this write-up I go through some of the lessons I learned in a recent project.
4 |
5 | ## What you'll learn:
6 |
7 | 1. Create a basic Electron app with React.
8 | 2. Set up a live-reloading dev server with Webpack.
9 | 3. Add in Sass and have Webpack process it.
10 | 4. Package, code sign and distribute your app.
11 |
12 |
13 | ## Getting Started
14 |
15 | We'll be starting off with the example app provided in the [Electron Quick Start guide](http://electron.atom.io/docs/latest/tutorial/quick-start/). For the sake of brevity the code below is stripped of comments.
16 |
17 | ```js
18 | var app = require('app');
19 | var BrowserWindow = require('browser-window');
20 |
21 | require('crash-reporter').start();
22 |
23 | var mainWindow = null;
24 |
25 | app.on('window-all-closed', function() {
26 | if (process.platform != 'darwin') {
27 | app.quit();
28 | }
29 | });
30 |
31 | app.on('ready', function() {
32 | mainWindow = new BrowserWindow({width: 800, height: 600});
33 |
34 | mainWindow.loadUrl('file://' + __dirname + '/index.html');
35 |
36 | mainWindow.openDevTools();
37 |
38 | mainWindow.on('closed', function() {
39 | mainWindow = null;
40 | });
41 | });
42 | ```
43 |
44 | Save it as `main.js` in your project directory. This bootstraps an Electron application that loads an `index.html` file in the same directory as `main.js`. Let's create that `index.html`.
45 |
46 |
47 | ```js
48 |
49 |
50 |
51 | ` with id `react-root` that React can attach to.
121 |
122 | ```
123 |
124 |
125 |
126 |
Electron App
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 | ```
135 |
136 | Run `npm run start` to see it all spinning. The script `./node_modules/babel-core/browser.js` transpiles our JSX with Babel on the client.
137 |
138 |
139 | ## Bundling it up
140 |
141 | Having the JSX transpile at runtime is fine for testing it out and joshin' around, but it isn't particularly ideal in any real setting. Let's fix it and use Webpack to bundle up our JavaScript.
142 |
143 | Edit `index.html` and replace the two script tags with one single tag pointing at `./build/bundle.js`. In a moment we'll configure Webpack to output our JavaScript into this file.
144 |
145 | ```
146 |
147 |
148 |
149 |
Electron App
150 |
151 |
152 |
153 |
154 |
155 |
156 | ```
157 |
158 | Before installing Webpack from `npm`, we're going to create its configuration file. Below is `webpack.config.js` in its entirety.
159 |
160 | ```
161 | var webpack = require('webpack');
162 |
163 | module.exports = {
164 | context: __dirname + '/src',
165 | entry: './entry.js',
166 |
167 | output: {
168 | filename: 'bundle.js',
169 | path: __dirname + '/build'
170 | },
171 |
172 | module: {
173 | loaders: [
174 | { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ }
175 | ]
176 | }
177 | };
178 | ```
179 |
180 | Let's go through the file, it's just regular JavaScript (which means you can do all sorts of fancy JavaScripting if you feel like it).
181 |
182 | ```
183 | // webpack.config.js
184 | module.exports = { /* ... */ };
185 | ```
186 |
187 | This exports the configuration object.
188 |
189 | ```
190 | context: __dirname + '/src',
191 | entry: './entry.js',
192 | ```
193 |
194 | The `entry` property is the entry point for Webpack when it starts bundling everything together. Everything that's required directly in this file, or in subsequently required files, will be processed by Webpack. This includes non-JavaScript as well, which we'll get to later when we include Sass styles.
195 |
196 | The `context` property is an absolute path. It's used when resolving the location of `entry`, and since our entry file is `./src/entry.js` we'll put `__dirname + '/src'` in the `context` property and `entry.js` in the `entry` property.
197 |
198 |
199 | ```
200 | output: {
201 | filename: 'bundle.js',
202 | path: __dirname + '/build'
203 | },
204 | ```
205 |
206 | The above instructs Webpack to output the file `bundle.js` in the path `__dirname + '/build`, which is what we wrote earlier in `index.html`.
207 |
208 | ```
209 | module: {
210 | loaders: [
211 | { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ }
212 | ]
213 | }
214 | ```
215 |
216 | Webpack supports a number of loaders for different file types. These are specified as an array of objects in `module.loaders`. The file type is matched by a regular expression, and when there's a match the file is processed by a loader.
217 |
218 | We can also ignore files with the `exclude` property. We don't want to bundle anything in the `node_modules` directory, so we exclude all JavaScript files from that folder by matching a regular expression.
219 |
220 | With `webpack.config.js` created, install Webpack (`npm install --save-dev webpack`) and run `./node_modules/.bin/webpack` in the project directory. This generates `./build/bundle.js`. To make running Webpack less tedious let's also add the command as an `npm` script.
221 |
222 | ```
223 | ...
224 | "scripts": {
225 | "start": "./node_modules/.bin/electron .",
226 | "build": "./node_modules/.bin/webpack"
227 | }
228 | ...
229 | ```
230 |
231 | Now run `npm run build && npm run start` and you should see the same `Hello from React!` page as in the previous section. However, now it's transpiled, bundled, and cooked so there's no real-time transpiling going on.
232 |
233 |
234 | ## Hot Reload Development Environment
235 |
236 | Webpack has a development server that detects updates to any files that are part of the bundle and automatically reloads those files. In fact, it's so fancy that it can replace only the modules that have been updated. Our example is not that fancy (but hey, feel free to go down that rabbit hole).
237 |
238 | First off we need to install the Webpack dev server with `npm install --save-dev webpack-dev-server`. With it installed you can run the live reloading dev server with the following command.
239 |
240 | ```
241 | ./node_modules/.bin/webpack-dev-server --hot --inline
242 | ```
243 |
244 | The dev server now continually builds the source files and serves them at `http://localhost:8080/`. In the name of consistency, let's change the `webpack.config.js` so that we serve the bundled files from `http://localhost:8080/build/` by adding a `publicPath` property to the `output` object.
245 |
246 | ```
247 | ...
248 | output: {
249 | filename: 'bundle.js',
250 | path: __dirname + '/build',
251 | publicPath: 'http://localhost:8080/build/'
252 | },
253 | ...
254 | ```
255 |
256 | We'll have to modify our project a tiny bit for this to work both development and production environments. In `index.html` we're still referring to the build output (`./build/bundle.js`) and not the dev server (`http://localhost:8080/build/bundle.js`). This is exactly what we want when packaging up the Electron app, but for development purposes we want it to look at the dev server. We'll make this happen by setting an environment variable as part of our `start` script in `package.json`.
257 |
258 | ```
259 | ...
260 | "start": "ENVIRONMENT=DEV ./node_modules/.bin/electron .",
261 | ...
262 | ```
263 |
264 | The environment variable `ENVIRONMENT` is set to `DEV` for the duration of the script command, which in this case is while the app is running. Since this is an Electron app and not a regular website, we can query for this variable in our `index.html`. If `process.env.ENVIRONMENT === 'DEV'` we point to the dev server's `bundle.js`.
265 |
266 | ```
267 | ...
268 |
269 |
270 |
283 |
284 | ...
285 | ```
286 |
287 | As in previous sections, let's add an `npm` script for running the dev server.
288 |
289 | ```
290 | ...
291 | "scripts": {
292 | "start": "ENVIRONMENT=DEV ./node_modules/.bin/electron .",
293 | "build": "./node_modules/.bin/webpack",
294 | "watch": "./node_modules/.bin/webpack-dev-server --hot --inline"
295 | }
296 | ...
297 | ```
298 |
299 | Now open two terminal windows and run `npm run watch` in one and `npm run start` in the other. As you make edits to `entry.js` or the files referenced from it, you'll see the Electron app update with the changes.
300 |
301 | _Note: Any changes to `main.js` or `index.html` will not automatically cause Webpack to live-reload. You will need to restart Webpack and the Electron app to see changes made in those files._
302 |
303 |
304 | ## Adding Sass
305 |
306 | Adding Sass styles is pretty simple using Webpack loaders. In `webpack.config.js`'s `module.loaders` we'll add a loader for pre-processing our Sass and then loading and applying it to our page (yes, we're letting Webpack add it to the document). First, install the loaders we need with `npm install --save-dev style-loader css-loader sass-loader`, and then use them in `webpack.config.js`.
307 |
308 | ```
309 | ...
310 | module: {
311 | loaders: [
312 | { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ },
313 | { test: /\.scss$/, loader: 'style-loader!css-loader!sass-loader' }
314 | ]
315 | }
316 | ...
317 | ```
318 |
319 | The syntax `style-loader!css-loader!sass-loader` will apply the `sass-loader`, `css-loader` and `style-loader` in right-to-left order to any `.scss` file that has been included in our JavaScript with `require()`. The `sass-loader` compiles the Sass markup to CSS, `css-loader` interprets and resolves `@import` and `url(...)` paths, and `style-loader` applies the CSS to the document. One nice thing to note is that Webpack will resolve any relative paths it encounters in the Sass markup.
320 |
321 | Now that we're prepped to load Sass, create a file `./static/styles/main.scss` in the project directory and `require()` it at the top of `./src/entry.js`.
322 |
323 | ```
324 | require('../static/sass/main.scss');
325 |
326 | var React = require('react');
327 | ...
328 | ```
329 |
330 | Run the project and you will see the styles from `main.scss` applied to the document. There are many other loaders out there, and if you for example prefer Less over Sass you'll easily replace `sass-loader` with `less-loader`.
331 |
332 |
333 | ## Extra: Package your Mac app
334 |
335 | Finally, you may wish to distribute your app. There's a command line tool called `electron-packager` that makes this process super simple. Install it (`npm install --save-dev electron-packager`) and add a `osx-package` script to your `package.json` scripts.
336 |
337 | ```
338 | ...
339 | "osx-package": "./node_modules/.bin/webpack -p && ./node_modules/electron-packager/cli.js ./ ElectronReactSass --out ./bin --platform=darwin --arch=x64 --version=0.34.0 --overwrite --ignore=\"ignore|bin|node_modules\""
340 | ...
341 | ```
342 |
343 | Unfortunately Apple does not allow Electron apps in the App Store, but you can still distribute it elsewhere. However, unless you code sign your app it will cause security warnings. Code signing is pretty simple process, but you will need a [Developer ID](https://developer.apple.com/developer-id/). In this app skeleton I've added the `npm` scripts `osx-sign` and `osx-verify` ([more on signing Electron apps](http://www.pracucci.com/atom-electron-signing-mac-app.html)).
344 |
345 | ```
346 | ...
347 | "osx-sign": "codesign --deep --force --verbose --sign \"
\" ./bin/ElectronReactSass-darwin-x64/ElectronReactSass.app",
348 |
349 | "osx-verify": "codesign --verify -vvvv ./bin/ElectronReactSass-darwin-x64/ElectronReactSass.app && spctl -a -vvvv ./bin/ElectronReactSass-darwin-x64/ElectronReactSass.app",
350 | ...
351 | ```
352 |
353 | Replace `` with your Developer ID and you should be ready to go. In other words, with these npm scripts in your `package.json`-file the following command should package your app, code sign it, and finally verify that your code signing went well.
354 |
355 | ```
356 | npm run osx-package && npm run osx-sign && npm run osx-verify
357 | ```
358 |
359 | All that's left now is have people download and use your Electron-powered app. Good luck!
360 |
361 | _--- Marcus Stenbeck / [@marcusstenbeck](http://twitter.com/marcusstenbeck) / [juxt.com](http://juxt.com/)_
362 |
363 | ## Links
364 |
365 | [This skeleton app on GitHub.](https://github.com/juxtinteractive/electron-react-sass)
366 |
367 | [Electron - Signing a Mac Application](http://www.pracucci.com/atom-electron-signing-mac-app.html)
368 |
369 | [electron-packager on GitHub](https://github.com/maxogden/electron-packager)
370 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Electron + React + Sass
5 |
6 |
7 |
8 |
22 |
23 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var app = require('app'); // Module to control application life.
4 | var BrowserWindow = require('browser-window'); // Module to create native browser window.
5 |
6 | // Report crashes to our server.
7 | require('crash-reporter').start();
8 |
9 | // Keep a global reference of the window object, if you don't, the window will
10 | // be closed automatically when the JavaScript object is garbage collected.
11 | var mainWindow = null;
12 |
13 | // Quit when all windows are closed.
14 | app.on('window-all-closed', function() {
15 | // On OS X it is common for applications and their menu bar
16 | // to stay active until the user quits explicitly with Cmd + Q
17 | if (process.platform != 'darwin') {
18 | app.quit();
19 | }
20 | });
21 |
22 | // This method will be called when Electron has finished
23 | // initialization and is ready to create browser windows.
24 | app.on('ready', function() {
25 | // Create the browser window.
26 | mainWindow = new BrowserWindow({width: 800, height: 600});
27 |
28 | // and load the index.html of the app.
29 | mainWindow.loadUrl('file://' + __dirname + '/index.html');
30 |
31 | // Only open dev tools in dev environment
32 | if(process.env.ENVIRONMENT === 'DEV') {
33 | // Open the DevTools.
34 | mainWindow.openDevTools();
35 | }
36 |
37 | // Emitted when the window is closed.
38 | mainWindow.on('closed', function() {
39 | // Dereference the window object, usually you would store windows
40 | // in an array if your app supports multi windows, this is the time
41 | // when you should delete the corresponding element.
42 | mainWindow = null;
43 | });
44 | });
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "electron-react",
3 | "version": "0.1.0",
4 | "author": "Marcus Stenbeck ",
5 | "main": "main.js",
6 | "devDependencies": {
7 | "babel-core": "^5.8.25",
8 | "babel-loader": "^5.3.2",
9 | "css-loader": "^0.21.0",
10 | "electron-packager": "^5.1.0",
11 | "electron-prebuilt": "^0.34.1",
12 | "react": "^0.14.0",
13 | "react-dom": "^0.14.0",
14 | "sass-loader": "^3.0.0",
15 | "style-loader": "^0.13.0",
16 | "webpack": "^1.12.2",
17 | "webpack-dev-server": "^1.12.1"
18 | },
19 | "scripts": {
20 | "start": "ENVIRONMENT=DEV ./node_modules/.bin/electron .",
21 | "build": "./node_modules/.bin/webpack",
22 | "watch": "./node_modules/.bin/webpack-dev-server --hot --inline",
23 | "osx-package": "./node_modules/.bin/webpack -p && ./node_modules/electron-packager/cli.js ./ ElectronReactSass --out ./bin --platform=darwin --arch=x64 --version=0.34.0 --overwrite --ignore=\"ignore|bin|node_modules\""
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/entry.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | require('../static/sass/main.scss');
3 |
4 | var React = require('react');
5 | var ReactDom = require('react-dom');
6 |
7 | var App = React.createClass({
8 | render: function() {
9 | return ;
10 | }
11 | });
12 |
13 | ReactDom.render(, document.getElementById('react-root'));
--------------------------------------------------------------------------------
/static/sass/main.scss:
--------------------------------------------------------------------------------
1 | body {
2 | display: flex;
3 | align-items: center;
4 | flex-direction: column;
5 | justify-content: space-around;
6 |
7 | margin: 0;
8 | padding: 0;
9 |
10 | min-height: 100vh;
11 |
12 | background: linear-gradient(-8deg, #FF4E50, #F9D423);
13 |
14 | text-shadow: 0 0.05em 0 rgba(0,0,0,0.1);
15 |
16 | font-size: 8vw;
17 | font-family: sans-serif;
18 | }
19 |
20 | #react-root {
21 | text-align: center;
22 | }
23 |
24 | .tech {
25 | padding: 0.4em 0.6em 0.4em;
26 | border-radius: 0.2em;
27 | box-shadow: 0 0.05em 0 rgba(0,0,0,0.1);
28 | }
29 |
30 | .electron {
31 | color: #45828E;
32 | background-color: #A5ECFA;
33 | }
34 |
35 | .react {
36 | color: #61dafb;
37 | background-color: #222;
38 | }
39 |
40 | .sass {
41 | color: #c6538c;
42 | background-color: #ffffff;
43 | }
44 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var path = require('path');
3 |
4 | module.exports = {
5 | context: path.join(__dirname, '/src'),
6 | entry: './entry.js',
7 |
8 | output: {
9 | filename: 'bundle.js',
10 | path: path.join(__dirname, '/build'),
11 | publicPath: 'http://localhost:8080/build/'
12 | },
13 |
14 | module: {
15 | loaders: [
16 | { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ },
17 | { test: /\.scss$/, loader: 'style-loader!css-loader!sass-loader' }
18 | ]
19 | }
20 | };
21 |
--------------------------------------------------------------------------------