Writing Complete Apps in Electron

We create open-source because we love it, and we share our finding so everyone else can benefit as well.

Writing Complete Apps in Electron

Electron has been a common go-to framework for developing desktop applications, and in that time there have been many unexpected nuances. Many developers would find these issues, and then abruptly move away from Electron because of it. So we will discuss everything you need to write your apps in Electron, as well as show you some resources and tricks to go with it.

Electron in a Nutshell

Electron is a framework built on the Chrome V8 engine, so your apps in Electron run on a stripped down Chrome browser of sorts. With it you have the ability to build a web-app frontend , but also allowed to add to the lower-level with C++. Electron is quite common, and you probably used a handful of Electron applications without even knowing it. Visual Studio Code, Basecamp, and Discord, all use Electron as their framework. Basecamp and Discord use Electron to access the main app remotely, where VS Code hosts holds code locally.

Electron is fairly cut and dry, but issues start to arise in the execution. Your apps consist of a main process, and a renderer process for each viewable window. Both of these processes have differing functionality, as well as limitations in regards to performance. Until we discuss it later, the most important concept to remember is that Electron has two separate processes for handling your code.

From Development to Production

When you first build Electron for production you can often find a lot of parts of the app breaking. With that, it is important to understand that your app is compressed within a single file in the asar format. Within this file you will find an environment similar to your development build environment.

Handling Case Sensitive Files

When working with multiple platforms, you need to keep track of the nuances of each platform, and running into issues with casing is common. Before you even start writing your project, it’s best to decide how you plan to handle file and folder naming. Once you make your decision, still with it, otherwise making changes in larger projects can be a nightmare.

While Linux is the only case-sensitive platform (by default), you will need to reference every file by the exact casing of your file imports, keeping in mind that Git is also case-sensitive. When you change the casing of a file, you end up with a record for two different files within git.

To get around the issue of duplicate files in git, you need do the following:

  1. change the name back to the original casing
  2. remove the file from your project by moving to a directory outside the repository
  3. commit the file deletion change
  4. re-create the file with the new casing
  5. commit the file addition

You have to do all of that, just to change the casing of a file, and in the process, you lose access to history associated with original file.

Building for Production

It should be understood that testing Electron features on production builds require much more than simply using the production environment variable. While the production environment is fine for testing the bundle, it will not allow you to test the build. Many features can break in this process, and you could end up driving yourself crazy trying to make them work. Most of problems arise due to referenced external files, since they are accessed within the asar file.

File Handling in Production

One concept that throws people off, is the concept of the Electron resource files. The resources folder is used for files like images, docs, and any other file that cannot be packed directly into the asar file. Electron provides a direct resourcePath object in the process object, with the only catch being that this path can only be accessed in production. This means your app needs to dynamically change the resource path based on the build environment.

When using an icon, you will find the Electron docs recommend setting the icon using an absolute path. Realistically, you always want relative paths. An absolute path can be set dynamically to access icons, but it will not take long to find that this method takes more effort to work in Linux. Instead we can simply stick with relative paths.

The following conditional path setter works great for handling resources:

conditional resource path

This path can be used to access all external files we need access to within the app package. For a better example, you view your app’s scaffolding by unpacking the asar, and looking at the file layout.

$ asar extract dist/<platform>/<app name>/app.asar unpacked/

Electron Specific Files in the Asar

No matter your situation, there may be times when altering data in the asar could be beneficial. The issue that you will find, is that Linux doesn’t support the ability to write to files within to asar during runtime. You will have the ability to read, but not write to files.

One way around this particular problem is to use the local user temp directory. This solution introduces a new issue, as placement of the app file in OSX and Windows portable can cause this path to fail. It’s best to avoid the need to alter files, otherwise the temp directory is your best solution.

Icon Handling

Having that default electron app icon replaced with our own icon, is the badge of honor showing it is an app separate from Electron itself. We want to drop our icon images into our resource folder. In OSX and Linux we reference these files when initializing the BrowserWindow object. For Windows, an ICO icon can simply be placed in the resource folder. Tray icons will need to be placed there or an images resource folder. Once placed, referencing that relative path when initialing your Tray object.

Icon Styling

Using a single icon for each platform is a pretty bad idea, as that icon will look ok on one platform, but horrible on another. For OSX or Linux, you will want to follow the style guidelines so your icon looks like it belongs there. Following Apple’s styling guidelines for icons is a great way to cover OSX and Linux app icons. Use electron-icon-maker as a way to create all the sizes you need. With Windows, you need a square icon with minimal padding for it to look decent.

Frontend Styling

When you want to create a multi-platform application with Electron, you expect the application to look the same across each OS. Unfortunately this isn’t always the case. You will find that all platforms will display the frontend differently. For the most part it all comes down to the platform specific UI elements, elements like scrollbars and fonts throw off the size. The frameless option for OSX will cause similar differences as well.

This can all be annoying, but the reality is that you can work around it with CSS. Hiding the scrollbars will fix many common UI differences in Windows/Linux. For frameless, dynamically altering the app height in the BrowserWindow object will match the height across all platforms. Otherwise it was all about making sure CSS styling was reset to be as uniform as possible, though at first glance you can always see the font differences. As long as you use static sizes with pixel counts, or REM counts, you will be fine.

Local and Remote Apps in Electron

How you host your application determines the support you can offer your application. This is mostly from security concerns that could potentially put your users in danger. Let’s go over why this matters so much, and why your application structure makes such a big difference.

NodeJS Support

Locally hosted application, have the access to the NodeJS modules. This can give a wider availability of options when writing your application. There are some structural concerns that come with this compatibility (see Selective Automation). When planning to write a fully-fledged local application, NodeJS can be a great way to reduce your restrictions of functionality.

To enable node integration within Electron, add the following to each BrowserWindow object initialized within Electron:

  new BrowserWindow(Object.assign({
    webPreferences: {
      devTools: process.env.NODE_ENV === 'development',
      nodeIntegration: true
    },
  }, browserWindowOpts))

Hosting Remote Applications

One of the great advantages of using Electron to serve a remote application, is that you can break free of the platform restriction. For instance, apps like Basecamp who use Electron as a remote client, the app backend is handled with Ruby on Rails.

When using utilizing Electron this way, there are several security concerns to be aware of. The most important concern is the protection of remote code execution. For this topic it is extremely important that you become familiar with the Electron Security documentation.

Note: When using the Webpack Development Server, you will often find security warnings from Electron. If the end product loads a local file in production, you can ignore these warnings.

To avoid issues with remote applications, one important concept is to use the sandbox feature. With this code execution is isolated to the application itself. Next is the requirement of the remote host using SSL encryption. Last, avoid any sort of NodeJS Integration with remote applications. Leaving this enabled can give a malicious attacker the ability to interact directly with the local system, a feature you certainly do not want to offer.

If you cannot avoid NodeJS, a preload script allows you to inject your own local API exposing the modules you need without exposing the entire API. If you choose to use preload, it’s worth checking out the contextIsolation option. This causes the Electron API to be separated from the window and document scope, making it out of reach.

Prepping Your Bundles for Production

When writing an Electron app, it should be understood that you are theoretically writing a full-stack application. One part is the one the user interacts with (Frontend), and the other is the Electron Main Process script (Backend).

JavaScript Support

Before you start, Chrome V8 does not fully support ES6/ESNext. You may need to write the Electron Main Process script with ES5 module imports/exports.

Some developers will setup an Electron-based Webpack config with HMR to get around this, but realistically it is overkill. Once you write the Electron portion of your application, you may find yourself rarely changing it after that point. Except with heavy local Electron apps with a lot in the backend, I can count the number of times I’ve changed this script. It simply isn’t changed enough to justify a fully-fledged Webpack setup, and mostly for syntactical sugar.

The frontend application will always justify a Webpack setup, and all JS/Electron boilerplates will have the environment setup this way. This particular code will be used within the Electron Renderer process, so you will want to make sure it’s not top-heavy. The Electron renderer process simply isn’t made for intensive processes, so a lot of moving parts may work better in the main process.

When bundling for production, a separate webpack configuration for production is good practice. This config will strip out all of the comments, bundle the required JS modules, and transpile it all down to ES5. UglifyJS is still the defacto standard, but when transpiling ES6/ESNext, you need Terser Webpack Plugin to accomplish this.

Obfuscation the Open-Source Transpile-Compile

It is not uncommon to see creators wanting to make their Electron app as private as possible. Due to licensing you cannot make your app closed source, but you may want to protect your IP. In that case, you can use obfuscation to make code traversal a living nightmare. Doing so makes reverse-engineering the only possibility for anyone wanting to reuse your code.

The Webpack Obfuscator plugin is what you needed, but only in your production configuration. Using this plugin in development will make debugging errors worthless. To get an idea of what is happening to your code when obfuscated, open your bundle after a production bundle. All variable names are renamed to a generic naming scheme, parameters changed to hex codes, and when possible variables are also converted.

The only data that is really retained are hard-coded strings. This means if you use redux in your application, all of the reducer action filters are retained. If you use action filter constants, you will see every single one hoisted to the top of the bundle. If you want to be less transparent about your actions, it’s best to get rid of the constants, or use hashed strings, so it makes sense in development, but have arbitrary hoisted values:

TODO_ACTION_CREATE = '0x0001'
TODO_ACTION_EDIT   = '0x0002'
TODO_ACTION_DELETE = '0x0003'

Handling Local Storage

Earlier we discussed that writing directly to files within the asar is out of the question for the Linux platform. If you want to retain the state of your application, local storage is the way to go. This particular solution works fine, but you need to be aware of how to handle it.

Since it is nothing more than basic site storage, any data you save will go right into the user’s local data storage, and in plain text. This means if your application retains any sensitive data, you will need to encrypt the data before it is saved to local storage. I personally use crypto-js to encrypt my data, using a hardcoded private key to encrypt and decrypt the data.

Since the string data is retained in obfuscation, you want to break up the cipher key into multiple constants, and assemble them before the encryption/decryption process:

import 'Crypto' from 'crypto-js'

const secret = 'private'
const secret2 = 'cipher'

// secret and secret2 make up the full private key
export default class Crypt {
  static encrypt = data => {
    return Crypto.AES.encrypt(data, secret + secret2).toString()
  }

  static decrypt = data => {
    return data == undefined ? '' : Crypto.AES.decrypt(data.toString(), secret + secret2).toString(Crypto.enc.Utf8)
  }
}

With hundreds of thousands of variables in your application and even more when adding modules, it would be close to impossible for anyone to extract and re-assemble the two parts of the cipher. This makes for a great practice when the scenario is unavoidable.

If you want to take the hardcoded cipher encryption to a super-paranoid level, you can split the key into even more parts. You can also assign the different values to different sections of your application. This will cause the values to end up in different parts of the bundle when hoisted. You can also advantage of the ES6 Temporal Dead Zone, assigning your values using a let statement, or const statement. Doing so causes the values to remain uninitialized until they are accessed in runtime, and avoids hoisting. Placing your let/const values within separate closures will cause them to be placed deeper into the bundle, or you can place everything in a class object.

Working with Local Storage

One downfall to working with local storage, is that the same storage is used across all environments. If you have worked with multi-environment JS apps, you know you want to test each environment independently. Since local storage is shared across all environments, the state you save in development will also show in production. Instead you can set your middleware to save state based on the environment:

const Environment = process.env.NODE_ENV === 'production' ? 'production' : 'development'

export const loadState = () => {
  let config = JSON.parse(localStorage.getItem(`${Environment}-config`))
  let status = JSON.parse(localStorage.getItem(`${Environment}-status`))

  let persistedState = {}

  config !== null ? persistedState['config'] = config : null
  status !== null ? persistedState['status'] = status : null

  return persistedState
}

export const saveConfig = state => {
  const configState = JSON.stringify(state)
  localStorage.setItem(`${Environment}-config`, configState)
}

It is common to constantly save all app state, so when the app is restarted it starts exactly where it left off. Users will have the ability to close and refresh the app, as well other forces that interfere with external APIs. If any API calls are interrupted, it can make the app feel very unnatural. In some cases it can be poison the state. For instance, an API call is in the middle of a response when the app closes. When it starts back up, it continues to wait for a response for a request that is no longer active or exists. It the state is saved, the app could stay in a constant wait state.

You can avoid this by using a save state middleware that will save state after specific redux filters are triggered. This is a great way to keep request-reliant actions from getting stuck, and a great way of persisting state around API calls:

export const saveState = (state, section) => {
  const savedState = JSON.stringify(state)
  localStorage.setItem(`${Environment}-${section}`, savedState)
}

const middleware = () => store => next => action => {
  next(action)
  let currentState = store.getState()

  // always save this state
  saveStatus(currentState.status, 'status')

  switch(action.type) {
    case 'SAVE_CONFIG_SUCCESS':
      // only save config when saved, or it could
      //   persist even when the user clears it
      saveConfig(currentState.config, 'config')
      break
      ...

When we selectively handle state, we get closer to emulating a real desktop application. If we ignore this detail, we fall back into acting like another web-app.

Building Apps in Electron

You may find you prefer to use the feature-rich electron-builder to build your Electron projects. It may be overkill for some, but when writing local-hosted Electron apps you want all the features it offers. You can choose the build and resource files, sign and notarize binaries, and upload all published builds to a separate GH project. It is a good idea to consider if you plan to build for multiple platforms. A personal favorite feature is having a configuration in my package.json, which can be used for every build on all 3 major platforms.

package.json and the Electron Builder Build Settings

  ...
  "scripts": {
    ...
    "dist": "electron-builder",
    "pub": "electron-builder --p always",
    "build-icons": "electron-icon-maker -i ~/icon_location/icon.png -o assets/app-icon/",
    "postinstall": "electron-builder install-app-deps"
  },
  "dependencies": {
  ...
  },
  "build": {
    "appId": "com.electron.appName",
    "productName": "appName",
    "afterSign": "./scripts/notarize.js",  // used for OSX notary
    "afterPack": "./scripts/afterPack.js",
    "publish": [
      {
        "provider": "github",
        "repo": "some-repo",
        "owner": "GHuser",
        "private": true,
        "releaseType": "release",
        "publishAutoUpdate": true
      }
    ],
    "copyright": "Copyright Author",
    "files": [
      "build/**/*"
    ],
    "directories": {
      "buildResources": "assets/app-icon"
    },
    "extraResources": [
      "assets/",
      "images/",
      "appName.VisualElementsManifest.xml"
    ],
    "mac": {
      "category": "public.app-category",
      "darkModeSupport": true,
      "entitlements": "build/entitlements.mac.plist",
      "gatekeeperAssess": false,
      "hardenedRuntime": true,
      "target": "dmg"
    },
    "dmg": {
      "artifactName": "${productName}-${version}_OSX.${ext}",
      "contents": [
        {
          "x": 110,
          "y": 220
        },
        {
          "x": 420,
          "y": 220,
          "type": "link",
          "path": "/Applications"
        }
      ],
      "sign": true
    },
    "linux": {
      "category": "menu-category",
      "target": [
        "AppImage"
      ],
      "artifactName": "${productName}-${version}_Linux.${ext}",
      "desktop": {
        "Name": "appName",
        "Terminal": false
      }
    },
    "win": {
      "icon": "assets/app-icon/win/icon.ico",
      "target": [
        {
          "target": "nsis",
          "arch": [
            "x64",
            "ia32"
          ]
        },
        {
          "target": "portable",
          "arch": [
            "x64",
            "ia32"
          ]
        }
      ]
    },
    "nsis": {
      "artifactName": "${productName}-${version}_Win-Setup.${ext}"
    },
    "portable": {
      "artifactName": "${productName}-${version}_Win-Portable.${ext}"
    },
    "buildDependenciesFromSource": true
}

The above shows a great amount of the functionality available through Electron Builder. Here are a few options we set:

  • platform-specific configuration
  • build files for the asar
  • external resource files
  • native dependencies
  • naming for each build
  • add script hooks to run processes around the build process
  • the

Seeing what’s here will give you an idea of what’s available and easily cross-reference new options in the electron builder configuration docs. The most important being the options to include files. If you want to include any external files with your application, extraResources is where to get it done. The other file options are specifically for the build itself, which you can see from the file and buildFile settings.

Using the Tray with Caution

You may jump on the opportunity to add a TrayMenu, but with it comes a common issue. When this is enabled, your app will not close normally in certain platforms. Users will need to shut the client down from the TrayMenu, and users will not catch on quickly. When the client hasn’t been stopped, and the app is started again, another instance will be started. If you use local storage, you will find the second instance will fail to load that data.

This issue stems from the way the app is closed. We can instead listen for the close event, and then destroy the window when this event is triggered, causing the app to always close.

main.js (Electron)
  const windowList = []
  
  let win = BrowserWindow(windowOptions)

  ...

  win.on('close', e => {
    windowList.splice(windowList.indexOf(win), 1)

    e.preventDefault()
    win.destroy()
    app.quit()
  })

Code-Signing With Your App

When you receive an error opening an app regarding the origin of an application, you are using an application that has not been signed. A matter On OSX, users are given one of two options, Cancel, or Move to Trash. This requires the user to explicitly allow the app via the Gatekeeper. When signing your applications, you show your users that you value their security, and remove that annoyance in the process. Luckily the process of code-signing is extremely simple with electron-builder, so there is little excuse for overlooking this.

With OSX, code-signing requires an Apple Developer Account. For $99 a year, it’s easier to justify the more apps you have. As of OSX Catalina, users also receive an error regarding Malware. This error can be avoided with notarization, which also requires a developer account.

Portable Builds

You may have yet to appreciate the portable build, until you encounter a user who wants to use your app in an environment where installations are forbidden. The following targets work as portable builds:

  • OSX – DMG
  • Linux – AppImage
  • Windows – portable

With builds taken care of, you then want to verify that your users can use the app anywhere on a given system. If your app is looking to access files that are not included within the application, you will need to workaround that. In some cases with OSX and Linux, the system cannot determine the local user, and where the user’s directories exist. To get around this, you can use public system resources like the temp directory for your file stores.

Testing and Debugging Electron

No matter what you create in Electron, you want to test your application, both functionally and automatically. Let’s start with the necessities, then add some extensions to go with the V8 engine.

The devtron extension, is a must-have for any Electron developer, and offers excellent debugging. Next, if you need a Chrome extension like the React Dev Tools, you can use it from your local Chrome installation. Within the Electron main.js file, add a version-detecting import for the Chome extension. With this you never have to update the import when the extension is updated. After that, we enable Devtron:

const path = require('path')
const fs = require('fs')
const os = require('os')

...

if(process.env.NODE_ENV && process.env.NODE_ENV === 'development') {
  let devtoolsExt
  win.openDevTools()

let extensionHash = 'fmkadmapgofadopljbjfkapdkoienihi/'

    switch(process.platform) {
      case 'darwin':
        devtoolsExt = path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'Default', 'Extensions', extensionHash)
        break
      case 'win32':
        devtoolsExt = path.join(os.homedir(), 'AppData', 'Local', 'Google', 'Chrome', 'User Data', 'Default', 'Extensions', extensionHash)
        break
      case 'linux':
        devtoolsExt = path.join(os.homedir(), '.config', 'google-chrome', 'default', 'extensions', extensionHash)
        break
    }

  require('devtron').install()
}

The extension hash shown is for the React Dev Tools, so change to the extension you desire. The trailing backslash is required for the following readdir method call.

Any devtool that caches debug information, you will want be as performant as possible. Redux Dev Tools will cache every action for the rollback feature, so slowdowns can happen quickly. You can set a maximum action age to keep it from holding too many actions at once:

configureStore.dev.js
const store = createStore(
    rootReducer,
    {...initialState, ...persistedState},
    compose(applyMiddleware(...develMiddleware),
    DevTools.instrument({ maxAge: 60 }))
  );

Debugging with Devtron

When working with Electron, all debug functionality within the V8 process comes from Devtron. When you want to observe IPC calls, or watch event listeners, Devtron gives you that power. This means any time your processes communicate, you have the ability to debug that traffic, making sure it’s doing what it should. Most importantly, it also gives you direct event handlers for crashes, hangs, and exceptions on the Electron process itself. You also get a require graph for your windows, but when analysing bundles, use the webpack-bundle-analyzer.

Automated Testing

There is a good reason to test the main process, but for most apps, it’s really not worth it. The majority of the code within the Electron script comes from Electron, which is rarely touched once it’s written. It’s only when you add functionality with no direct relation to Electron, that you want to write tests around it. Even then, you can use a unit-test or two to test such functionality.

Handling tests around the biggest part of the application is most important, and that’s often the app in the renderer process(es). We can test these like we would any JS application. As mentioned before, there is an issue with testing using NodeJS integration. The problem arises when we decide to test our components, and that component has a NodeJS module in it. Electron is it’s own entity in this regard, so if you want to run end-to-end tests (a.k.a. e2e, integration) on that component, you will find it will fail when trying to include the NodeJS module.

For e2e tests you can use Spectron, a chrome-driver/web-driver testing platform for Electron. Lots of e2e tests can take a lot of time, so it’s isn’t feasible for running all of your tests. Let’s discuss another way to test our app.

Selective Automation

The biggest issue with testing with Electron, has always been to work around the NodeJS modules. We can take a more strategic and structurally sound route to avoid this. We need to be more explicit about where our NodeJS modules are imported and reside. The biggest problem is breaking the restrictions that comes with the NodeJS/Electron modules. Restricting NodeJS module imports to the lowest level components which only deal with logic, can help. Once it comes to testing those components, mock the modules, so tests can avoid them completely, and the errors that entail. With the visual components that need a NodeJS module, pass closures via props from the root component. This allows the visual component access, but keeping the module from being directly associated with it.

While there are other ways to go about it, using the root not only allows you to run unit-tests on the visual components, but keep them free to run e2e tests with whatever test runner you prefer. UI component libraries like Storybook, or StyleGuidist, will also be free of the module errors in this regard as well. The point being, module placement is critical for any library reusing these components.

Testing Electron with Cypress

The Cypress team has been working on an Electron-based implementation of Cypress for some time. This would certainly bring a more comfortable and quick environment to Electron e2e testing, giving a speedy test runner to the Electron environment. One big selling point is the ability to use it for one long e2e spec to test the entire app all at once, and quickly! If you are also interested in this project, check out the following post on their blog, and show your support on the following GitHub issue on the Cypress Electron branch.

Making the Most of Your Apps in Electron

Updating Electron

Allowing your app to offer automatic updates is a feature that offers great convenience to your users, but requires careful attention. Using the publish option from Electron Builder is what you need to start. The rest is setting up the Electron Builder Auto-Update events. Once added, it is important to note that you will need to test updates with a production build. While you can test the events to an extent in development, it requires the auto update xml files in the app build itself, something you cannot achieve outside of a production build.

Performance Handling

When it comes to handling resource intensive processes, you may find yourself instinctively placing your resource heavy scripts within the Renderer process. Doing this will quickly prove to be a taxing move on the performance. Similar to performance for a web-app in a browser, the renderer process is really only there to display, and has a limited amount of resources. Watching the performance monitor, if you see a function that’s too heavy, it may be time to move it somewhere else.

By default, larger scripts that demand large amount of the CPU and memory are better run within the main process. As long as the script is non-blocking, Electron will keep the script from interfering with performance by running in the background. The major downside comes with the need for the Inter-Process Communication, passing the data to and from the main process and back to the renderer. If you are passing a lot of data back and forth, this could become extremely inconvenient.

If placing your script in the main process isn’t feasible, you also have the option of placing that particular script within a Web Worker. These will place your script into its own NodeJS process, separate from Electron, with a lack of access to the Electron API. This will make your app a multi-threaded application, allowing memory corruption, and race conditions, but can be avoided with Thread Safety concerns.

Multi Window Applications

It is certainly easy to create a multi-window application app in Electron, but what if you want to communicate between these windows? Here is where we run into some short-comings with the V8 Engine. First off, there is no way to directly communicate between one window and another. Instead, we have to communicate through the main process, and have the main process contact the other window, all while keeping track of the ID of the calling window, so we know where to return the response.

To accomplish this task, we need to create our own message passing system, which you can get from our Window to Window Communication in Electron.

Native Modules

One of the most intimidating features is building native modules, but is actually an easier concept than it’s initially perceived. It’s important to keep in mind that native modules are incredibly rewarding additions. They bring great power to your applications, and why you can also find existing JS modules including native modules.

The need for native modules isn’t common, except when you plan to write your own C++ modules. Unless you are familiar with building C++ modules in general, I wouldn’t recommend digging too deep into this process. Regardless, we can take a look at some common requirements.

While it sometimes works out of the box, a lot of failures are due to dependency changes getting in the way. To avoid this, add a postinstall script to sync application dependencies any time a dependency changes.

"scripts": {
  ...
  "postinstall": "electron-builder install-app-deps"

When it comes to module building, there are several solutions to choose from. For this it’s suggested to read the Electron doc on Using Native Modules. The idea is that we are building platform-specific modules for each given platform, so we can offer these additional features across multiple platforms. Another common problem is building on a version of NodeJS that is incompatible, usually because it’s too new. If you can stay away from bleeding-edge versions of NodeJS, you can usually keep common build errors at bay.

The most obvious option is to build modules manually using node-gyp, and requires node-gyp to build.

$ yarn global add node-gyp

When a native module doesn’t work in your application, your first step should always be to rebuild electron, making sure it’s not a mixup of version dependencies. So install that locally for future use:

$ yarn add electron-rebuild -D

If your plan is to only grab and build pre-made native modules, the easiest option for accomplishing this is to use the node-pre-gyp package, which will help you install prebuilt native binaries, but has a limited selection of modules.

Introducing WASM

While native modules are great, there is another venue for introducing native modules, and that is through the use of WASM (WebAssembly). WebAssembly, allows for CPU/GPU heavy applications to be run within the browser. If you are familiar with the Unity game engine, you may be familiar with the ability to build games using WebGL in Unity 2018.1 and beyond. The engine uses WASM to allow graphic-intensive games to be played from the browser. Check out the Unity’s Angry Bots demo.

Note: Electron also offers WebGL support, it’s just a matter of flipping the switch in the BrowserWindow options.

Aside from the ability to access the system on a lower-level, the performance boost is also a great benefit of using WASM for parts of your application. When using WASM, it completely bypasses the JavaScript interpretor, in turn creating less of an overhead when being run. You don’t need to move all your code over to WASM, instead you can import your WASM modules using them like any other JavaScript class object.

Now for the more intimidating portion, the development process. When it comes to development in the Rust language, it is surprisingly pleasant for a static language. Not only is it a lot more forgiving when it comes to building native modules, it is outstanding about giving legible errors:

To try it out, the following tutorial will guide you through the process of creating a WASM app in Rust, as well as including it into a web-application. If you want to incorporate that application into an Electron app, take a look at this Electron-WASM example project. Really, the process is a lot easier that one would think, and a whole lot easier than building node native modules.

Summary

I know there is a lot to take in here, but Electron is more involved than one would originally imagine. For now, these are the most common use-cases to be aware of when working with Electron.

 

No Comments

Add your comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.