Electron 进程模型全解析

390 阅读5分钟

大家好,我是徐徐,今天我们来探讨一下 Electron 的进程架构设计。

前言

随着跨平台桌面应用开发需求的增长,基于 Web 技术的桌面应用框架日益受到欢迎。Electron 作为其中的佼佼者,通过将 Chromium 和 Node.js 的优势相结合,为开发者提供了一个强大而灵活的开发平台。理解 Electron 的进程模型对于构建高性能、安全可靠的桌面应用至关重要。本文将探讨 Electron 的进程架构设计,包括主进程、渲染进程以及它们之间的通信机制,帮助开发者更好地掌握这一现代桌面应用开发框架。

Electron 进程模型的设计背景

  1. Chromium 的启示:Chromium 浏览器本身采用了多进程架构,以提高稳定性、安全性和性能。Electron 继承了这一架构,并在此基础上扩展以适应桌面应用的需求。
  2. Node.js 的结合:将 Node.js 与 Chromium 结合,可以让开发者利用 Web 技术构建用户界面的同时,使用 Node.js 提供的强大系统功能。这种结合为现代桌面应用提供了极大的灵活性和功能扩展能力。
  3. 现代应用的需求:随着 Web 技术的不断发展,越来越多的应用选择基于 Web 技术进行开发。Electron 的进程模型正是为满足这种需求而设计,使得开发者能够以 Web 技术为基础,构建高性能、跨平台的桌面应用。

作为应用开发者,你将控制两种类型的进程:主进程 和 渲染器进程。 这类似于上图所述的 Chrome 的浏览器和渲染器进程。

总体关系图

主进程 (Main Process)

Electron 只有一个主进程。Electron 应用程序的起点是一个在主进程中运行的 JavaScript 文件。主进程在 Node.js 环境中运行,这意味着它可以访问所有的 Node.js API,但无法访问 Chromium API。

特点

  1. 唯一性:每个 Electron 应用只有一个主进程。
  2. 负责系统级别的操作:包括创建和管理应用窗口、处理菜单、与操作系统进行交互(如文件系统访问、系统托盘等)。
  3. Node.js 环境:可以使用所有的 Node.js API。

功能

  • 创建和管理应用窗口。
  • 处理原生 GUI 元素(如菜单和对话框)。
  • 监听和响应来自操作系统的事件(如应用启动、退出等)。
  • 与渲染进程通信。

示例代码

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

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  });

  win.loadFile('index.html');
}

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

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

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

渲染进程 (Renderer Process)

Electron 中的渲染进程负责向用户渲染用户界面。它独立于主进程运行,这两个进程通过进程间通信(IPC)进行通信。渲染进程运行自己的 JavaScript 代码,可以访问 DOM 元素并利用 Web API。这种分离为应用程序提供了更好的安全性和稳定性。

特点

  1. 多个渲染进程:每个 Electron 应用可以有多个渲染进程,每个窗口对应一个渲染进程。
  2. Chromium 环境:基于 Chromium 的浏览器上下文,可以使用 HTML、CSS 和 JavaScript 构建用户界面。
  3. 隔离性:每个渲染进程相互独立,崩溃互不影响。

功能

  • 渲染网页内容。
  • 处理用户交互。
  • 与主进程通信。

示例代码(index.html):

<!DOCTYPE html>
<html>
<head>
  <title>Electron App</title>

</head>

<body>
  <h1>Hello, Electron!</h1>

  <button id="myButton">Click Me</button>

  <script>
    const { ipcRenderer } = require('electron');

    document.getElementById('myButton').addEventListener('click', () => {
      ipcRenderer.send('button-click');
    });
  </script>

</body>

</html>

预加载脚本 (Preload Script)

Electron 中的预加载脚本用于将主进程和渲染进程连接在一起。预加载脚本是一个在渲染进程加载主要 HTML 文件之前加载和执行的 JavaScript 文件。它允许开发人员向渲染进程添加自定义行为,并在渲染进程的上下文中执行。预加载脚本是在创建新的 BrowserWindow 时通过 webPreferences 选项指定的。

特点

  1. 上下文隔离
    • 安全性:预加载脚本在渲染进程和 Node.js 环境之间提供了一个安全的桥梁。通过上下文隔离,渲染进程无法直接访问 Node.js 的全局对象,这防止了潜在的安全漏洞。
    • 独立运行:预加载脚本在渲染进程的上下文之外独立运行,确保其不会受到渲染进程内容的影响。
  2. 早期执行
    • 初始化:预加载脚本在渲染进程加载任何网页内容之前执行,确保所有的初始化操作和 API 定义在内容加载之前完成。
  3. 有限权限
    • 精细控制:预加载脚本只能访问被显式允许的 Node.js 功能,通过 contextBridgeipcRenderer 模块来选择性地暴露功能。
  4. 简化通信
    • 统一接口:通过预加载脚本,可以定义一个统一的接口,简化渲染进程与主进程之间的通信逻辑。

功能

  • 暴露安全 API
  • 进程间通信
  • 初始化操作
  • 增强功能

示例代码

preload.js

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

contextBridge.exposeInMainWorld('api', {
  send: (channel, data) => {
    let validChannels = ['toMain'];
    if (validChannels.includes(channel)) {
      ipcRenderer.send(channel, data);
    }
  },
  receive: (channel, func) => {
    let validChannels = ['fromMain'];
    if (validChannels.includes(channel)) {
      ipcRenderer.on(channel, (event, ...args) => func(...args));
    }
  }
});

渲染进程(index.html 中的 script 标签内)

document.getElementById('myButton').addEventListener('click', () => {
  window.api.send('toMain', 'button was clicked');
});

window.api.receive('fromMain', (data) => {
  console.log(data);
});

结语

总体来说Electron 采用多进程架构,包括一个主进程和多个渲染进程。主进程负责管理应用生命周期和系统级别操作,运行在 Node.js 环境中,具有完整的系统访问权限。渲染进程负责显示用户界面,每个窗口对应一个独立的渲染进程,运行在浏览器环境中,默认启用上下文隔离以增强安全性。通过预加载脚本,渲染进程可以安全地访问部分 Node.js 功能,并通过 ipcRenderer 与主进程通信,实现复杂功能的扩展和数据的安全传递。这种架构保证了应用的高性能、高安全性和高稳定性。