Skip to content

多进程模型

多进程模型

向军大叔每晚八点在 抖音bilibli 直播

xj-small

Electron 将使用两种类型的进程:主进程渲染器进程

Chrome的多进程架构

不同进程承载着不同的任务,本章讨论的进程通信(IPC)就是解决不同进程间任务传递的方式。

  • 比如渲染进程通过主进程调用原生Node.js API,比如文件操作
  • 主进程通过原生菜单改变渲染进程页面内容
  • IPC通信使用 ipcMainipcRenderer 两个模块传递消息

主进程

每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点。 主进程在 Node.js 环境中运行,这意味着它具有使用 Node.js API 的能力。

主进程的主要目的是使用 BrowserWindow 模块创建和管理应用程序窗口。

渲染器进程

每个 Electron 应用都会为使用 BrowserWindow 打开的窗口生成一个单独的渲染器进程。

默认情况下渲染进程与主进程使用 preload.js预加载做为通信桥梁。

预加载脚本

预加载(preload)脚本包含了那些执行于渲染进程中,且先于网页内容开始加载的代码 。这些脚本虽运行于渲染器的环境中,却因能访问有限的 Node.js、Electron高级权限。

因为Electron项目与其他桌面应用是有区别的,他具有浏览器的特性,所以开放主进程的node.js给渲染进程,是有安全隐患的。默认electron是不会开放高级权限给渲染进程,而是要求开发者自行决定渲染进程可以使用哪些主进程任务,这块功能就要在预加载脚本中完成。

预加载脚本像一个桥接器,用于渲染脚本renderer.js与main.js脚本的连接。

基础知识

预加载脚本运行在具有 HTML DOM APIs 和 Node.js、Electron 的有限功能访问权限的环境中。

Preload.js 是渲染进程与主进程通信的桥梁。

使用场景

  • 使用有限的node.js、Electron高级api
  • 主进程与渲染进程进行IPC通信,比如渲染进程让主进程帮助在本地保存文件

使用示例

下面使用预加载脚本preload.js,通过node 的process查看软件版本的信息

main.js 主进程脚本

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

const createWindow = () => {
  const win = new BrowserWindow({
    width: 300,
    height: 300,
    webPreferences: {
    	//预加载脚本
      preload: path.resolve(__dirname, 'preload.js'),
    },
  })
  win.loadFile(path.resolve(__dirname, 'index.html'))
  win.webContents.openDevTools()
}

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

preload.js 预加载脚本

document.addEventListener('DOMContentLoaded', () => {
  for (const soft of ['chrome', 'electron', 'node']) {
    console.log(soft)
    document.querySelector(`#${soft}`).innerHTML = `${soft}:` + process.versions[soft]
  }
})

index.html 模板文件

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'" />
    <meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'" />
    <title>houdunren</title>
  </head>
  <body>
    <div id="chrome"></div>
    <div id="node"></div>
    <div id="electron"></div>
    <script src="renderer.js"></script>
  </body>
</html>

最终会打印出node、electron、chrome版本信息

image-20230128172019607

进程通信

下面介绍主进程与渲染进程是如何进行通信的。

渲染进程到主进程

下面介绍渲染进程向主进程通信,这是单向通信行为。本例实现的功能是渲染进程向主进程发送请求,更改窗口标题。

main.js

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

const createWindow = () => {
  const win = new BrowserWindow({
    width: 300,
    height: 300,
    alwaysOnTop: true,
    x: 1500,
    y: 100,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
    },
  })
  win.webContents.openDevTools()

  win.loadFile(path.resolve(__dirname, 'index.html'))
}

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

  //主进程事件监听
  ipcMain.on('setTitle', (event, title) => {
    //获取用于控制网页的webContents对象
    const webContents = event.sender
    //获取窗口
    const win = BrowserWindow.fromWebContents(webContents)
    //设置窗口标题
    win.setTitle(title)
  })
})

preload.js

const { contextBridge, ipcRenderer } = require('electron')
//为渲染进程暴露API
contextBridge.exposeInMainWorld('api', {
  //该API用于向主进程事件
  setTitle: (title) => ipcRenderer.send('setTitle', title),
})

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'" />
    <title>houdunren</title>
  </head>
  <body>
    <input type="text" name="title" />
    <button>更改标题</button>
    <script src="renderer.js"></script>
  </body>
</html>

renderer.js

window.addEventListener('DOMContentLoaded', () => {
  document.querySelector('button').addEventListener('click', () => {
    const value = document.querySelector('[name=title]').value

    //使用preload.js暴露出的API,触发主进程事件
    window.api.setTitle(value)
  })
})

主进程到渲染进程

下面介绍主进程主动向渲染进程通信,这也是单向通信IPC。

将消息从主进程发送到渲染器进程时,需要指定是哪一个渲染器接收消息。 消息需要通过 WebContents 实例的send方法发送到渲染器进程。

main.js

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

const createWindow = () => {
  const win = new BrowserWindow({
    width: 300,
    height: 300,
    alwaysOnTop: true,
    x: 1500,
    y: 100,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
    },
  })
  //定义菜单
  const menu = Menu.buildFromTemplate([
    {
      label: '菜单',
      submenu: [
        {
        	//主进程向渲染进程发送消息
          click: () => win.webContents.send('increment', 1),
          label: '增加',
        },
      ],
    },
  ])
  Menu.setApplicationMenu(menu)

  //打开开发者工具
  win.webContents.openDevTools()
  win.loadFile(path.resolve(__dirname, 'index.html'))
}

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

//接收渲染进程的结果
ipcMain.on('finish', (event, value) => {
  console.log('最后结果是:' + value)
})

perload.js

const { contextBridge, ipcRenderer } = require('electron')
//为渲染进程暴露API
contextBridge.exposeInMainWorld('api', {
  //为渲染进程设置接口,用于接收主进程的消息
  incrementNumber: (callback) => ipcRenderer.on('increment', callback),
})

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'" />
    <title>houdunren</title>
  </head>
  <body>
    <h1></h1>
    <script src="renderer.js"></script>
  </body>
</html>

renderer.js

//向预加载脚本传递回调方法,用于处理主进程的消息
window.api.incrementNumber((event, value) => {
  const h1 = document.querySelector('h1')
  h1.innerHTML = Number(h1.innerText) + value
  //向主进程发送消息
  event.sender.send('finish', h1.innerHTML)
})

双向通信

使用 ipc 的invoke 进行渲染进程与主进程的通信,主进程会返回 promise。

index.html

...
<button id="btn">IPC</button>
<script src="renderer.js"></script>
...

main.js

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

const createWindow = () => {
    const win = new BrowserWindow({
        width: 300,
        height: 300,
        alwaysOnTop: true,
        webPreferences: {
            preload: path.resolve(__dirname, 'preload.js'),
        },
    })
    win.webContents.openDevTools()
    win.loadFile(path.resolve(__dirname, 'index.html'))
    return win
}

app.whenReady().then(() => {
    const win = createWindow()
    ipcMain.handle('mainShow', (event) => {
        return 'is main handle'
    })
})

preload.js

const { ipcRenderer } = require('electron')
const { contextBridge } = require('electron/renderer')

contextBridge.exposeInMainWorld('api', {
    show: () => {
        return ipcRenderer.invoke('mainShow')
    },
})

renderer.js

const bt = document.querySelector('#btn')
bt.addEventListener('click', async () => {
    const res = await api.show()
    console.log(res)
})

其他进程的通信的使用是类似的,可以参考 electron 文档学习