chalmery

chalmery

github

Electron (1)

Electron is a desktop framework for JavaScript that makes it possible to package HTML and JavaScript into desktop applications. Many applications, such as vscode, notion, figma, and typora, have already used this technology.

image-20230614231941-kvlx0yu

1. Overall Interaction Form#

What is the most important thing in a desktop application, or what is the difference between it and a browser page? In general, it is the ability to interact with the operating system. So, what does it mean to be able to interact with the operating system? Let's take a look at the browser. The browser is like a box, and all the pages inside it have limited rights. For example,

  • I want to create or modify a file in the "Documents" folder using a JavaScript function in an HTML page I wrote in the browser.
  • I want to use the system notification API to display system notifications.
  • I want to modify the system volume.

All of these cannot be done because they involve operating system APIs. So how does Electron achieve this? Let's take a look at this diagram:

bafkreihsovyhp2czleo53nzxtzlrmvbcq3tech4mzljwu2xnauk4sztahm

Electron acts as an intermediate layer, providing JavaScript with the ability to access system APIs in a roundabout way. Any operation related to the system needs to go through Electron.

As a simple example, suppose I want to get the path of a file through a file selector. How can I achieve this? You might think that the browser can do it too, by using a selector to choose a file. Why do we need Electron?

It's true that you can select a file, but you can't get its path. Think about it, if a regular webpage can easily get the path of a file on your local machine just by clicking on it, or even obtain other operating system information, would you as a browser developer allow this? It would obviously be very dangerous for the user of the browser if a webpage could leak information or even delete files from their computer.

Therefore, the file selector we want to use is the operating system's file selector, which JavaScript cannot achieve. That's why we have Electron, which provides the following API: https://www.electronjs.org/zh/docs/latest/api/dialog

import {dialog} from "electron";

function open() {
    let files = dialog.showOpenDialogSync({
        title: 'Select File Path',
        properties: ['openDirectory', 'multiSelections']
    })
    console.log(files)
    return files
}

Now that we have the file selector, how do we notify Electron? In other words, how can we interact with it? Electron provides a way similar to the publish-subscribe model in JavaScript, where we publish an event and consumers listen to the event.

Documentation: https://www.electronjs.org/zh/docs/latest/api/ipc-renderer

On the JavaScript side, we can send an event to Electron using ipcRenderer.send:

const electron = window.electron

electron.ipcRenderer.send('event name', data)

On the Electron side, we can listen to the event sent by JavaScript using ipcMain.on. And when the event is processed, how can we inform JavaScript? We can use event.reply to send an event with data back to JavaScript, similar to ipcRenderer.send:

import {ipcMain} from "electron"

ipcMain.on('event name', (event, data) => {
    // Processing

    // Callback
    event.reply('new event name', data to be returned)
})

How do we handle it on the JavaScript side? Taking React as an example, we can use ipcRenderer.on to listen to the event sent by Electron in the useEffect hook. It's important to include an empty dependency array ([]) in the useEffect hook so that the event listener is only created once when the page is loaded. In the return statement, we remove the event listener to avoid creating multiple listeners every time the page is loaded.

useEffect(() => {
    // Handle the event sent by Electron
    electron.ipcRenderer.on('event name', (event, data) => {
        // Processing
    });

    return () => {
        electron.ipcRenderer.removeAllListeners('event name');
    };
}, []);

With this, we have completed an interaction. It's simple, isn't it?

2. Packaging#

Packaging is all about configuration. Here is my configuration:

/**
 * @see https://www.electron.build/configuration/configuration
 */
{
  "appId": "electron-music",
  "productName": "electron-music",
  "copyright": "Copyright © 2023 ${author}",
  "asar": true,
  "directories": {
    "output": "release",
    "buildResources": "public"
  },
  "files": [
    "dist"
  ],
  "win": {
    "icon": "public/icons/music256x256.png",
    "target": [
      {
        "target": "dir",
        "arch": [
          "x64"
        ]
      },
      {
        "target": "nsis",
        "arch": [
          "x64"
        ]
      }
    ]
  },
  "nsis": {
    "oneClick": false,
    "allowElevation": true,
    "allowToChangeInstallationDirectory": true,
    "installerIcon": "public/icons/music256x256.png",
    "uninstallerIcon": "public/icons/music256x256.png",
    "installerHeader": "public/icons/music256x256.png",
    "installerHeaderIcon": "public/icons/music256x256.png",
    "installerSidebar": "public/icons/music256x256.png",
    "uninstallerSidebar": "public/icons/music256x256.png",
    "uninstallDisplayName": "electron-music-${version}",
    "createDesktopShortcut": true,
    "createStartMenuShortcut": true,
    "include": "script/installer.nsi",
    "script": "script/installer.nsi",
    "deleteAppDataOnUninstall": true,
    "runAfterFinish": false,
    "menuCategory": false,
    "perMachine": true,
    "language": "2052"
  },
  "mac": {
    "icon": "public/icons/music256x256.png",
    "category": "Productivity",
    "target": [
      {
        "target": "default",
        "arch": [
          "arm64",
          "x64"
        ]
      }
    ]
  },
  "linux": {
    "icon": "public/icons/music256x256.png",
    "target": [
      "AppImage",
      "tar.gz"
    ],
    "category": "Audio",
    "artifactName": "${productName}-Linux-${version}.${ext}"
  }
}

Reference: My Music Player Project

3. Notes#

1. Do something before closing the window in Linux#

Check the environment. For example, it is not possible to hide the window in KDE. In KDE, when you click the close button, the window object is directly destroyed. (As a workaround, we can avoid using the system's built-in top bar and implement our own, so that this problem does not occur.)

app.on('closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

Error message:

libva error: vaGetDriverNameByIndex() failed with unknown libva error, driver_name = (null)
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.