preface

There is a saying that “where there is a demand, there is a market”, and the same is true in technology. In the past front-end field, when facing the need to involve the operating system, front-end coder often seems to be inadequate. It was the need for desktop applications that led to Electron’s arrival.

What is Electron?

Introduction to the

Go to the website and you’ll find an introduction to building cross-platform desktop applications using JavaScript, HTML, and CSS. As the name suggests, we can build cross-platform desktop applications with complete autonomy, no need to rely heavily on native desktop developers, effectively reduce communication costs, no need to ask grandparents to coordinate resources, and full access to operating system related underlying apis that were previously limited.

Of course, this does not mean that the benefits are all bad, after all, when gaining more power, they will also bear more risks.

Good application

  • Visual Studio Code
  • Atom
  • Postman
  • WhatsApp
  • MongoDB desktop management tool Compass
  • Interface management tool Apifox
  • . .

Technology selection

Electron core composition

Electron is based on Chromium and Node, which makes it seamless and easy to use to develop cross-platform desktop applications, lowering the learning threshold and making it easier to get started.

To compensate for the lack of front-end access to system apis, Electron encapsulates the system apis internally, including system dialog boxes, system trays, system menus, and clipboards. Other low-level support, such as network access control and local file systems, is provided by Node.

Electron breaks through the Node and Chromium event cycles through the message loop between operating systems to ensure the loose coupling between them. Then introduced the main process, the concept of rendering process.

A new secure thread is launched for polling. When Nodejs has a new event, PostTask forwards it to the Chromiums event loop to complete the Electron event fusion

Specific related source: github.com/electron/el…

Electron working mechanism

Don’t say anything. Just do the picture

In our traditional development, the front end personnel can control the display area on the left, while when developing desktop application based on Electron, we can control the area as shown on the right, which is all independently developed by the front end.

In Electron development, the page is no longer opened manually by user input, but is developed independently by hard coding.

The Electron application consists of the main process and the rendering process, which correspond to the upper and lower parts in the figure on the right.

process

A Electron application consists of one and only one main process plus multiple renderers.

The main process

Function: bridge function, connect the operating system and render process, responsible for managing all Windows and their corresponding render process.

  • There is one and only one, the entire app portal
  • Create and manage renderers
  • Control the application life cycle
  • Use the NodeJS feature
  • Call the operating system API
  • .

Rendering process

Functions: responsible for rendering the page, receiving user input, corresponding user interaction, etc.

  • To render the page
  • Use part of the Electron module API
  • Use the NodeJS feature
  • An application can have multiple renderers
  • Controls the user interaction logic
  • To access the Dom API

Status of core module ownership

The above figure is the attribution of common modules sorted out by the author. Detailed main process and rendering process will be partially explained in the subsequent actual combat part.

The IPC communication

Now that we know roughly what the two processes do, it’s time to think about how they communicate with each other.

Electron provides ipcMain and ipcRenderer as a communication bridge between the main and rendering processes.

From the interface definition, it is not difficult to infer that its pipe IPC implements IpcMain and IpcRenderer by inheriting EventEmitter, and expands other tool class methods. Even if glance through electron source is also so, interested students can go to study, here only do a simple understanding.

At this point, communication between the main process and the renderer process is easy to understand, via pipeline IPC, using the well-known publish-subscribe model.

The window to get

  • BrowserWindow. The keyboardfocusmanager.getfocusedwindow () : get the current active window
  • Remote.getcurrentwindow (): Gets the current renderer process association window
  • Browserwindow.fromi (id): Gets window instances by ID
  • BrowserWindow. GetAllWindow () : get all the Windows

remote

Before getting into the basics of the actual project, I’ll introduce a special remote module

Remote: This is the Electron internal module through which the renderer can access the main process’s modules, objects, and methods. Remote can still perform operations in the renderer that should be done by the main process, such as creating Windows and menus in the renderer process. The premise is that the nodeIntegration configuration is enabled when the window is created, giving the renderer the ability to access the Node.js API. But the mechanism behind this is the same, by notifying the main process, which receives the message and does something about it, and then returns the relevant instance to the renderer as a remote object.

limitations

Of course, remote, while tremendously convenient for developers, also brings some limitations

  • High performance loss: cross-process operation
  • Create confusion: Asynchrony leads to out-of-order execution
  • Create illusion: Proxy objects mess up data
  • Security issues: malicious code attacks

The Remote module will be removed from inside ELECTRON in the near future, but it’s a long way off, so stay tuned.

In actual combat

From here, we will demonstrate the use of relevant core modules from the actual basic function exercise of the project.

Process of visits

Render process TO main process

The core rationale is that the Remote module is exposed, allowing developers to access it relatively freely.

For example, if we want to get the path of the application in the main process, we can get it in the main process like this:

import { app } from 'electron'
// Get the application path
const ROOT_PATH = app.getAppPath()
Copy the code

With the remote module in the renderer process, such simple attribute acquisition becomes more convenient:

const { app } = require('electron').remote
// Get the application path
const ROOT_PATH = app.getAppPath()
Copy the code

It can not only access the properties of the main process, but also call the related methods.

const { remote } = require('electron')
// The renderer process opens developer tools
remote.getCurrentWindow().webContents.openDevTools()
Copy the code

Conclusion: Through remote module, we can easily access the main process modules, objects and methods.

Main process TO render process

The renderer process is controlled by the main process and can be easily accessed through the created renderer window win.webContents object.

The related examples on the official website here are relatively perfect and can be viewed by yourself.

const {BrowserWindow} = require('electron')
let win = new BrowserWindow({width: 800.height: 600})
win.loadURL('http://github.com')
// Get the url of the current page window
let currentURL = win.webContents.getURL()
Copy the code

Process of communication

Its core is pipeline IPC communication, which has been described above and will not be repeated.

Main process TO render process

There are two main ways to communicate:

  • IpcMain receives renderer messages
  • WebContents is sent to the renderer process

For example, there is a place in the project where I need to listen for users to open the outer chain through a tag, but I don’t want it to create a new window, so I need system intervention to deal with it.

My solution is to open the target link through the system default browser through the process communication + shell module.

<a href="www.baidu.com" target="_blank">baidu</a>
Copy the code
const { ipcMain, shell } = require('electron');
ipcMain.on('open-url'.(event, url) = > {
  // 'open-url' is the pipe message name
  // event sends information about the message
  // Event. sender is the instance of the webContents object for the renderer
  // the url is the pass parameter
  
  // Open the target external link through the system default browser
  shell.openExternal(url)
})
Copy the code

If, at this point in time, we want to tell the renderer that we have successfully received and executed the callback, we can use the renderer instance to inform the renderer of the message:

Method 1: webContents is directly returned

const { ipcMain, shell } = require('electron');
const win = new BrowserWindow({
  / /... .
})
ipcMain.on('open-url'.(event, url) = > {
  / /... .
  // Open the target external link through the system default browser
  shell.openExternal(url)
  // Notify the renderer of a message
  win.webContents.send('ready-open-url')})Copy the code

Method 2: ipcmain. on receives message notification, event. Sender is the instance of webContents object of render process, we can also do message notification directly:

const { ipcMain, shell } = require('electron');
const win = new BrowserWindow({
  / /... .
})
ipcMain.on('open-url'.(event, url) = > {
  / /... .
  // Open the target external link through the system default browser
  shell.openExternal(url)
  // Notify the renderer of a message
  event.sender.send('ready-open-url')})Copy the code

Method 3: when ipcmain. on receives a message notification, the Event provides the reply method and the corresponding message is sent to the source renderer, essentially the same logic as method 2.

/ /... .
ipcMain.on('open-url'.(event, url) = > {
  / /... .
  // Open the target external link through the system default browser
  shell.openExternal(url)
  // Notify the renderer of a message
  event.replay('ready-open-url')})Copy the code

Render process TO main process

Mainly through the ipcRenderer module to the main process for message notification.

Again, in the example above, to open the outer chain, we need to inform the main process in the rendering process that I need to open some outer chain. Details are as follows:

This example is the practice in Vue

const { ipcRenderer } = require('electron')
const links = document.querySelectorAll('a[href]')
links.forEach(link= > {
  link.addEventListener('click'.e= > {
    const url = link.getAttribute('href')
    e.preventDefault()
    ipcRenderer.send('open-url', url)
  })
})
Copy the code

In some cases, we need to send synchronous messages, so we can set the value of returnValue directly in the main process, and the renderer process does not need to listen again.

Or take the above to open the outer chain to make a demonstration:

/ / main process
ipcMain.on('open-url'.(event, url) = > {
  // Open the target external link through the system default browser
  shell.openExternal(url);
  // Set the return value
  event.returnValue = 'success';
})

// Render process
const returnVal = ipcRenderer.sendSync('open-url', url);
console.log(returnVal) // success
Copy the code

Of course, synchronous communication will block the rendering process, so choose carefully

Render process TO render process

When our program is relatively complex and creates multiple renderers, it is easy to have multiple renderers communicate with each other.

The solution is also obvious, since the parent (main process) is born, then directly through the main process to make a transition, can achieve a communication between the two sides. After all, Windows are often created in the main process, which holds instances of all Windows and communicates with each other by getting the ID of the target window.

For each window, webcontents.getProcessid () or webcontents.id gets the id of the corresponding window.

The pseudocode is as follows:

Ipcrenderer. sendTo(win2.webcontents. id, 'send-msg', params1, Ipcrenderer. on(' sword-msg ', (event, params1, params2) => {//... . })Copy the code

In ipcrenderer. sendTo, the first parameter is the target window ID, the second parameter is the pipe message name, and the rest are transfer parameters.

Of course, you need to send the message to the target window is open, otherwise it can not receive.

At this point, the process communication of the three scenarios is described.

One small note to note ⚠️ :

In the process of communication between processes, the sent JSON objects are serialized and deserialized, so it is necessary to pay attention to the data in the method and prototype chain will not be passed.

Much like setData, a small program that transfers view-level and logic-level data, evaluteJavascript ultimately transfers to strings.

Setting up the development environment

The installation of electron may be a long process, and we strongly suggest that if you have conditions, you can surf the Internet scientifically, which can save a lot of trouble. Of course, if not, it doesn’t matter (fake), we have a solution.

Package management tool, we choose their own, NPM/YARN can be, here as YARN.

Initialize the project

yarn init
Copy the code

The electron dependency package is a bit large and downloaded from Github by default, so it is extremely difficult. Set up the mirror

yarn config set set ELECTRON_MIRROR=http://npm.taobao.org/mirrors/electron/
Copy the code

Global installation electron

yarn global add electron
Copy the code

This is the file structure in the normal node_modules/electron installation. If an error is reported later, the installation will most likely fail.

You can choose to do this manually, if you are not scientific about the Internet

Solution:

  • TXT is created in the /node_modules/electron/ directory
    • Win Enter electron. Exe
    • MAC input: Electron. The app/Contents/MacOS/Electron
  • Dist is created in /node_modules/electron/
    • Locate the corresponding version under the version package address and decompress it to the dist directory

At this point, the electron installation is a success.

In package.json, the “main” entry file is the electron startup file, which is the code related to the main process.

The following is a Vue framework for the development of the project file structure.

Introducing a modern framework

By referring to the template project can quickly start development, a word – incense!

Angular

  • Official maintenance version: github.com/angular/ang… (Cons: Longer pauses)
  • Community active version: github.com/maximegris/…

React

  • electron-react-boilerplate

The project template gathers Electron, React, Redux, React Router, Webpack, React Hot Loader, etc. It is not too sweet to start the new Electron.

Vue

  • Vue CLI Plugin Electron Builder: github.com/nklayman/vu…
  • Electron – vue: github.com/SimulatedGR… (Also has basically stopped)

By referring to the front Three Musketeers framework, we can quickly start developing the Electron GUI application. Of course, if you are committed to jQuery, you can also reference the development, but it is not recommended, this relates to the Electron performance, which is not expanded here.

Release packaging

Set the icon

  • Prepare a 1024 x 1024 PNG image and place it under public
  • Install the electric-Icon-Builder plug-in
yarn add electron-icon-builder --dev
Copy the code

Easy to install failure more than a few times (science online)

  • Package. json adds directive configuration
"build-icon": "electron-icon-builder --input=./public/logo.png --output=build --flatten"
Copy the code
  • perform
yarn build-icon
Copy the code

Generate the application icon into the corresponding Build folder

Packing installation Packages

yarn electron:build
Copy the code

Until Done out after it is Done ~

A electron application will be generated.

Core Module Demo

Setting global variables

In project development, there is often a need is theme skin, in the process of trying to think of the MAC system theme switch. This shows how to set global variables and fetch them in render.

The main process

import { nativeTheme } from 'electron' 
/** Add the global attribute ** /
global.selfConfigs = {
  nativeTheme: () = > nativeTheme.shouldUseDarkColors
}
Copy the code

Rendering process

const nativeTheme = require('electron').remote.getGlobal('selfConfigs').nativeTheme()
Copy the code

Of course, it is also possible to call the nativeTheme directly from remote.

Script injection

  • Script injection is performed through the Preload configuration item
let win = new BrowserWindow({
  webPreferences: {
    preload: jsFilePath,
    nodeIntegration: true
  }
})
Copy the code
  • Inject scripts through executeJavaScript

For example, add custom properties to a Window

The main process

let win = new BrowserWindow({
  / /...
})
win.webContents.executeJavaScript(` window.onlyConfig = {a:1,b:2} `)
Copy the code

Rendering process

console.log(window.onlyConfig) // {a:1,b:2}
Copy the code

Realize system message notification

There are two ways to do this, and the way they are used is not very different.

  • The DRAWBACK of the HTML API for sending message notifications is that it requires user authorization
  • The main process sends system messages directly
    const { Notification } = this.$electron.remote
    const notification = new Notification({
      title: 'New Notification', BVVv    body: 'You have created a new MD document, please click to view'
    })
    notification.show()
    notification.on('click'.() = > {})
Copy the code

Realize system tray and related menu

The system Tray is provided by the Tray module for adding Tray ICONS and context menus to the notification bar.

I don’t want to say anything. I want to put on a big sticker

The realization principle is relatively simple, refresh the tray icon through the timer, and add the corresponding context menu for logical operation, more functions can be DIY.

/** Add system tray ** /
  let toggleSwitch = true; let toggleFlag = false; let timer
  const icon1 = path.join(__dirname, '.. /public/icon.png')
  const icon2 = path.join(__dirname, '.. /public/icon2.png')
  tray = new Tray(icon1)
  tray.setToolTip('Electron system tray ')
  tray.on('click'.() = > {
    console.log('Tray click')
    win.isVisible() ? win.hide() : win.show()
  })
  tray.on('right-click'.() = > {
    const menuConfig = Menu.buildFromTemplate([
      {
        label: toggleSwitch ? 'Turn on the blinking icon' : 'Turn off blinking icon'.click: () = > {
          if (toggleSwitch) {
            timer = setInterval(() = > {
              if (toggleFlag) {
                tray.setImage(icon2)
              } else{ tray.setImage(icon1) } toggleFlag = ! toggleFlag },600)}else {
            tray.setImage(icon1)
            clearInterval(timer) } toggleSwitch = ! toggleSwitch } }, {label: 'exit'.click: () = > app.quit()
      }
    ])
    tray.popUpContextMenu(menuConfig)
  })
  /** Add system tray ** /
Copy the code

Realize the system right menu

In the past, we dealt with the idea of generating a right-click menu according to the user’s right mouse coordinates, relatively troublesome and also need to consider the boundary state. For example, the mdnice used in this article is used in this scheme using the custom right-click menu.

The current mouse position can be obtained through the Screen module exposed by electron

window.oncontextmenu = () = > {
  const point = require('electron').screen.getCursorScreenPoint();
}
Copy the code

In Electron, we can directly customize the system’s right click menu for greater compatibility.

// Listen for the right-click menu trigger
  win.webContents.on('context-menu'.(event, params) = > {
    constselectEnabled = !! params.selectionText.trim().lengthconst template = [
      {
        label: 'Generate qr code for current page'.click: () = > {
          console.log(The current page address is:${params.pageURL}`)}}]if(selectEnabled) { template.unshift(... [{label: 'copy'.role: 'copy'.visible: () = >! selectEnabled }, {label: 'cut'.role: 'cut'})}const RightMenu = Menu.buildFromTemplate(template)
    RightMenu.popup()
  })
Copy the code

Finally, the following basic effects are achieved:

Q&A

The NPM electron installation failed. Procedure

Solution: Avoid installation failure through CNPM Taobao image installation

Require is not defined

Reason: Electron12 cannot introduce Nodejs modules into the rendering process by default

Solution:

Go to new BrowserWindow in./background.js and add the configuration item nodeIntegration set to true

After importing the electron. Remote, undefined is prompted

Cause: After version Electron10, remote is disabled by default and needs to be enabled manually

Solution:

Go to new BrowserWindow in./background.js and add the configuration item

const win = new BrowserWindow({
    width: 800.height: 600.webPreferences: {
      enableRemoteModule: true.// Address remote as undefined
      nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION
    }
  })
Copy the code

MAC shortcut key failure problem

Found that under the MAC, should be skilled copy, cut, paste and other shortcuts failed. Tell me, as a qualified CV engineer, you can bear this?

At this time think of the expression package, see the document!

Add a section of code without stopping, instantly feel cow force coax coax

  // Determine the MAC registration shortcut key
  if (process.platform === 'darwin') {
    const contents = win.webContents
    globalShortcut.register('CommandOrControl+C'.() = > {
      contents.copy()
    })
    globalShortcut.register('CommandOrControl+V'.() = > {
      contents.paste()
    })
  }
Copy the code

Later, IT was found that this scheme was not an effective solution. After the shortcut keys were registered, I found that electron occupied the original shortcut keys of the system. Only then did I find that the shortcut keys of other applications except electron were invalid

After a careful study, by determining whether the application is active, to register/unregister the relevant shortcut keys.

// Process the system's own shortcut keys copy all selection, etc
  win.on('focus'.() = > {
    // MAC hotkeys fail
    if (process.platform === 'darwin') {
      globalShortcut.register('CommandOrControl+C'.() = > {
        console.log('Registered copy shortcut key successful')
        contents.copy()
      })
      globalShortcut.register('CommandOrControl+V'.() = > {
        console.log('Registered paste shortcut key successful')
        contents.paste()
      })
      globalShortcut.register('CommandOrControl+X'.() = > {
        console.log('Registered clipping shortcut key successful')
        contents.cut()
      })
      globalShortcut.register('CommandOrControl+A'.() = > {
        console.log('Registered full selection shortcut keys successful')
        contents.selectAll()
      })
    }
  })
  win.on('blur'.() = > {
    globalShortcut.unregister('CommandOrControl+C') // Unlog the keyboard event
    globalShortcut.unregister('CommandOrControl+V') // Unlog the keyboard event
    globalShortcut.unregister('CommandOrControl+X') // Unlog the keyboard event
    globalShortcut.unregister('CommandOrControl+A') // Unlog the keyboard event
  })
Copy the code

Chinese garbled characters appear on the Console in Windows

Common gb2312:936 UTF8:65001 Run the following command to resolve the problem

"start": "chcp 65001 && electron ."
Copy the code

Vue builds history mode project package blank

If the history mode fails to match the corresponding static resource, you need to perform a layer processing or switch the router mode to hash.

conclusion

Advantages of electron

  • Low entry barrier
  • Short development cycle

Lack of electron

  • The application volume is too large after packaging
  • Release too fast
  • Security issues
  • High resource consumption
  • Platform mounting difficulty

Front-end imagination

  • There are no browser compatibility problems
  • ES advanced syntax is supported
  • No cross-domain problems
  • Support Node. Js

reference

  • The electron’s official website
  • Electron Field (Introduction, Advancement and Performance Optimization)
  • The Vacuum + React Resume Platform from 0 to 1