takeaway

It has been a period of time to develop the client program using Electron, and the overall feeling is very good, but there are also some pits. This paper systematically summarizes Electron from [operation principle] to [practical application]. 【 Many pictures, long warning ~ 】

All the example code for this article is on my Github electron- React. In addition, electron- React can also be used as a scaffolding project using electron + React + Mobx + Webpack technology stack.

Desktop applications

Graphical User Interface desktop applications, also known as GUI programs, have some differences. Desktop applications crystallize GUI programs from A GUI to a “desktop,” making the cold, wood-like concept of a computer more human, vivid, and dynamic.

The various client programs we use on our computers are desktop applications. In recent years, the rise of the WEB and mobile has made desktop applications less and less popular, but desktop applications are still essential for some daily functions or industry applications.

The traditional desktop application development methods are as follows:

1.1 Native Development

Directly compile the language into executable files, directly call the system API, complete UI drawing and so on. This kind of development technology has high operating efficiency, but generally speaking, the development speed is slow and the technical requirements are high. For example:

  • useC++ / MFCThe development ofWindowsapplication
  • useObjective-CThe development ofMACapplication

1.2 Hosting Platform

There was native development and UI development from the beginning. After a compilation, the intermediate file is obtained, and the platform or virtual machine is used to complete the secondary loading compilation or interpretation. Running efficiency is lower than native compilation, but after platform optimization, its efficiency is also considerable. In terms of development speed, it is faster than native compilation technology. Such as:

  • useC# / .NET Framework(Development onlyWindows applications)
  • Java / Swing

However, the above two are too unfriendly to front-end developers, which are basically areas that front-end developers will not touch, but in this era of [big front-end πŸ˜…], front-end developers are trying to get into all kinds of areas, using WEB technology to develop client methods suddenly appear.

1.3 WEB development

The use of WEB technology for development, the use of browser engine to complete UI rendering, the use of Node.js server-side JS programming and can call system API, it can be considered as a client shell of the WEB application.

In the interface, the powerful ecosystem of the WEB brings infinite possibilities for UI, and the development and maintenance costs are relatively low, and the front-end developers with WEB development experience can easily get started to develop.

This article will focus on one of the technologies for developing client programs using WEB technology [ELECTRON]

Second, the Electron

Electron is an open source library developed by Github for building cross-platform desktop applications using HTML, CSS, and JavaScript. Electron does this by merging Chromium and Node.js into the same runtime environment and packaging it as an application for Mac, Windows, and Linux.

2.1 Reasons for using Electron development:

  • Use a strong ecologyWebTechnology development, low development cost, strong scalability, more coolUI
  • Cross-platform, a set of code can be packaged asWindows, Linux, MacThree sets of software, and fast compilation
  • Available directly in existingWebExtend your application to provide capabilities that the browser doesn’t have
  • You are a front-end πŸ‘¨πŸ’» ~

Of course, there are downsides: performance is lower than native desktop apps, and the final package is much larger than native apps.

2.2 Development Experience

compatibility

While you’re still developing with WEB technology, you don’t need to worry about compatibility, just worry about the version of Chrome you’re currently using with Electron, which is generally new enough to use the latest API and syntax, and you can manually upgrade the Chrome version. Also, you don’t have to worry about the style and code compatibility of different browser bands.

The Node environment

This is something many front-end developers have dreamed of, using the powerful API provided by Node.js in the WEB interface, which means you can manipulate files, call system apis, and even manipulate databases directly from the WEB page. Of course, you can use hundreds of thousands of additional NPM modules in addition to the full Node API.

Cross domain

You can make network requests directly using the Request module provided by Node, which means you don’t have to cross domains anymore.

Powerful scalability

Node-ffi provides powerful extensibility for applications (more on that in a later section).

2.3 Who is using Electron

A number of applications have been developed using Electron, including the familiar VS Code client, GitHub client, Atom client, and so on. Very impressed, last year thunderbolt in the release of thunderbolt X10.1 copywriting:

Starting with xunlei X 10.1, we completely rewrote the Main interface of Xunlei using the Electron software framework. The xunlei X with the new frame can perfectly support 2K, 4K and other high-definition display, the text rendering interface is also more clear and sharp. From the technical point of view, the new framework is more flexible and efficient than the old framework in interface drawing and event processing, so the interface fluency is significantly better than the old framework. How big is the increase? You’ll see if you try.

You can debug the VS Code client interface by opening VS Code and clicking On Help to switch Developer Tools.

Iii. Principle of Electron operation

Electron combines Chromium, Node.js and an API for calling native functions of the operating system.

3.1 the Chromium

Chromium is an open source project launched by Google to develop Chrome browser. Chromium is equivalent to the project version or experimental version of Chrome. New functions will be implemented on Chromium first, and will be applied to Chrome after verification, so Chrome’s functions will be relatively backward but stable.

Chromium provides Electron with powerful UI capabilities that allow interface development without compatibility considerations.

3.2 the Node. Js

Node.js is a development platform that lets JavaScript run on the server side. Node uses an event-driven, non-blocking I/O model to be lightweight and efficient.

Chromium alone cannot have the ability to operate the native GUI directly. Electron integrates Nodejs, which enables it to develop the interface as well as the underlying API of the operating system. Path, FS, Crypto and other modules commonly used in Nodejs can be directly used in Electron.

3.3 system API

In order to provide GUI support for native systems, Electron has a native APPLICATION programming interface (API) built in, which enables calling system functions such as calling system notifications and opening system folders.

In development mode, Electron is developed separately in calling system APIS and drawing interfaces. Let’s take a look at how Electron divides processes.

3.4 main process

Electron distinguishes between two kinds of processes: the main process and the render process, each responsible for its own function.

Electron a process running the package.json main script is called the main process. A Electron application always has one and only one main process.

Responsibilities:

  • Create render process (multiple)
  • Controls the application lifecycle (startup, exit)APPAs well as to theAPPDo some event listening)
  • Call the underlying functions of the system and call the native resources

Callable API:

  • Node.js API
  • ElectronProvides the main processAPI(Including some system functions andElectronAdditional features)

3.5 Rendering Process

Since Electron uses Chromium to present Web pages, Chromium’s multi-process architecture is also used. Each Web page in Electron runs in its own rendering process.

The main process creates the page using the BrowserWindow instance. Each BrowserWindow instance runs the page in its own rendering process. When an instance of BrowserWindow is destroyed, the rendering process is terminated.

You can think of the renderer as a browser window, which can have multiple and independent versions of each other, but unlike the browser, it calls the Node API.

Responsibilities:

  • withHTMLandCSSRendering interface
  • withJavaScriptDo some interface interaction

Callable API:

  • DOM API
  • Node.js API
  • ElectronRender process providedAPI

Iv. Electron foundation

4.1 Electron API

In the previous section we mentioned that the render into and main processes can call the Electron API separately. All of Electron’s apis are assigned to a process type. Many apis can only be used in the main process, some can only be used in the renderer process, and some can be used in both main and renderer processes.

You can obtain the Electron API in the following way

const { BrowserWindow, ... } = require('electron')
Copy the code

Here are some common Electron apis:

In later chapters, we will select the commonly used modules for detailed introduction.

4.2 Using the Node.js API

You can use the Node.js API in Electron at the same time in the main and render processes.) All the apis that are available in Node.js are also available in Electron.

import {shell} from 'electron';
import os from 'os';

document.getElementById('btn').addEventListener('click', () => { 
  shell.showItemInFolder(os.homedir());
})
Copy the code

One very important note: native Node.js modules (that is, modules that need to compile the source code before they can be used) need to be compiled before they can be used with Electron.

4.3 Process Communication

The main and renderer processes have different responsibilities, but they also need to collaborate and communicate with each other.

For example, managing native GUI resources on a Web page is dangerous and can easily leak resources. So on web pages, you are not allowed to call native GUI-related apis directly. If a renderer wants to perform native GUI operations, it must communicate with the main process and request that the main process perform these operations.

4.4 The renderer communicates with the main process

IpcRenderer is an instance of EventEmitter. You can use some of the methods it provides to send synchronous or asynchronous messages from the renderer to the main process. It can also receive messages returned by the main process.

Introduce ipcRenderer in the render process:

import { ipcRenderer } from 'electron';
Copy the code

Asynchronous sending:

Send a synchronous message to the main process through a channel, which can carry arbitrary parameters.

Internally, the parameters are serialized to JSON, so the function and prototype chains on the parameter object are not sent.

ipcRenderer.send('async-render'.'I'm an asynchronous message from the renderer process');
Copy the code

Synchronous sending:

 const msg = ipcRenderer.sendSync('sync-render'.'I'm a sync message from the renderer');
Copy the code

Note: Sending a synchronization message will block the entire renderer process until a response is received from the main process.

The main process listens for messages:

The ipcMain module is an instance of the EventEmitter class. When used in the main process, it handles asynchronous and synchronous information sent from the renderer process (web page). Messages sent from the renderer process will be sent to this module.

Ipcmain. on: Listens to a channel. When a new message is received, the listener starts with a listener(Event, args…). Is called in the form of.

  ipcMain.on('sync-render', (event, data) => {
    console.log(data);
  });
Copy the code

4.5 The main process communicates with the renderer process

The main process can send a message to the renderer using BrowserWindow’s webContents, so you must find the corresponding BrowserWindow object before sending the message. :

const mainWindow = BrowserWindow.fromId(global.mainId);
 mainWindow.webContents.send('main-msg'.`ConardLi]`)
Copy the code

Send according to the source:

In the ipcMain callback that receives the message, the first argument, the Event property sender, gets the webContents object of the source renderer, which we can use to respond to the message directly.

  ipcMain.on('sync-render', (event, data) => {
    console.log(data);
    event.sender.send('main-msg'.'The main process received an asynchronous message from the renderer! ')});Copy the code

The renderer listens:

Ipcrenderer. on: Listener (Event, args…) when a new message arrives Call the listener.

ipcRenderer.on('main-msg', (event, msg) => {
    console.log(msg);
})
Copy the code

4.6 Communication Principles

IpcMain and ipcRenderer are both instances of the EventEmitter class. The EventEmitter class, which is the basis of NodeJS events, is exported by the Events module in NodeJS.

The core of EventEmitter is the encapsulation of event triggering and event listener functions. It implements the interfaces required by the event model, including addListener, removeListener, EMIT, and other utility methods. Similar to native JavaScript events, a publish/subscribe (observer) approach is used, using an internal _events list to record registered event handlers.

We listen and send messages via ipcMain and ipcRenderer’s on and Send, which are related interfaces defined by EventEmitter.

4.7 remote

The Remote module provides a simple way for the renderer process (Web pages) to communicate with the main process (IPC). With the Remote module, you can call methods on main process objects without having to explicitly send interprocess messages, similar to Java’s RMI.

import { remote } from 'electron';

remote.dialog.showErrorBox('Dialog module only for main process'.'I'm using remote')
Copy the code

But when we call a remote object’s method or function, or create a new object through the remote constructor, we are actually sending a synchronous interprocess message.

In the example above of calling dialog via the remote module. The dialog object we created in the renderer is not actually in our renderer, it just has the main process create a Dialog object and return the corresponding remote object to the renderer.

4.8 Communication between renderers

Electron does not provide a way for renderers to communicate with each other, we can set up a message relay station in the main process.

Communication between renderers first sends messages to the main process, and the main process’s relay station receives the messages and distributes them according to the conditions.

4.9 Rendering process data Sharing

The easiest way to share data between two renderers is to use the HTML5 API already implemented in the browser. A good solution is to use the Storage API, localStorage, sessionStorage, or IndexedDB.

Just like in a browser, this storage is equivalent to storing a piece of data permanently in the application. Sometimes you don’t need this storage, just some data sharing for the life of the current application. You can do this with the IPC mechanism in Electron.

Store the data in a global variable in the main process, then access it in multiple renderers using the Remote module.

Initialize global variables in the main process:

global.mainId = ... ; global.device = {... }; global.__dirname = __dirname; global.myField = {name: 'ConardLi' };
Copy the code

Read from the render process:

import { ipcRenderer, remote } from 'electron';

const { getGlobal } = remote;

const mainId = getGlobal('mainId')
const dirname = getGlobal('__dirname')
const deviecMac = getGlobal('device').mac;
Copy the code

Change in render process:

getGlobal('myField').name = 'Code Secret Garden';
Copy the code

Multiple renderers share global variables of the same main process so that renderers can share and pass data.

Five, the window

5.1 BrowserWindow

The main process module BrowserWindow is used to create and control the BrowserWindow.

  mainWindow = new BrowserWindow({
    width: 1000.height: 800.// ...
  });
  mainWindow.loadURL('http://www.conardli.top/');
Copy the code

You can view all of its construction parameters here.

5.2 Frameless Window

A frameless window is a window that has no edges, and parts of the window (such as a toolbar) are not part of the web page.

In the BrowserWindow constructor, setting frame to false specifies that the window is borderless. Hiding the toolbar causes two problems:

  • 1. Window control buttons (minimize, full screen, close) are hidden
  • 2. Unable to drag and drop Windows to move

The toolbar button can be displayed again by specifying the titleBarStyle option, setting it to Hidden to return a full-size content window that hides the title bar, still with the standard window control button in the upper left corner.

new BrowserWindow({
    width: 200.height: 200.titleBarStyle: 'hidden'.frame: false
  });
Copy the code

5.3 Window Dragging and Dropping

By default, bezel-less Windows are not draggable. We can manually specify the drag region in the interface using the CSS property – Webkit-app-region: drag.

In a frameless window, the drag behavior may conflict with the selection text by setting -webkit-user-select: none; Disable text selection:

.header {
  -webkit-user-select: none;
  -webkit-app-region: drag;
}
Copy the code

Conversely, set -webkit-app-region: no-drag inside the dragable region to specify a specific drag-free region.

5.4 Transparent Window

You can also make a frameless window transparent by setting the transparent option to true:

new BrowserWindow({
    transparent: true.frame: false
  });
Copy the code

5.5 the Webview

Use the WebView tag to embed “foreign” content in Electron. Foreign content is contained in the WebView container. Embedded pages in an application can control the layout and redrawing of external content.

Unlike iframe, webView runs in a different process from the application. It won’t have the same permissions as your Web page, and all interactions between the application and the embedded content will be asynchronous.

6. Dialog box

The Dialog module provides apis to display native system dialogs, such as open file boxes and alert boxes, so web applications can give users the same experience as system applications.

Note: Dialog is the main process module and you can use remote to call the renderer

6.1 Error Messages

Dialog. showErrorBox Is used to display a modal dialog box that displays error messages.

 remote.dialog.showErrorBox('wrong'.'This is an error popup! ')
Copy the code

6.2 the dialog

Dialog. showErrorBox is used to call system dialogs and can be specified with several different types: “None “, “info”, “error”, “question” or” warning”.

On Windows, “question” displays the same icon as “info”, unless you use the “icon” option to set the icon. On macOS, “Warning” and “error” display the same warning icon

remote.dialog.showMessageBox({
  type: 'info'.title: 'Prompt message'.message: 'This is a dialog box! '.buttons: ['sure'.'cancel']
}, (index) => {
  this.setState({ dialogMessage: 'You clicked${index ? 'cancel' : 'sure'}!!!!! 】 `})})Copy the code

6.3 file box

Dialog. showOpenDialog is used to open or select the system directory.

remote.dialog.showOpenDialog({
  properties: ['openDirectory'.'openFile']
}, (data) => {
  this.setState({ filePath: 'select path:${data[0]}】 `})})Copy the code

6.4 information box

It is recommended to use the HTML5 API directly, which can only be used in the renderer process.

let options = {
  title: 'Info-box title'.body: I am a message ~ ~ ~ ',}let myNotification = new window.Notification(options.title, options)
myNotification.onclick = (a)= > {
  this.setState({ message: 'You clicked the message box!!' '})}Copy the code

Seven,

7.1 Obtaining System Information

Obtain the process object of the main process through remote, and obtain various versions of the current application:

  • process.versions.electron:electronVersion information
  • process.versions.chrome:chromeVersion information
  • process.versions.node:nodeVersion information
  • process.versions.v8:v8Version information

Get the current application root directory:

remote.app.getAppPath()
Copy the code

Obtain the current system root directory using the NODE OS module:

os.homedir();
Copy the code

7.2 Copy and Paste

The Clipboard supplied by Electron is available in both the renderer and main processes to perform copy and paste operations on the system clipboard.

Write to the clipboard as plain text:

clipboard.writeText(text[, type])
Copy the code

Get the contents of the clipboard as plain text:

clipboard.readText([type])
Copy the code

7.3 screenshot

Desktopcapreceived is expected of media sources that capture audio and video from the desktop. It can only be called in the renderer process.

The following code is an example of getting a screenshot and saving it:

  getImg = (a)= > {
    this.setState({ imgMsg: 'Capturing the screen... ' })
    const thumbSize = this.determineScreenShotSize()
    let options = { types: ['screen'].thumbnailSize: thumbSize }
    desktopCapturer.getSources(options, (error, sources) => {
      if (error) return console.log(error)
      sources.forEach((source) = > {
        if (source.name === 'Entire screen' || source.name === 'Screen 1') {
          const screenshotPath = path.join(os.tmpdir(), 'screenshot.png')
          fs.writeFile(screenshotPath, source.thumbnail.toPNG(), (error) => {
            if (error) return console.log(error)
            shell.openExternal(`file://${screenshotPath}`)
            this.setState({ imgMsg: 'Save the screenshot to:${screenshotPath}` })
          })
        }
      })
    })
  }

  determineScreenShotSize = (a)= > {
    const screenSize = screen.getPrimaryDisplay().workAreaSize
    const maxDimension = Math.max(screenSize.width, screenSize.height)
    return {
      width: maxDimension * window.devicePixelRatio,
      height: maxDimension * window.devicePixelRatio
    }
  }

Copy the code

Eight, the menu

The menu of the application program can help us quickly reach a certain function without using the interface resources of the client. Generally, the menu can be divided into two types:

  • Application menu: Located at the top of the application and available globally
  • Context menu: can customize any page display, custom call, such as right-click menu

Electron provides us with the Menu module for creating native application menus and context menus, which is a main process module.

You can construct a Menu object using a custom Menu template via Menu’s static buildFromTemplate(template) method.

Template is an array of MenuItems. Let’s look at some important parameters of MenuItem:

  • label: Indicates the text displayed in the menu
  • click: Event handler after clicking the menu
  • role: Indicates a predefined menu, for examplecopy(Copy),paste(Paste),minimize(minimize)…
  • enabled: indicates whether the project is enabled. This property can be changed dynamically
  • submenu: submenu, also aMenuItemAn array of

Recommendation: It is better to specify any menu item that role matches the standard role, rather than trying to implement the behavior in the click function manually. The built-in role behavior will provide the best local experience.

The following example is a simple menu template.

const template = [
  {
    label: 'file'.submenu: [{label: 'New File'.click: function () {
          dialog.showMessageBox({
            type: 'info'.message: 'hey! '.detail: 'You hit New File! ',})}}]}, {label: 'edit'.submenu: [{
      label: 'cut'.role: 'cut'
    }, {
      label: 'copy'.role: 'copy'
    }, {
      label: 'paste'.role: 'paste'}]}, {label: Minimize.role: 'minimize'}]Copy the code

8.1 Application Menu

Using Menu’s static method, setApplicationMenu, you create an application Menu that, on Windows and Linux, will be set as the top-level Menu for each window.

Note: This API app must be called after the module ready event.

Menus can be treated differently by different systems depending on the life cycle of the application.

app.on('ready'.function () {
  const menu = Menu.buildFromTemplate(template)
  Menu.setApplicationMenu(menu)
})

app.on('browser-window-created'.function () {
  let reopenMenuItem = findReopenMenuItem()
  if (reopenMenuItem) reopenMenuItem.enabled = false
})

app.on('window-all-closed'.function () {
  let reopenMenuItem = findReopenMenuItem()
  if (reopenMenuItem) reopenMenuItem.enabled = true
})

if (process.platform === 'win32') {
  const helpMenu = template[template.length - 1].submenu
  addUpdateMenuItems(helpMenu, 0)}Copy the code

8.2 Context Menu

Use the example method menu.popup for Menu to customize the pop-up context Menu.

    let m = Menu.buildFromTemplate(template)
    document.getElementById('menuDemoContainer').addEventListener('contextmenu', (e) => {
      e.preventDefault()
      m.popup({ window: remote.getCurrentWindow() })
    })
Copy the code

8.3 the shortcut

In the menu options, we can specify an Accelerator property to specify shortcut keys for actions:

  {
    label: Minimize.accelerator: 'CmdOrCtrl+M'.role: 'minimize'
  }
Copy the code

In addition, we can use globalShortcut to register global shortcuts.

    globalShortcut.register('CommandOrControl+N', () => {
      dialog.showMessageBox({
        type: 'info'.message: 'hey! '.detail: 'You triggered the manual registration shortcut.',})})Copy the code

CommandOrControl represents the Command key on macOS and the Control key on Linux and Windows.

Nine, print,

In many cases the printing used in the program is not perceived by the user. In addition, in order to control the printed content flexibly, we often need to use the API provided by the printer to develop, which is very tedious and difficult to develop. The first time Electron was used in a business was for its printing function, which will be described here.

Electron provides a printing API that is flexible enough to control the display of print Settings and to write print content in HTML. Electron provides two ways to print, either directly to the printer or to a PDF.

And there are two types of objects that can be called to print:

  • throughwindowthewebcontentObject, using this method requires a separate print window, which can be hidden, but the communication call is relatively complex.
  • Use pagewebviewElement is called to print, which can be placedwebviewHidden in the invoked page, communication is relatively simple.

These two methods have both print and printToPdf methods.

9.1 Invoking system Printing

Contents. Print ([options], [callback]);Copy the code

There are only three simple options in print configuration:

  • silent: Whether the printing configuration is not displayed when printing (whether to print silently)
  • printBackground: Indicates whether to print the background
  • deviceName: Name of the printer device

The first step is to configure the name of the printer we are using and determine whether the printer is available before invoking print.

Use the getPrinters method of webContents to get a list of printers that have been configured for the current device. Note that the configured printer is not available, but the driver has been installed on the device.

Through getPrinters access to printer object: electronjs.org/docs/api/st…

We only care about two here, name and status, and a status of 0 indicates that the printer is available.

The second argument to print, callback, is the callback used to determine whether the print task issued, rather than the callback after the print task completed. So when a print task is issued, the callback function is called and returns true. This callback does not determine whether the print was actually successful.

    if (this.state.curretnPrinter) {
      mainWindow.webContents.print({
        silent: silent, printBackground: true.deviceName: this.state.curretnPrinter
      }, () => { })
    } else {
      remote.dialog.showErrorBox('wrong'.'Please select a printer first! ')}Copy the code

9.2 Printing a PDF file

The usage of printToPdf is basically the same as print, but print has very few configuration items, whereas printToPdf extends many attributes. Here a look at the source found that there are a lot of not posted into the document, about three dozen, including can print margin, print header footer and so on configuration.

contents.printToPDF(options, callback)
Copy the code

The callback function is called after printing failure or success to obtain information about printing failure or a buffer containing PDF data.

    const pdfPath = path.join(os.tmpdir(), 'webviewPrint.pdf');
    const webview = document.getElementById('printWebview');
    const renderHtml = 'I was temporarily inserted into webView content... ';
    webview.executeJavaScript('document.documentElement.innerHTML =`' + renderHtml + '`; ');
    webview.printToPDF({}, (err, data) => {
      console.log(err, data);
      fs.writeFile(pdfPath, data, (error) => {
        if (error) throw error
        shell.openExternal(`file://${pdfPath}`)
        this.setState({ webviewPdfPath: pdfPath })
      });
    });
Copy the code

The printing in this example is done using the WebView, and the printed content is dynamically inserted into the WebView by calling the executeJavaScript method.

9.3 Selection of two printing schemes

As mentioned above, both WebView and WebContent can be used to call the printing function. To use WebContent to print, the first step is to have a printing window, which cannot be printed and created at any time, which costs performance. You can start it up while the program is running and listen for events.

This process needs to communicate well with those calling printing, and the general process is as follows:

It can be seen that communication is very tedious. Using webView to print can achieve the same effect, but the communication mode will become simple, because the rendering process and webview do not need to go through the main process to communicate, through the following way:

  const webview = document.querySelector('webview')
  webview.addEventListener('ipc-message', (event) => {
    console.log(event.channel)
  })
  webview.send('ping');const {ipcRenderer} = require('electron')
  ipcRenderer.on('ping', () => {
    ipcRenderer.sendToHost('pong')})Copy the code

I’ve written a DEMO for ELectron printing before: ELectron print-demo can be clone if you are interested.

9.4 Printing Function Encapsulation

Here are a few tool functions that encapsulate common printing functions.

/** * Get the system printer list */
export function getPrinters() {
  let printers = [];
  try {
    const contents = remote.getCurrentWindow().webContents;
    printers = contents.getPrinters();
  } catch (e) {
    console.error('getPrintersError', e);
  }
  return printers;
}
/** * Get the system default printer */
export function getDefaultPrinter() {
  return getPrinters().find(element= > element.isDefault);
}
/** * Checks if a print driver is installed */
export function checkDriver(driverMame) {
  return getPrinters().find(element= > (element.options["printer-make-and-model"] | |' ').includes(driverMame));
}
/** * Gets the printer object */ based on the printer name
export function getPrinterByName(name) {
  return getPrinters().find(element= > element.name === name);
}

Copy the code

X. Program protection

10.1 collapse

Crash monitoring is an essential protection feature for every client application. There are two things we expect when an application crashes:

  • 1. Upload crash logs and report to the police in time
  • 2. The monitoring program crashes, prompting you to restart the program

Electron provides us with crashReporter to help us log crashes. We can create a crashReporter by using crashreporter.start:

const { crashReporter } = require('electron')
crashReporter.start({
  productName: 'YourName'.companyName: 'YourCompany'.submitURL: 'https://your-domain.com/url-to-submit'.uploadToServer: true
})
Copy the code

When a crash occurs, the crash log will be stored in a temporary folder named YourName Crashes in a file folder. SubmitURL specifies your crash log upload server. You can customize where to save these temporary files by calling the app.setPath(‘temp’, ‘my/ Custom /temp’)API before starting the crash reporter. You can also pass crashReporter. GetLastCrashReport () the date of the last time to get a crash report and ID.

We can listen for a crash of the renderer process through webContents’ crashed, and some main process crashes have also been tested to trigger this event. So we can determine whether the main window is destroyed by different restart logic, the following is the whole crash monitoring logic:

import { BrowserWindow, crashReporter, dialog } from 'electron';
// Enable the process crash record
crashReporter.start({
  productName: 'electron-react'.companyName: 'ConardLi'.submitURL: 'http://xxx.com'.// Interface for uploading crash logs
  uploadToServer: false
});
function reloadWindow(mainWin) {
  if (mainWin.isDestroyed()) {
    app.relaunch();
    app.exit(0);
  } else {
    // Destroy other Windows
    BrowserWindow.getAllWindows().forEach((w) = > {
      if(w.id ! == mainWin.id) w.destroy(); });const options = {
      type: 'info'.title: 'Renderer process crashes'.message: 'This process has crashed.'.buttons: ['overloaded'.'off']
    }
    dialog.showMessageBox(options, (index) => {
      if (index === 0) mainWin.reload();
      elsemainWin.close(); }}})export default function () {
  const mainWindow = BrowserWindow.fromId(global.mainId);
  mainWindow.webContents.on('crashed', () = > {const errorMessage = crashReporter.getLastCrashReport();
    console.error('The program crashed! ', errorMessage); // Log can be uploaded separately
    reloadWindow(mainWindow);
  });
}
Copy the code

10.2 Minimizing to a tray

Sometimes we don’t want the user to close the application by clicking the close button, but rather minimize the application to the tray and do the actual exit operation on the tray.

The first step is to listen for the window closing event, prevent the default behavior of the user closing operation, and hide the window.

function checkQuit(mainWindow, event) {
  const options = {
    type: 'info'.title: 'Close confirmation'.message: 'Are you sure you want to minimize the procedure to the tray? '.buttons: ['confirm'.'Close program']}; dialog.showMessageBox(options, index => {if (index === 0) {
      event.preventDefault();
      mainWindow.hide();
    } else {
      mainWindow = null;
      app.exit(0); }}); }function handleQuit() {
  const mainWindow = BrowserWindow.fromId(global.mainId);
  mainWindow.on('close', event => {
    event.preventDefault();
    checkQuit(mainWindow, event);
  });
}
Copy the code

At this time, the program can no longer be found, there is no program in the task tray, so we need to create a good task tray, and do a good job of event monitoring.

Windows platforms can achieve better results using ICO files

export default function createTray() {
  const mainWindow = BrowserWindow.fromId(global.mainId);
  const iconName = process.platform === 'win32' ? 'icon.ico' : 'icon.png'
  tray = new Tray(path.join(global.__dirname, iconName));
  const contextMenu = Menu.buildFromTemplate([
    {
      label: 'Show home screen'.click: (a)= > {
        mainWindow.show();
        mainWindow.setSkipTaskbar(false); }}, {label: 'exit'.click: (a)= > {
        mainWindow.destroy();
        app.quit();
      }
    },
  ])
  tray.setToolTip('electron-react');
  tray.setContextMenu(contextMenu);
}
Copy the code

11. Expand your capabilities

In many cases, your application will need to interact with external devices. Typically, manufacturers will provide you with development kits for hardware devices, which are written mostly in C++. In the case of electron, we don’t have the ability to call C++ code directly. We can use Node-ffi to do this.

Node-ffi provides a powerful set of tools for invoking the dynamic link library interface using pure JavaScript in a Node.js environment. It can be used to build interface bindings for libraries without using any C++ code.

Note that node-ffi does not call C++ code directly, you need to compile C++ code into dynamically linked libraries: Dll on Windows, dylib on Mac OS, so on Linux.

Node-ffi loading libraries is limited and can only handle C-style libraries.

Here is a simple example:

const ffi = require('ffi');
const ref = require('ref');
const SHORT_CODE = ref.refType('short');


const DLL = new ffi.Library('test.dll', {
    Test_CPP_Method: ['int'['string',SHORT_CODE]], 
  })

testCppMethod(str: String.num: number): void {
  try {
    const result: any = DLL.Test_CPP_Method(str, num);
    return result;
  } catch (error) {
    console.log('Call failed ~',error); }}this.testCppMethod('ConardLi'.123);
Copy the code

In the above code, we wrapped the dynamic link library test.dll generated by the C++ interface with ffi and used ref to do some type mapping.

When calling these mapping methods using JavaScript, it is recommended to use TypeScript to specify parameter types, because weakly-typed JavaScript can pose unexpected risks when calling interfaces to strongly typed languages.

With this capability, front-end developers can also play a role in IOT 😎 ~

12. Environmental selection

Typically, our application may run in multiple environments (Production, Beta, UAT, MOKE, Development…). Different development environments may have different back-end interfaces or other configurations. We can build a simple environment selection function into the client program to help us develop more efficiently.

Specific strategies are as follows:

  • In the development environment, we directly enter the environment selection page, read the selected environment to respond to the redirection operation
  • Keep the environment selection entry in the menu for switching during development
const envList = ["moke"."beta"."development"."production"];
exports.envList = envList;
const urlBeta = 'https://wwww.xxx-beta.com';
const urlDev = 'https://wwww.xxx-dev.com';
const urlProp = 'https://wwww.xxx-prop.com';
const urlMoke = 'https://wwww.xxx-moke.com';
const path = require('path');
const pkg = require(path.resolve(global.__dirname, 'package.json'));
const build = pkg['build-config'];
exports.handleEnv = {
  build,
  currentEnv: 'moke'.setEnv: function (env) {
    this.currentEnv = env
  },
  getUrl: function () {
    console.log('env:', build.env);
    if (build.env === 'production' || this.currentEnv === 'production') {
      return urlProp;
    } else if (this.currentEnv === 'moke') {
      return urlMoke;
    } else if (this.currentEnv === 'development') {
      return urlDev;
    } else if (this.currentEnv === "beta") {
      returnurlBeta; }},isDebugger: function () {
    return build.env === 'development'}}Copy the code

13. Packing

The last and most important step is to package your written code as a runnable.app or.exe executable.

Here I do two parts of the packaging atmosphere, the rendering process packaging and the main process packaging.

13.1 Render process packaging and upgrading

In general, most of our business logic code is in the rendering process is complete, in most cases we just need to update and upgrade the rendering process without the need for changes to the main process code, we apply colours to a drawing process of the packaging and general web project actually packing is not too big difference, use webpack packaging.

Here are the advantages of packaging render processes separately:

After packing the HTML and JS files, we usually upload them to our front-end static resource server, and then inform the server that our renderer process has a code update, which can be described as a separate upgrade of the renderer process.

Attention, and the upgrading of shell is different, the rendering process upgrade is only a static resource HTML and js file on the server update, without the need to download the update the client, so every time we start the program detected offline update package, you can directly to refresh read the latest version of the static resource file, even if in the process of the program is run to force update, Our program simply forces a page refresh to read the latest static resources, which is a very user friendly upgrade.

Note here that once we configure this, it means a complete separation of the renderer process from the main process packaging upgrade, and the files we read when we start the main window should no longer be local files, but files that are packaged and placed on the static resource server.

To facilitate development, here we can distinguish between local and online loading of different files:

function getVersion (mac,current){
  // Obtain the latest version based on the MAC address and current version
}
export default function () {
  if (build.env === 'production') {
    const version = getVersion (mac,current);
    return 'https://www.xxxserver.html/electron-react/index_'+version+'.html';
  }
  return url.format({
    protocol: 'file:'.pathname: path.join(__dirname, 'env/environment.html'),
    slashes: true.query: { debugger: build.env === "development"}}); }Copy the code

The specific webpack configuration is not posted here, but can be viewed in my Github electron- React /scripts directory.

Note here that in the development environment we can launch the app with the devServer and electron commands of Webpack:

  devServer: {
    contentBase: './assets/'.historyApiFallback: true.hot: true.port: PORT,
    noInfo: false.stats: {
      colors: true,
    },
    setup() {
      spawn(
        'electron'['. '] and {shell: true.stdio: 'inherit',
        }
      )
        .on('close', () => process.exit(0))
        .on('error', e => console.error(e)); }},/ /...
Copy the code

13.2 Main Process Packaging

The main process is to package the whole program into a runnable client program. There are two commonly used packaging schemes, namely, electric-Packager and electric-Builder.

The electron Packager configuration is a bit cumbersome for me, and it can only package applications directly as executables.

Here, I recommend using electron Builder, which not only has a convenient function to configure protocol, built-in Auto Update, and a simple configuration of package.json to complete the packaging work, and the user experience is very good. Moreover, the electron Builder can not only package the application directly into the executable program such as EXE APP, but also into the installation package format such as MSI DMG.

You can easily do various configurations in package.json:

 "build": {
   "productName": "electron-react".// App name in Chinese
   "appId": "electron-react"./ / app id
   "directories": { // Package the output folder
     "buildResources": "resources"."output": "dist/"
   }
   "files": [ // The source file remains after packaging
     "main_process/"."render_process/",]."mac": { // MAC package configuration
     "target": "dmg"."icon": "icon.ico"
   },
   "win": { // Windows package configuration
     "target": "nsis"."icon": "icon.ico"
   },
   "dmg": { // DMG file package configuration
     "artifactName": "electron_react.dmg"."contents": [{"type": "link"."path": "/Applications"."x": 410."y": 150
       },
       {
         "type": "file"."x": 130."y": 150}},"nsis": { // nsis file package configuration
     "oneClick": false."allowToChangeInstallationDirectory": true."shortcutName": "electron-react"}},Copy the code

When you run the electron builder package command, you can specify parameters for the package.

-- MAC, -m, -O, -- macOS MacOS package -- Linux, -l Linux package --win, -w, -- Windows Windows package -- MWL for macOS, Windows and Linux package --x64 x64 (64-- Ia32 Ia32 (32Bit installation package)Copy the code

For the Update of the main process, you can use the auto-update module provided with auto-Builder, and the manual Update module is also implemented in auto-react. Due to space reasons, I will not repeat the details here. If you are interested, you can check the Update module under main on github.

13.3 Packaging Optimization

An App packaged with electron Builder is much bigger than a native client App with the same functionality, even if it is empty, it is larger than 100MB. There are many reasons:

The first point; To achieve this cross-platform effect, each Electron app includes the entire V8 engine and Chromium kernel.

Second point: the whole node_modules will be packed into the package. Everyone knows that the node_module of an application is very large, which is also the reason for the large size of Electron application after packaging.

The first cannot be changed, but the application volume can be optimized from the second: Electron packs only the dependencies of Denpendencies, not the dependencies of devDependencies. Therefore, dependencies in Denpendencies should be minimized. In the above process, we packaged the renderer process with WebPack, so all renderer dependencies can be moved into devDependencies.

Alternatively, we can optimize this by using a dual packajson.json approach, placing dependencies that are only used in the development environment in package.json at the root of the entire project, and placing platform-specific or runtime dependencies in the app directory. See two-package-structure for details.

reference

  • electronjs.org/docs
  • Jlord. Us/essential – e…
  • Imweb. IO/topic / 5 b9f5…
  • www.jianshu.com/p/1ece6fd7a…
  • zhuanlan.zhihu.com/p/52991793

The project source code address: github.com/ConardLi/el…

summary

I hope you have achieved the following points after reading this article:

  • To understandElectronThe basic operation principle of
  • masterElectronCore fundamentals of development
  • To understandElectronBasic use of frame, printing, protection, packaging and other functions

If there are any mistakes in this article, please correct them in the comments section. If this article has helped you, please like it and follow it.

Want to read more quality articles, can follow my Github blog, your star✨, like and follow is my continuous creation power!

I recommend you to follow my wechat public account [Code Secret Garden] and push high-quality articles every day. We can communicate and grow together.

After concern public number reply [add group] pull you into high quality front end communication group.