Wake up the wrist front-end Electron Gui desktop application development detailed tutorial (process model, process communication, process sandboxing)

process model

Electron inherits the multi-process architecture from Chromium, which makes the framework very similar in architecture to a modern web browser.

Why not a single process?

A web browser is an extremely complex application. Besides their primary ability to display web content, they have many secondary responsibilities such as: managing numerous windows (or tabs) and loading third-party extensions.

In the early days, browsers typically used a single process to handle all of these functions. While this mode means you have less overhead per open tab, it also means that one website that crashes or becomes unresponsive affects the entire browser.

multi-process model

To solve this problem, the Chrome team decided to make each tab render in its own process, thereby limiting the damage that bad or malicious code on one web page could cause to the entire application. A single browser process then controls these tab processes, as well as the entire application lifecycle. The diagram below from the Chrome comics visualizes this model:
insert image description here

Electron apps are structured very similarly. As an application developer, you will control two types of processes: the main process and the renderer process. This is similar to Chrome's browser and renderer processes described above.

Main ProcessMain Process

Every Electron application has a single main process, which serves as the application's entry point. The main process runs in a Node.js environment, which means it has the ability to require modules and use all Node.js APIs.

window management

The main purpose of the main process is to create and manage application windows using the BrowserWindow module.

Each instance of the BrowserWindow class creates an application window and loads a web page in a separate renderer process. You can interact with web content from the main process using the window's webContent object.

const {
    
     BrowserWindow } = require('electron')

const win = new BrowserWindow({
    
     width: 800, height: 1500 })
win.loadURL('https://github.com')

const contents = win.webContents
console.log(contents)

Note: Renderer processes are also created for web embeds, such as the BrowserView module. Embedded web content also has access to
the webContents object.

Since the BrowserWindow module is an EventEmitter, you can also add handlers for various user events (such as minimizing or maximizing your window).

When a BrowserWindow instance is destroyed, its corresponding renderer process is also terminated.

application life cycle

The main process can also control your application's lifecycle through Electron's app module. This module provides a set of events and methods that you can use to add custom application behavior (for example: programmatically quit your application, modify the application dock, or display an about panel).

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

Native API

In order to make Electron's functions not limited to encapsulating web content, the main process also adds a custom API to interact with the user's operating system. Electron has various modules that control native desktop functionality, such as menus, dialogs, and tray icons.

renderer process

Every Electron app spawns a separate renderer process for every opened BrowserWindow (embedded with every web page). As the name suggests, the renderer is responsible for rendering web content. So in fact, the code running in the renderer process must comply with web standards (at least as far as Chromium is currently used).

Therefore, all user interface and application functions in a browser window should be written using the same tools and specifications as you use in web development.

While explaining each page specification is beyond the scope of this guide, the minimum you need to know is:

以一个 HTML 文件作为渲染器进程的入口点。
使用层叠样式表 (Cascading Style Sheets, CSS) 对 UI 添加样式。
通过 <script> 元素可添加可执行的 JavaScript 代码。

Additionally, this also means that the renderer does not have direct access to require or other Node.js APIs. In order to include NPM modules directly in the renderer, you must use the same packaging tools as you use for web development (eg webpack or parcel)

To facilitate development, a complete Node.js environment can be used to generate a renderer process. Historically, this was the default, but for security reasons this has been disabled.

Why is there a security problem here? The official document does not specify it. Let me explain:

For example, our main process creates a window window, loadUrl("http://a phishing website.com"), and loads the rendering page, but this phishing website uses scripts that call computer system resources, then your computer system Doesn't privacy mean traffic?

At this point, you might be wondering how the renderer process UI can interact with Node.js and Electron's native desktop functionality, since these features are only accessible from the main process. In fact, there really isn't a way to import Electron content scripts directly.

reload script

Preload scripts include code that executes in the renderer process and starts loading before the web page content. Although these scripts run in the environment of the renderer, they have more permissions because they can access the Node.js API.

Preload scripts can be attached to the main process in the webPreferences option of the BrowserWindow constructor.

const {
    
     BrowserWindow } = require('electron')

const win = new BrowserWindow({
    
    
  webPreferences: {
    
    
    preload: 'path/to/preload.js',
  },
})

Because the preload script shares the same global Window interface as the browser and has access to the Node.js API, it enhances the renderer by exposing arbitrary APIs in the global window for your web content to use.

Although the preload script and the renderer it is attached to share a global window object, you cannot attach any changes directly to the window from it, because contextIsolation is the default.

preload.js

window.myAPI = {
    
    
  desktop: true,
}

renderer.js

console.log(window.myAPI)

## undefined

Context Isolation means that preload scripts are isolated from the renderer's main execution environment to avoid leaking any privileged APIs into your web content code.

Instead, we'll use the contextBridge module to safely implement interactions:

preload.js

const {
    
     contextBridge } = require('electron')

contextBridge.exposeInMainWorld('myAPI', {
    
    
  desktop: true,
})

renderer.js

console.log(window.myAPI)
// => { desktop: true }

This feature is useful for two main purposes:

By exposing the ipcRenderer helper module in the renderer, you can use inter-process communication (IPC) to trigger the main process task from the renderer (and vice versa).

If you're developing an Electron wrapper for an existing web app hosted at a remote URL, you can add a custom property on the renderer's window global variable so that the web client can use design logic that only applies to desktop apps.

efficiency process

Every Electron application can spawn multiple child processes from the main process using the UtilityProcess API. The main process runs in a Node.js environment, which means it has the ability to require modules and use all Node.js APIs. Utility processes can be used for hosting, for example: untrusted services, CPU-intensive tasks or previously crash-prone components hosted in the main process or processes spawned using the Node.jschild_process.fork API. The main difference between efficiency processes and processes spawned by Node.js child_process module is that the utility process can establish a communication channel with the renderer process using MessagePort. Electron applications can always use the efficiency process API in preference to the Node.js child_process.fork API when a child process needs to be forked from the main process.

context isolation

What is context isolation?

Context isolation will ensure that your preloaded scripts and Electron's internal logic run in a separate context from the loaded webcontent page. This is important for security because it helps prevent websites from accessing Electron's internal components and high-level APIs that your preload scripts can access.

This means that, in reality, the window object that your preload script accesses is not an object that the website can access. For example, if you set window.hello = 'wave' in the preload script and context isolation is enabled, undefined will be returned when the website tries to access the window.hello object.

Context isolation has been enabled by default since Electron 12, and it's a recommended security setting for all applications.

migrate

Without context isolation, window.X = apiObject would often be used when serving an API from a preloaded script So what now?

Before: Context isolation disabled

It is a common usage for preload scripts to expose the loaded page API during the render process. While context isolating, your preload script may expose a common global window object to the renderer process. Thereafter, you can add arbitrary attributes from it to the preload script.

preload.js

window.myAPI = {
    
    
  doAThing: () => {
    
    }
}

The doAThing() function can be used directly in the rendering process.

renderer.js

window.myAPI.doAThing()

After: Enable context isolation

Electron provides a specialized module to help you do this without hindrance. The contextBridge module can be used to safely expose an API to a running renderer process from a stand-alone, context-isolated preload script. The API can also be accessed from the window.myAPI website as before.

preload.js

const {
    
     contextBridge } = require('electron')

contextBridge.exposeInMainWorld('myAPI', {
    
    
  doAThing: () => {
    
    }
})

renderer.js

window.myAPI.doAThing()

Safety Precautions

Just turning on and using contextIsolation doesn't immediately mean that everything you do is safe. For example, this code is unsafe.

wrong use of preload.js

contextBridge.exposeInMainWorld('myAPI', {
    
    
  send: ipcRenderer.send
})

It directly exposes a high-level permission API without any parameter filtering. This will allow any website to send arbitrary IPC messages, which is not what you want to happen. Instead, the correct way to expose an interprocess communication-related API is to provide an implementation for each communication message.

preload.js is used correctly

contextBridge.exposeInMainWorld('myAPI', {
    
    
  loadPreferences: () => ipcRenderer.invoke('load-prefs')
})

interprocess communication

Inter-process communication (IPC) is one of the key parts of building feature-rich desktop applications in Electron. Because the main process and the renderer process have different responsibilities in Electron's process model, IPC is the only way to perform many common tasks, such as calling native APIs from the UI or triggering web content changes from native menus.

IPC channel

In Electron, processes communicate by passing messages through developer-defined "channels" using the ipcMain and ipcRenderer modules. These channels are arbitrary (you can name them whatever you want) and bidirectional (you can use the same channel name in both modules).

In this guide, we'll introduce some basic IPC patterns and provide concrete examples. You can use these examples as a reference for your application code.

Mode 1: Renderer process to main process (one-way)

To send a one-way IPC message from the renderer process to the main process, you can use the ipcRenderer.send API to send the message, and the ipcMain.on API to receive it.

This pattern is typically used to call the main process API from web content. We'll demonstrate this pattern by creating a simple application whose window title can be changed programmatically.

For this demonstration, you need to add code to the main process, renderer process, and preload script. The complete code is as follows, and we will explain each file individually in subsequent chapters.

main.js main process code

const {
    
     app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')

function createWindow() {
    
    
    const mainWindow = new BrowserWindow({
    
    
        width: 400,
        height: 300,
        webPreferences: {
    
    
            preload: path.join(__dirname, 'preload.js')
        }
    })

    ipcMain.on('set-title', (event, title) => {
    
    
        const webContents = event.sender
        const win = BrowserWindow.fromWebContents(webContents)
        win.setTitle(title)
    })

    mainWindow.loadFile('index.html')
}

app.whenReady().then(() => {
    
    
    createWindow()

    app.on('activate', function () {
    
    
        if (BrowserWindow.getAllWindows().length === 0) createWindow()
    })
})

app.on('window-all-closed', function () {
    
    
    if (process.platform !== 'darwin') app.quit()
})

Static page index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
    <title>Hello World!</title>
  </head>
  <body>
    Title: <input id="title"/>
    <button id="btn" type="button">Set</button>
    <script src="./renderer.js"></script>
  </body>
</html>

The preload script preload.js

const {
    
     contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
    
    
    setTitle: (title) => ipcRenderer.send('set-title', title)
})

Rendering process renderer.js

const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
    
    
    const title = titleInput.value
    window.electronAPI.setTitle(title)
});

Guess you like

Origin blog.csdn.net/qq_47452807/article/details/129310890