Electron

Originally named Atom Shell.
ESM has been supported since Electron 28.
Reference: https://github.com/Elanis/web-to-desktop-framework-comparison
NW.js (originally node-webkit) is Electron's main competitor. Both are cross-platform desktop application solutions based on the Chromium+Node.js architecture.
NW.js was founded 2 years earlier than Electron, but Electron is now far more popular than NW.js, and this situation is unlikely to change in the future.
  • Electron separates the Main process and the Renderer process.
    The Renderer process is divided into two contexts, executing Node.js code and browser-side code, respectively.
Electron's model separates concerns, but whether this forced separation of concerns design is meaningful is highly debatable.
The exceptionally cumbersome IPC design and continuous security posturing make the development experience extremely poor.
  • NW.js is based on the Chrome Apps architecture, where Node.js and the Chromium renderer share the same process, allowing direct use of Node.js modules on the browser side.
    Because Node.js native plugins can be loaded directly on the browser side, NW.js performance is far superior to Electron, which relies on IPC, and WASM-based solutions.
There are two modes: separate context and mixed context:
  • In separate context mode, there are two environments: Node context and Web context. The Node context is the runtime environment for Node.js modules, and the Web context is the runtime environment for NW.js scripts.
    Due to the isolation of global variables in different contexts, reference equality issues are prone to occur.
  • In mixed context mode, only one context is used.
    Reference equality issues do not occur, but there is a risk of the context being polluted by third-party libraries.
  • Electron's entry point is the JavaScript script of the Main process.
  • NW.js's entry point is mainly an HTML file, but it can also be a JavaScript script.
Electron's documentation is significantly better than NW.js's documentation. The latter's documentation has poor readability and is severely lacking in updates.
  • Electron has no native source code protection; V8 bytecode solutions like bytenode or electron-vite are needed.
  • NW.js has an officially provided code compiler nwjc, which works on the same principle as bytenode but has a slightly different implementation:
    Because NW.js can directly modify V8, it avoids many limitations present in external solutions like bytenode.
  • Electron has many commercial company users; the most famous project is VSCode.
  • Despite its early start, NW.js currently has a relatively small user base.
    Games made with RPG Maker are likely NW.js's largest user source currently.
Neutralinojs does not bundle Chromium; instead, it uses the WebView on the operating system.
WebView, as can be imagined, brings compatibility issues, and therefore will not be considered a production-grade solution.
Neutralinojs does not support older Windows systems: https://github.com/neutralinojs/neutralinojs/issues/486
Tauri uses Rust and the operating system's WebView, does not support Node.js modules, and requires APIs to be written in Rust.
WebView, as can be imagined, brings compatibility issues, and therefore will not be considered a production-grade solution.
In addition to desktop platforms, Tauri also plans to support iOS and Android.
  • Context isolation creates unnecessary complexity.
  • API lacks uniformity; there are multiple ways just to remove the window's menu bar.
  • Overly fast major version iteration strategy.
  • Lack of useful testing frameworks.
The main process of an Electron application, which is the entry point for the entire application. There can only be one instance.
The process responsible for the user interface in an Electron application, launched by the main process. There can be multiple instances.
Renderer processes, by default, do not have direct access to native Node.js modules.
All Node.js modules must be called indirectly through context-isolated interfaces created by preload scripts.
Node.js integration for the renderer process can be enabled by setting the nodeIntegration: true option when creating the window.
By default, the preload script and the renderer process's host environment are isolated.
They can only communicate via contextBridge and window.postMessage.
This behavior can be disabled by setting contextIsolation: false.
contextBridge.exposeInMainWorld is the only way to expose APIs from the preload script to the renderer process's host environment.
Data types available for bridging are limited:
In addition to basic structured-cloneable types, functions, Promises, errors, and other objects are allowed, but still cannot meet common needs.
Bridging also has the following characteristics:
  • For preload, API exposure is one-time; it cannot be re-exposed, and API changes cannot be synchronized to the host environment.
  • For the host environment, remote functions will appear as native code internally.
  • Exposed objects are frozen, so for the host environment, the API is unwritable.
  • For unsupported types, three different results may occur: passing undefined / object property copying / error.
The renderer process can use ipcRenderer to send requests to the main process.
This IPC supports two modes:
  • send: only sends messages to ipcMain
  • invoke: request-reply mode
Due to context isolation, calling ipcRenderer.send/invoke in the renderer process is very inconvenient.
The main process can send requests to the renderer process, but only if it first obtains the renderer process's WebContents object.
There are two ways to obtain the WebContents object:
  • The renderer first sends a request to the main process, and the event.sender received by the main process is the WebContents object.
  • The main process retains the BrowserWindow when creating the window, and then uses BrowserWindow.webContents.
Due to context isolation, registering event listeners in the renderer process is very inconvenient.
Standard MessagePort interface.
https://www.electronjs.org/docs/latest/tutorial/message-ports
Suitable for communication between main and renderer, and between renderer and renderer.
Since main and renderer can communicate via ipcMain and ipcRenderer,
this communication method is primarily used for communication between renderer and renderer.
Before establishing communication, MessagePort objects must first be passed via ipcMain and ipcRenderer.
IPC can be achieved using network protocols, such as HTTP and WebSocket.
Considering how cumbersome it is to establish communication between Main and Renderer (Web context) in Electron,
using network protocol-based IPC to reduce complexity is also an option.
  • IPC interfaces will be exposed to other programs on the same network.
  • Performance is worse than other methods.
Due to different ABIs, Electron cannot directly use Node.js's native modules; modules must be compiled specifically for Electron.
https://www.electronjs.org/docs/latest/tutorial/devtools-extension
Electron has limited support for the Chrome Extensions API and can load some development tools.
On Windows and macOS, Electron's built-in API can be used to set up startup:
https://www.electronjs.org/docs/latest/api/app#appsetloginitemsettingssettings-macos-windows
However, the implementation of related features is full of regrettable bugs:
  • https://github.com/electron/electron/issues/32657
  • https://github.com/electron/electron/issues/33308
This implementation does not support custom args.
A solution specifically for persisting configuration items as JSON files.
It uses the author's conf module under the hood, supporting JSON Schema and version migration based on package.json.
An in-memory document database that periodically persists to the file system.
Not actively developed.
A pure JavaScript module that uses JSON as a database.
This thing is basically a class for reading and writing JSON, with no outstanding features.
Author stopped maintaining it.
An advanced database that combines RxJS and PouchDB.
Due to using the PouchDB ecosystem, it officially supports Electron.
A typical example of disproportionate download counts to GitHub Stars, actual user base is not large.
Although the author puts a lot of effort into this project, it still has a steep learning curve for new users.
This project also relies on too many packages the author cannot control, making the project very fragile:
  • RxJS
  • PouchDB
  • pouchdb-find (RxCollection's find method directly uses this package, thus sharing the same flaws)
  • mquery (copied code)
Starting from v10, RxDB began preparing to move away from PouchDB.
Considering the project's popularity, maintainability, and the reliability of its dependencies, RxDB is not worth using.
In Node.js, PouchDB utilizes LevelDB as the storage backend via the levelup module.
The levelup module's backend is a native module, but it natively supports Electron, so it can be used without electron-rebuild.
PouchDB is not designed for multi-process scenarios, has many memory-based optimizations, and its Node.js LevelDB backend also does not support multi-process.
Therefore, when using it in Electron, instances should only be created in the Main process and then exposed to the Renderer via IPC for access.
pouchdb-find is an official extension for implementing MongoDB-style query syntax.
This package has many issues, including but not limited to:
  • Incorrect results even with an index:
    https://github.com/pouchdb/pouchdb/issues/7810
    Although the related issue was closed, I still encountered the same problem in the v7.2.2 package.
  • Selector discarding:
    https://github.com/pouchdb/pouchdb/issues/6572
    https://github.com/pouchdb/pouchdb/issues/6371
Relying on this package for queries is clearly unreliable.
Without Mongo support, PouchDB has no practical value.
The last version was released in 2020, and bugs during that period have not been fixed. Many critical bug issues have been ignored by maintainers.
Its type definition @types/pouchdb is also old (adapted for PouchDB v6.4).
The author of RxDB also stated in the RxDB v10 release notes that PouchDB is not well maintained.
https://github.com/pouchdb/pouchdb/issues/3224
As a native module that does not support Node-API (N-API),
better-sqlite3 needs to be processed with electron-rebuild before it can be used, which may lead to dependency locking issues.
This is the SQLite3 module maintained by the Ghost team.
This module has been rewritten based on Node-API since v5.
The callback interface also sets `this`, making it impossible to use arrow functions.
To get a Promise wrapper, a third-party wrapper node-sqlite needs to be installed.
Each Database instance in this library is just a connection; there are no race conditions.
During a transaction, all statements run on the instance become part of that transaction.
Therefore, a new instance always needs to be created to start a transaction. To reuse instances, it means creating a pool, which complicates the solution.
This is the SQLite3 library used by VSCode, it is a fork of sqlite3.
https://www.electronjs.org/docs/latest/tutorial/application-distribution
Essentially, application distribution is achieved by distributing pre-built Electron binary packages that meet a specific directory structure.
For example, on Windows, the manual distribution method is to download the pre-built Electron binary package for the corresponding version and platform,
create a resources/app directory under the Electron directory, and then copy the entire project files into this directory.
Afterward, it is usually necessary to rename the Electron executable and change its icon.
The official electron-packager package can be used to simplify the above steps.
A tar-like file format specific to Electron, designed to package Electron projects into a single file.
The resources/app.asar file can replace the resources/app folder.
Some native modules will pop up this error after being packaged into an asar file.
Without asar packaging, Electron can correctly find and load native modules.
Using --asar.unpack=*.{node,dll} instead of electron-packager's --asar can solve this problem.
References:
  • https://github.com/electron/packager/issues/424
  • https://github.com/electron/asar/issues/75
app.isPackaged is widely used to determine whether an Electron application is in a development or production environment.
However, its judgment method is counter-intuitive; it checks whether the filename of the Electron executable is still 'electron'.
This package automates the creation of applications for various platforms.
  • Supports setting executable icons.
  • Supports removing devDependencies to reduce package size.
electron-packager lacks the feature of ignoring packaging paths according to an ignore file. Although there was once a PR for a .electronignore file, it was rejected
(the reasons for rejection lacked logical and factual support).
The lack of this feature makes electron-packager a useless package, because unless a large number of ignore parameters are added to the command line, the development environment will be packaged along.
When the --all option is enabled, this project's use of I/O and CPU resources is significantly higher than required for the tasks it performs.
After inspecting the source code, it was found that the program simply parallelizes all tasks, which in electron v28 means downloading 10 electrons simultaneously.
Considering the server's bandwidth and the downloader's bandwidth, this unrestrained parallelism only increases the probability of encountering network errors.
Open source solution for Windows installers.
Supported by electron-builder.
Supported by the community package electron-wix-msi.
UWP format supported since Windows 8, can be published to the Microsoft Store.
electron-builder can use the MakeAppx tool to package programs as appx.
The primary format supported by Microsoft starting in 2018, intended to replace MSI, for Windows 10 and above, can be published to the Microsoft Store.
  • There is no MSIX packaging solution yet in the Electron community.
  • The packaging solution provider Advanced Installer supports packaging Electron projects as MSIX.
  • Microsoft's official MSIX Packaging Tool can be used to repackage EXE or MSI installers into MSIX:
    https://docs.microsoft.com/en-us/windows/msix/packaging-tool/create-app-package
Electron had an officially maintained ChromeDriver-based testing library, Spectron, but it was eventually deprecated in early 2022 due to lack of maintainers.
A potential alternative to Spectron is Playwright.
However, as of Playwright v1.16, Playwright-based testing is still an experimental feature and largely unavailable:
  • Unable to debug errors
  • `evaluate` does not support importing Node.js modules
A library for creating window overlays.
One-stop development and packaging for Electron applications.
One-stop packaging and distribution for Electron applications.
The documentation is inconsistent to a pathetic degree.
Electron uses a different ABI version than Node.js, so native modules downloaded by npm and yarn targeting the current Node.js version cannot be used directly.
electron-rebuild is a tool to solve this problem.
For maintainability reasons, it is not recommended to use native modules that require electron-rebuild.
  • electron-rebuild cannot select the correct ABI version, which means modules relying on electron-rebuild may become a difficult-to-upgrade dependency.
    https://github.com/electron/electron-rebuild/issues/886
  • Despite being in the official repository, electron-rebuild often fails to keep up with the latest Electron versions.