Using JS to Develop Cross-Platform Desktop Applications, From Principle to Practice

Introduction

UseElectronThe development of client programs has been going on for some time, the overall feeling is still very good, and there are also some pits encountered. This article is from [operation principle] to [practical application]ElectronMake a systematic summary. [Multi-map, Long Wen Warning ~]

All the example codes in this article are in mygithub electron-reactIn fact, reading articles in combination with code is more effective. In additionelectron-reactCan also be used asElectron + React + Mobx + WebpackScaffolding Project in technology stack.

I desktop applications

Desktop applications, also known as GUI programs (Graphical User Interface), are somewhat different from GUI programs. The desktop application program changes the GUI program from GUI to “desktop”, making the cold wood-like computer concept more humanized, vivid and energetic.

All kinds of client programs used on our computers belong to desktop applications. In recent yearsWEBAnd the rise of mobile terminals makes desktop applications dim gradually, but desktop applications are still essential in some daily functions or industrial applications.

Traditional desktop application development methods are generally the following two types:

1.1 Primary Development

Directly compile the language into executable files and directly call the systemAPITo complete UI drawing, etc. This kind of development technology has higher operating efficiency, but generally speaking, the development speed is slower and the technical requirements are higher, such as:

  • UseC++ / MFCexploitationWindowsApplication
  • UseObjective-CexploitationMACApplication

1.2 Managed Platform

From the beginning, there were local development and UI development. After one compilation, intermediate files are obtained, and secondary loading compilation or interpretation operation is completed through a platform or a virtual machine. The operating efficiency is lower than that of native compilation, but the efficiency is also considerable after platform optimization. In terms of development speed, it is faster than native compilation technology. For example:

  • UseC# / .NET Framework(Can only be developedWindows application)
  • Java / Swing

However, the above two types are too unfriendly to front-end developers, which are basically areas that front-end developers will not cover. However, in this era of “big front-end”, front-end developers are trying to get involved in various fields and use them.WEBThe way to develop the client side of technology was born.

1.3 WEB development

UseWEBTechnology development, using browser engine to completeUIRendering, utilizingNode.jsImplement server sideJSProgram and call the systemAPI, you can imagine it as a set of a client shellWEBApplication.

On the interface,WEBThe powerful ecology ofUIIt brings infinite possibilities, and the development and maintenance costs are relatively low, includingWEBFront-end developers with development experience can easily get started with development.

This article focuses on the use ofWEBOne of the technologies for developing client programselectron

Second, Electron

ElectronByGithubDevelop, useHTML,CSSAndJavaScriptTo build an open source library of cross-platform desktop applications.ElectronBy incorporatingChromiumAndNode.jsMerge into the same runtime environment and package it asMac,WindowsAndLinuxThe system is applied to achieve this goal.

2.1 Reasons for Using Electron Development:

  • 1. Use a strong ecologicalWebTechnology development, development cost is low, scalability is strong, more coolUI
  • 2. Cross-platform, a set of code can be packaged asWindows、Linux、MacThree sets of software and fast compilation
  • 3. Can be directly in the existingWebThe application is expanded to provide capabilities that browsers do not have.
  • You are a front end ~

Of course, we must also recognize its shortcomings: its performance is lower than that of native desktop applications, and the final packaged applications are much larger than native applications.

2.2 Development Experience

Compatibility

Although you still use itWEBTechnology development, but you don’t have to consider compatibility issues, you just need to care about your current use.ElectronThe version of corresponds toChromeIn general, it is new enough for you to use the latest versionAPIAnd you can upgrade manuallyChromeVersion. Similarly, you don’t need to consider the compatibility of different browser styles and codes.

Node environment

This may be a function that many front-end developers have dreamed of before, inWEBUsed in the interfaceNode.jsThe power providedAPIThis means that you are inWEBThe page can directly operate files and call the system.API, and even operate the database. Of course, in addition to the completeNode APIYou can also use hundreds of thousands more.npmModules.

Cross-domain

You can use it directlyNodeProvidedrequestModules make network requests, which means you don’t need to be bothered by cross-domains.

Powerful scalability

With the help ofnode-ffiTo provide powerful extensibility for applications (described in detail in the following sections).

2.3 who is using Electron

At present, there are already many applications in use on the market.ElectronHas been developed, including those we are familiar with.VS CodeClients,GitHubClients,AtomClient, etc. Impressive, Thunderbolt released Thunderbolt X last year10.1When the copy:

Starting from Thunderbolt X version 10.1, we have completely rewritten Thunderbolt’s main interface using the Electron software framework. Thunderbolt X using the new framework can perfectly support high-definition displays such as 2K and 4K, and the text rendering in the interface is clearer and sharper. From a technical point of view, the interface drawing and event handling of the new framework are more flexible and efficient than those of the old framework, so the smoothness of the interface is also significantly better than that of the old framework. How big is the specific promotion? You will know when you try.

You can open itVS Code, click [Help] [Switch Developer Tools] to debugVS CodeThe interface of the client.

Third, the principle of Electron operation

ElectronCombinedChromiumNode.jsAnd for calling local functions of the operating systemAPI.

3.1 Chromium

ChromiumYesGoogleFor developmentChromeOpen source projects launched by browsers,ChromiumEquivalent toChromeNew features will take the lead in the engineering or experimental version ofChromiumOnly after verification will it be applied toChromeOn, thereforeChromeThe function of will be relatively backward but stable.

ChromiumForElectronProvide powerfulUIAbility to develop interfaces without considering compatibility.

3.2 Node.js

Node.jsIs a letJavaScriptThe development platform running on the server side,NodeUse event-driven, non-blockingI/OThe model is lightweight and efficient.

By itselfChromiumIs can’t have direct operation of nativeGUICapable,ElectronInternal integrationNodejs, which allows it to have the operating system bottom layer while developing the interface.APIThe ability to,NodejsCommonly used inPath、fs、CryptoModules such asElectronCan be used directly.

3.3 system apis

In order to provide a native systemGUISupport,ElectronBuilt-in native application program interface supports calling some system functions, such as calling system notifications and opening system folders.

In the development mode,ElectronCalling systemAPIAnd we will take a look at the followingElectronAbout how processes are divided.

3.4 Main Process

ElectronTwo processes are distinguished: the main process and the rendering process, each responsible for its own functions.

Electronrunpackage.jsonThemainThe process of the script is called the main process. OneElectronApplications always have and only have one main process.

Responsibilities:

  • Create rendering process (multiple)
  • Control the application life cycle (start, exitAPPAnd yesAPPDo some event monitoring)
  • Call the system’s underlying functions, call native resources

API callable:

  • Node.js API
  • ElectronMain process providedAPI(including some system functions andElectronAdditional functions)

3.5 Rendering Process

Due toElectronUsedChromiumTo showwebPage, soChromiumThe multi-process architecture of is also used. Each ..Electronhit the targetwebThe page runs in its own rendering process.

The main process uses the BrowserWindow instance to create the page. Each BrowserWindow instance runs the page in its own rendering process. When a BrowserWindow instance is destroyed, the corresponding rendering process is also terminated.

You can think of the rendering process as a browser window, which can have multiple and independent windows, but unlike browsers, it can callNode API.

Responsibilities:

  • UseHTMLAndCSSRendering interface
  • UseJavaScriptDo some interface interaction

API callable:

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

IV. Electron Foundation

4.1 Electron API

In the above chapter, we mentioned that rendering in and main process can be called respectivelyElectron API. AllElectronTheAPIAre assigned to a process type. ManyAPICan only be used in the main process, someAPIIt can only be used in the rendering process, and some main processes and rendering processes can be used.

You can obtain it byElectron API

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

Here are some common onesElectron API

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

4.2 use the apis of Node.js

You can be at the same timeElectronUsed by the main process and rendering process ofNode.js APIAll inNode.jsCan be usedAPIInElectronCan also be used in.

import {shell} from 'electron';
 import os from 'os';
 
 document.getElementById('btn').addEventListener('click', () => {
 shell.showItemInFolder(os.homedir());
 })

There is a very important hint: the native Node.js module (that is, the module that needs to be compiled before it can be used) needs to be compiled before it can be used with Electron.

4.3 Process Communication

Although the main process and the rendering process have different responsibilities, they also need to cooperate and communicate with each other.

For example, inwebPage management nativeGUIResources are very dangerous and can easily be leaked. So inwebPage, do not allow direct calls to nativeGUIcorrelativeAPI. If you want the rendering process to be nativeGUIOperations, you must communicate with the main process, request the main process to complete these operations.

4.4 Rendering Process Communicates to Main Process

ipcRendererIs aEventEmitterAn instance of. You can use some of the methods it provides to send synchronous or asynchronous messages from the rendering process to the main process. You can also receive a reply from the main process.

Introduced in rendering processipcRenderer

import { ipcRenderer } from 'electron';

Asynchronous transmission:

viachannelSending a synchronization message to the main process can carry any parameters.

Internally, the parameter is serialized asJSONTherefore, functions and prototype chains on parameter objects will not be sent.

Send ('sync-render',' I am an asynchronous message from the rendering process');

Synchronous transmission:

Constmsg = ipcrender.sendsync ('async-render',' i am a synchronization message from the rendering process');

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

The main process listens for messages:

ipcMainModule isEventEmitterAn instance of the class. When used in the main process, it processes asynchronous and synchronous information sent from the renderer process (web page). Messages sent from the renderer process will be sent to the module.

ipcMain.on: monitoringchannelWhen a new message is receivedlistenerWill takelistener(event, args...)The form of is called.

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

4.5 Communication between Main Process and Rendering Process

Can be passed in the main processBrowserWindowThewebContentsSends a message to the rendering process, so you must first find the corresponding rendering process’sBrowserWindowObject. :

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

According to the source of the message:

InipcMainIn the callback function that accepts the message, through the first parametereventProperty ofsenderCan get the source rendering processwebContentsObject, with which we can directly respond to messages.

ipcMain.on('sync-render', (event, data) => {
 console.log(data);
 Event.sender.send('main-msg',' main process received [asynchronous] message from rendering process!'  )
 });

Rendering process listening:

ipcRenderer.on: monitoringchannelWhen the new message arrives, it will pass throughlistener(event, args...)calllistener.

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

4.6 Communication Principle

ipcMainAndipcRendererAre allEventEmitterAn instance of the class.EventEmitterClass isNodeJSThe basis of the event, which consists ofNodeJShit the targeteventsModule export.

EventEmitterThe core of is the encapsulation of event triggering and event listener functions. It implements the interfaces required by the event model, includingaddListener,removeListener,emitAnd other tools and methods. same as nativeJavaScriptEvents are similar, adopting a publish/subscribe (observer) approach, using internal_eventsList to record registered event handlers.

We passedipcMainAndipcRendererTheon、sendBoth listening and sending messages areEventEmitterRelated interfaces defined by.

4.7 remote

remoteThe module communicates between the rendering process (web page) and the main process (IPC) provide a simple method. UseremoteModule, you can callmainProcess object without explicitly sending interprocess messages, similar toJavaTheRMI.

import { remote } from 'electron';

Remote.dialog.showErrorBox ('dialog module unique to the main process',' I called using remote')

But in fact, when we call methods and functions of remote objects or create a new object through remote constructors, we are actually sending a synchronous interprocess message.

Pass aboveremoteModule calldialogIn the case of. We created in the rendering processdialogThe object is not actually in our rendering process, it just lets the main process create onedialogObject and returned the corresponding remote object to the rendering process.

4.8 Communication between Rendering Processes

ElectronThere is no way for rendering processes to communicate with each other. We can set up a message transfer station in the main process.

Communication between rendering processes first sends a message to the main process, and the transfer station of the main process distributes the message according to conditions after receiving the message.

4.9 Rendering Process Data Sharing

The easiest way to share data between two rendering processes is to use a browser that has already been implemented.HTML5 API. The better one is to useStorage API,localStorage,sessionStorageOr ..IndexedDB。

Just like being used in a browser, this storage is equivalent to permanently storing a portion of data in an application. Sometimes you don’t need such storage, you just need to share some data in the life cycle of the current application. At this time you can useElectronInternalIPCImplementation of the mechanism.

The data is stored in a global variable of the main process and then used in multiple rendering processesremoteModule to access it.

Initialize global variables in the main process:

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

Read during rendering:

import { ipcRenderer, remote } from 'electron';
 
 const { getGlobal } = remote;
 
 const mainId = getGlobal('mainId')
 const dirname = getGlobal('__dirname')
 const deviecMac = getGlobal('device').mac;

Changes during rendering:

Getglobal ('myfield'). name =' code secret garden';

Multiple rendering processes share the global variables of the same main process, thus achieving the effect of data sharing and transfer of rendering processes.

V. windows

5.1 BrowserWindow

Main process moduleBrowserWindowUsed to create and control browser windows.

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

You can stay atHereView all its construction parameters.

5.2 frameless windows

Frameless windows are windows without borders, and parts of windows (such as toolbars) are not part of a web page.

InBrowserWindowIn the construction parameters of, will beframeSet tofalseYou can specify the window as a borderless window. Hiding the toolbar will cause two problems:

  • 1. The window control buttons (minimize, full screen, close button) will be hidden
  • 2. Cannot drag and drop the moving window

You can specifytitleBarStyleOption to display the toolbar button again and set it tohiddenIndicates that a full-size content window with hidden title bar is returned, and there is still a standard window control button in the upper left corner.

new BrowserWindow({
 width: 200,
 height: 200,
 titleBarStyle: 'hidden',
 frame: false
 });

5.3 Window Drag

By default, borderless windows cannot be dragged. We can go through the interfaceCSSAttribute-webkit-app-region: dragSet the drag area manually.

In the frameless window, the dragging behavior may conflict with the selected text, which can be set by-webkit-user-select: none;Disable text selection:

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

On the contrary, it is set inside the draggable area.-webkit-app-region: no-dragA specific non-draggable area can be specify.

5.4 Transparent Window

By incorporatingtransparentThe option is set totrue, you can also make frameless windows transparent:

new BrowserWindow({
transparent: true,
frame: false
});

5.5 Webview

UsewebviewLabel atElectronEmbedding “foreign” content in applications. Foreign content is contained inwebviewIn a container. Embedded pages in applications can control the layout and redrawing of foreign content.

AndiframeDifferent,webviewRun in a different process than the application. It does not have the same permissions as your web page, and all interactions between the application and embedded content will be asynchronous.

VI. Dialog Box

dialogThe module providesapiTo show native system dialog boxes, such as opening a file box,alertBox, sowebApplications can bring users the same experience as system applications.

Note: dialog is the main process module. You can use remote to call in the rendering process.

6.1 Error Prompt

dialog.showErrorBoxUsed to display a modal dialog box that displays error messages.

Remote.dialog.showErrorBox ('error',' this is an error bullet!'  )

6.2 dialog box

dialog.showErrorBoxUsed to call the system dialog box, you can specify several different types for: “none“, “info“, “error“, “question“or”warning“。

On Windows, “question” and “info” display the same icon 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:' tip message',
Message:' This is a dialog box!'  ,
Buttons: ['OK',' Cancel']
}, (index) => {
Setstate: ` [you clicked ${index?  Cancel':' OK'}!  !  】` })
})

6.3 File Frame

dialog.showOpenDialogUsed to open or select a system directory.

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

6.4 Information Box

Direct use is recommended here.HTML5 API, which can only be used in the renderer process.

let options = {
 Title:' information box title',
 Body:' I am a message ~ ~ ~',
 }
 let myNotification = new window.Notification(options.title, options)
 myNotification.onclick = () => {
 Setstate:' [you clicked on the message box!  !  】' })
 }

VII. System

7.1 Access to System Information

viaremoteGets the of the main processprocessObject, you can get various version information of the current application:

  • process.versions.electronelectronVersion information
  • process.versions.chromechromeVersion information
  • process.versions.nodenodeVersion information
  • process.versions.v8v8Version information

Get the current application root directory:

remote.app.getAppPath()

UsenodeTheosThe module obtains the current system root directory:

os.homedir();

7.2 Copy and Paste

ElectronProvidedclipboardBoth the rendering process and the main process can be used to perform copy and paste operations on the system clipboard.

Write to clipboard as plain text:

clipboard.writeText(text[, type])

Gets the contents of the clipboard as plain text:

clipboard.readText([type])

7.3 screenshots

desktopCapturerInformation about media sources used to capture audio and video from the desktop. It can only be called during rendering.

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

getImg = () => {
This.setState({ imgMsg:' intercepting 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 msg: `screenshots saved to: ${screenshotPath}`})
})
}
})
})
}

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

Viii. menu

The menu of the application program can help us to reach a certain function quickly without using the interface resources of the client. General menus are divided into two types:

  • Application menu: it is located at the top of the application and can be used globally.
  • Context menu: you can customize the display of any page and customize the call, such as right-click menu.

ElectronHas provided us withMenuThe module is used to create native application menu and context menu. It is a main process module.

You can passMenuThe static method ofbuildFromTemplate(template)To construct a menu object using a custom menu template.

templateIs aMenuItemLet’s take a look at the array ofMenuItemSeveral important parameters of:

  • label: Text to be Displayed in Menu
  • click: Event handling function after clicking menu
  • role: system predefined menu, for examplecopy(Copy),paste(paste),minimize(Minimize) …
  • enabled: Indicates whether the item is enabled. This property can be changed dynamically
  • submenu: submenu, also aMenuItemAn array of

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

The following example is a simple menutemplate.

const template = [
{
Label:' file',
submenu: [
{
Label:' new file',
click: function () {
dialog.showMessageBox({
type: 'info',
Message:' hey!'  ,
Detail:' you clicked on the new file!'  ,
})
}
}
]
},
{
Label:' edit',
submenu: [{
Label:' cut',
role: 'cut'
}, {
Label:' copy',
role: 'copy'
}, {
Label:' paste',
role: 'paste'
}]
},
{
Label:' minimize',
role: 'minimize'
}
]

8.1 Application Menu

UseMenuThe static method ofsetApplicationMenu, you can create an application menu in theWindowsAndLinuxIn fact,menuWill be set as the top menu for each window.

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

We can handle menus differently according to different life cycles of applications and different systems.

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)
 }

8.2 Context Menu

UseMenuThe example method ofmenu.popupThe pop-up context menu can be customized.

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

8.3 Shortcut Keys

In the menu options, we can specify oneacceleratorProperty to specify the shortcut key for the operation:

{
 Label:' minimize',
 accelerator: 'CmdOrCtrl+M',
 role: 'minimize'
 }

In addition, we can also useglobalShortcutTo register global shortcut keys.

globalShortcut.register('CommandOrControl+N', () => {
dialog.showMessageBox({
type: 'info',
Message:' hey!'  ,
Detail:' You triggered the shortcut key for manual registration.',
})
})

CommandOrControl stands for Command on macOS and Control on Linux and Windows.

IX. Printing

In many cases, the printing used in the program is imperceptible to the user. And if you want to flexibly control the printed content, you often need to use the printer to provide it to us.apiFurther development is very complicated and difficult. For the first time in businessElectronIn fact, it uses its printing function, so here we will introduce some more.

ElectronThe provided printing api can control the display of printing settings very flexibly, and can write the printed content through html.ElectronThere are two ways to print, one is to directly call the printer to print, the other is to print topdf.

And there are two kinds of objects that can call printing:

  • viawindowThewebcontentObject, using this method requires a separate printed window, which can be hidden, but communication calls are relatively complex.
  • The that uses the pagewebviewElement to call printing, you canwebviewHidden in the calling page, the communication method is relatively simple.

The above two ways have at the same timeprintAndprintToPdfMethods.

9.1 Call System Print

contents.print([options], [callback]);

There are only three simple configurations in print options:

  • silent: Does the printing configuration not appear when printing (silent printing)
  • printBackground: print background
  • deviceName: Printer Device Name

First of all, we should configure the printer name we use, and first determine whether the printer is available before calling printing.

UsewebContentsThegetPrintersThe method can obtain a list of printers that the current device has configured. Note that the configured printers are not available, but only drivers have been installed on this device.

viagetPrintersGets the printer object:https://electronjs.org/docs/a …

We only care about two here,nameAndstatus,statusFor0When, the printer is available.

printThe second parameter of thecallbackIs used to determine whether the print job issued a callback, rather than a callback after the completion of the print job. Therefore, when a general print job is issued, the callback function will call and return parameterstrue. This callback cannot determine whether the printing was really successful.

if (this.state.curretnPrinter) {
 mainWindow.webContents.print({
 silent: silent, printBackground: true, deviceName: this.state.curretnPrinter
 }, () => { })
 } else {
 Remote.dialog.showErrorBox ('error',' please select a printer first!'  )
 }

9.2 Print to PDF

printToPdfThe basic usage of andprintSame, butprintThere are very few configuration items for, andprintToPdfMany attributes are extended. I looked through the source code here and found that there are still many that have not been pasted into the api. There are about 30 that can be configured to print margin, print header and footer, etc.

contents.printToPDF(options, callback)

callbackFunction is called after printing failure or printing success to obtain printing failure information or includePDFBuffer for data.

const pdfPath = path.join(os.tmpdir(), 'webviewPrint.pdf');
 const webview = document.getElementById('printWebview');
 Const renderHtml =' i was temporarily inserted into webview ...';
 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 })
 });
 });

The printing in this example useswebviewCompleted by callingexecuteJavaScriptThe method can dynamicallywebviewInsert printed content.

9.3 Selection of Two Printing Schemes

As mentioned above, usingwebviewAndwebcontentCan call the printing function, usewebcontentIn order to print, there must first be a print window. This window cannot be printed and created at any time, which consumes a lot of performance. You can start it when the program is running and monitor events.

This process needs to communicate with the caller of printing. The general process is as follows:

Visible communication is very complicated, usewebviewPrinting can achieve the same effect, but the communication method will become simple because the rendering process andwebviewCommunication does not need to go through the main process, and can be done in the following ways:

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')
 })

Previously specifically forELectronPrint and write oneDEMOelectron-print-demoIf you are interested, you cancloneCome down and have a look.

9.4 Printing Function Package

The following are several tool function packages for common printing functions.

/**
 * Get a list of system printers
 */
 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);
 }
 /**
 * check whether a print driver is installed
 */
 export function checkDriver(driverMame) {
 return getPrinters().find(element => (element.options["printer-make-and-model"] || '').includes(driverMame));
 }
 /**
 * according to the printer name to obtain the printer object
 */
 export function getPrinterByName(name) {
 return getPrinters().find(element => element.name === name);
 }

X. procedural protection

10.1 collapse

Crash monitoring is an essential protection function for every client program. When a program crashes, we generally expect to do two things:

  • 1. Upload crash log and call the police in time
  • 2. The monitoring program crashed, prompting the user to restart the program

electronFor uscrashReporterTo help us record crash logs, we cancrashReporter.startTo create a crash reporter:

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

When the program crashes, the crash log will be stored in a temporary folder namedYourName CrashesIn the file folder of the.submitURLUsed to specify your crash log upload server. Before starting the crash reporter, you can call theapp.setPath('temp', 'my/custom/temp')The API defines the path to save these temporary files. You can also passcrashReporter.getLastCrashReport()To get the date and time of the last crash reportID.

We can pass.webContentsThecrashedTo monitor the collapse of the rendering process. In addition, it has been tested that the collapse of some main processes will also trigger this event. So we can, according to the LordwindowWhether it is destroyed or not to judge different restart logic, the following logic makes the whole crash monitoring:

import { BrowserWindow, crashReporter, dialog } from 'electron';
 //Open Process Crash Record
 crashReporter.start({
 productName: 'electron-react',
 companyName: 'ConardLi',
 Submit url:' http://xxx.com',' interface to upload crash log
 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: ['Overload',' Close']
 }
 dialog.showMessageBox(options, (index) => {
 if (index === 0) mainWin.reload();
 else mainWin.close();
 })
 }
 }
 export default function () {
 const mainWindow = BrowserWindow.fromId(global.mainId);
 mainWindow.webContents.on('crashed', () => {
 const errorMessage = crashReporter.getLastCrashReport();
 Error ('program crashed!'  , errorMessage);  //Log can be uploaded separately
 reloadWindow(mainWindow);
 });
 }

10.2 Minimize to Tray

Sometimes we don’t want the user to close the program when he clicks the close button, but to minimize the program to the tray and do the real exit operation on the tray.

First of all, we should monitor the closing event of the window, prevent the default behavior of the user’s 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 program 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);
 });
 }

At this time, the program can no longer be found, and there is no our program in the task tray, so we must first create the task tray and monitor the events.

Windows platform usageicoFiles can achieve better results

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:' display main interface', click: () => {
mainWindow.show();
mainWindow.setSkipTaskbar(false);
}
},
{
Label:' exit', click: () => {
mainWindow.destroy();
app.quit();
}
},
])
tray.setToolTip('electron-react');
tray.setContextMenu(contextMenu);
}

XI. Expanding Capacity

In many cases, your application needs to interact with external devices. In general, manufacturers will provide you with development kits for hardware devices. These development kits basically pass throughC++Write, in useelectronIn case of development, we do not have direct callC++The ability of code, we can take advantage ofnode-ffiTo realize this function.

node-ffiProvides a powerful set of tools forNode.jsUse pure in the environmentJavaScriptCall the dynamic link library interface. It can be used to build interface bindings for libraries without using anyC++Code.

pay attention tonode-ffiIt cannot be called directlyC++Code, you need toC++Code Compiled as Dynamic Link Library: InWindowsNext isDllInMac OSNext isdylib ,LinuxYesso.

node-ffiLoadLibraryThere is a limit, can only deal withCStylishLibrary.

The following 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) {
Log ('call failed ~', error);
}
}

this.testCppMethod('ConardLi',123);

In the above code, we useffiPackingC++Dynamic Link Library Generated by Interfacetest.dllAnd use therefDo some type mapping.

UseJavaScriptWhen calling these mapping methods, it is recommended to use theTypeScriptBecause of the weak typeJavaScriptThere may be unexpected risks when calling interfaces in strongly typed languages.

With this capability, front-end development engineers can alsoIOTThe field is showing its prowess ~

XII. Environmental Selection

In general, our applications may run in multiple environments (productionbetauatmokedevelopment…), different development environments may correspond to different back-end interfaces or other configurations, we can build a simple environment selection function in the client program to help us develop more efficiently.

The specific strategies are as follows:

  • In the development environment, we directly enter the environment selection page, read the selected environment and then redirect the response.
  • Select the entry in the menu reservation environment to switch during the development process.
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") {
 return urlBeta;
 }
 },
 isDebugger: function () {
 return build.env === 'development'
 }
 }

XIII. Packing

The last and most important step is to package the written code into executable code..appOr.exeExecutable file.

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

13.1 Package and Upgrade of Rendering Process

In general, most of our business logic code is completed in the rendering process. In most cases, we only need to update and upgrade the rendering process without changing the main process code. The packaging of our rendering process is actually normal.webThere is not much difference in project packaging, usewebpackJust pack it.

Here I will talk about the benefits of separate packaging of rendering processes:

PackedhtmlAndjsFile, we usually have to upload to our front-end static resource server, and then inform the server that our rendering process has code updates, which can be said to be a separate upgrade of the rendering process.

Note that unlike the shell upgrade, the upgrade of the rendering process is only on the static resource serverhtmlAndjsThe update of files does not need to be downloaded from the update client again, so when we start the program, we can directly refresh and read the latest version of static resource files whenever we detect that there is an update in the offline package. Even if we have to force the update during the program running, our program only needs to force the refresh page to read the latest static resources. Such an upgrade is very user-friendly.

Note here that once we configure it this way, it means that the rendering process and the main process are completely separated from each other for packaging and upgrading. The files we read when starting the main window should no longer be local files, but files that are put on the static resource server after packaging is completed.

To facilitate development, we can distinguish between loading different files locally and online:

function getVersion (mac,current){
//Obtain the latest version according to the mac and current version of the device
}
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" }
});
}

SpecificwebpackThe configuration will no longer be posted here and can be posted to mine.github electron-reactThe/scriptsLook under the directory.

It should be noted here that in the development environment we can combinewebpackThedevServerAndelectronCommand to startapp

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

13.2 main process packaging

The main process, that is, the whole program is packaged into a client program that can be run. There are generally two common packaging schemes.electron-packagerAndelectron-builder.

electron-packagerI think the packaging configuration is a bit cumbersome, and it can only package applications directly into executable programs.

I recommend it here.electron-builder, it not only has convenient configurationprotocolThe function of, built-inAuto UpdateSimple configurationpackage.jsonThe whole packaging work can be completed, and the user experience is very good. Andelectron-builderNot only can applications be packaged directly intoexe appSuch as executable programs, can also be packaged intomsi dmgSuch as installation package format.

You can stay atpackage.jsonEasy to configure:

"build": {
 "productname": "electron-reach",//app chinese name
 "appid": "electron-reach",//app logo
 "directories": {// Packed and Output Folder
 "buildResources": "resources",
 "output": "dist/"
 }
 "files": [// source files that remain after packaging
 "main_process/",
 "render_process/",
 ],
 "mac": {// mac Packaging Configuration
 "target": "dmg",
 "icon": "icon.ico"
 },
 "win": {// windows packaging configuration
 "target": "nsis",
 "icon": "icon.ico"
 },
 "dmg": {// dmg File Packaging Configuration
 "artifactName": "electron_react.dmg",
 "contents": [
 {
 "type": "link",
 "path": "/Applications",
 "x": 410,
 "y": 150
 },
 {
 "type": "file",
 "x": 130,
 "y": 150
 }
 ]
 },
 "nsis": {// nsis File Packaging Configuration
 "oneClick": false,
 "allowToChangeInstallationDirectory": true,
 "shortcutName": "electron-react"
 },
 }

carry outelectron-builderWhen packing commands, you can specify parameters to pack.

-mac,-m,-o,-macos macos packaging
 -linux,-linux packaging
 -win,-w,-windows package
 --mwl packages macOS, Windows and Linux at the same time
 -x64 x64 (64-bit installation package)
 -IA32IA32 (32-bit installation package)

You can use the update on the main processelectron-builderOwnAuto UpdateModule, atelectron-reactThe manual update module has also been implemented, which will not be repeated here due to space reasons. If you are interested, please contact me atgithubexaminemaininferiorupdateModules.

13.3 Packaging Optimization

electron-builderPacked upAppIt is much larger than the native client application with the same function, even if it is empty, the volume should be at100mbAbove. There are many reasons:

The first point is: In order to achieve a cross-platform effect, eachElectronApplications include the entireV8Engines andChromiumThe kernel.

The second point: when packing, the wholenode_modulesPacked in, everyone knows an applicationnode_moduleThe volume is very large, which also makesElectronThe reason for the large volume after packaging is applied.

The first point we cannot change, we can optimize the application volume from the second point:ElectronWhen packaging, only thedenpendenciesInstead of packagingdevDependenciesTo package the dependencies in the. So we should reduce it as much as possible.denpendenciesThe dependence in. In the above process, we usedwebpackPackage the rendering process, so all the dependencies of the rendering process can be moved in.devDependencies.

In addition, we can also use doublepackajson.jsonIn order to optimize it, the dependency that is only used in the development environment is placed in the root directory of the whole project.package.jsonNext, install the platform-related or runtime-required dependencies on theappUnder the directory. For details, seetwo-package-structure.

References

Source code address of this project:https://github.com/ConardLi/e …

Summary

I hope you can reach the following points after reading this article:

  • UnderstandElectronThe basic operating principle of
  • MasterElectronCore Basic Knowledge of Development
  • UnderstandElectronOn the basic use of functions such as bullet frame, printing, protection, packaging, etc.

If there are any mistakes in the article, please correct them in the comment area. If this article helps you, please comment and pay attention.

If you want to read more quality articles, please pay attention to me.github博客Your star✨, praise and attention are the driving force of my continuous creation!

It is recommended to pay close attention to my WeChat public number “code Secret Garden” and push high-quality articles every day so that we can communicate and grow together.

图片描述

After paying attention to the public number, reply [Add Group] to pull you into the high-quality front-end communication group.