Electron基础

Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在Windows上运行的跨平台应用 macOS和Linux——不需要本地开发 经验。

核心概念

入口文件

任何 Electron 应用程序的入口都是 main 文件。 这个文件控制了主进程,它运行在一个完整的 Node.js 环境中,负责控制应用的生命周期,显示原生界面,执行特殊操作并管理渲染器进程。

进程模型

Electron 继承了来自 Chromium 的多进程架构,这使得此框架在架构上非常相似于一个现代的网页浏览器。

网页浏览器是个极其复杂的应用程序。 除了显示网页内容的主要能力之外,他们还有许多次要的职责,例如:管理众多窗口 ( 或 标签页 ) 和加载第三方扩展。

在早期,浏览器通常使用单个进程来处理所有这些功能。 虽然这种模式意味着打开每个标签页的开销较少,但也同时意味着一个网站的崩溃或无响应会影响到整个浏览器。

为了解决这个问题,Chrome 团队决定让每个标签页在自己的进程中渲染, 从而限制了一个网页上的有误或恶意代码可能导致的对整个应用程序造成的伤害。 然后用单个浏览器进程控制这些标签页进程,以及整个应用程序的生命周期。

Electron 应用程序的结构非常相似。 应用开发者控制两种类型的进程:主进程 和 渲染器进程。

主进程

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

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

BrowserWindow 类的每个实例创建一个应用程序窗口,且在单独的渲染器进程中加载一个网页。 可从主进程用 window 的 webContent 对象与网页内容进行交互。
当一个 BrowserWindow 实例被销毁时,与其相应的渲染器进程也会被终止。

主进程还能通过 Electron 的 app 模块来控制应用程序的生命周期。
此模块提供了大量事件和方法,可用于添加自定义应用程序行为(例如,以编程方式退出应用程序、修改应用程序停靠栏或显示“关于”面板

为了使 Electron 的功能不仅仅限于对网页内容的封装,主进程也添加了自定义的 API 来与用户的作业系统进行交互。 Electron 有着多种控制原生桌面功能的模块,例如菜单、对话框以及托盘图标。

渲染器进程

Electron 应用会为打开的 BrowserWindow 生成一个单独的渲染器进程。 渲染器进程负责 渲染 网页内容。 所以实际上,运行于渲染器进程中的代码是须遵照网页标准的。
因此,一个浏览器窗口中的所有的用户界面和应用功能,都应与在网页开发上使用相同的工具和规范来进行攥写。

渲染器无权直接访问 require 或其他 Node.js API。 为了在渲染器中直接包含 NPM 模块,必须使用与在 web 开发时相同的打包工具 (例如 webpack 或 parcel)

预加载脚本

预加载脚本在渲染器进程加载之前加载,并有权访问 window 和 document 渲染器全局变量和 Node.js 环境。

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

预加载脚本可以在 BrowserWindow 构造方法中的 webPreferences 选项里被附加到主进程。

因为预加载脚本与浏览器共享同一个全局 Window 接口,并且可以访问 Node.js API,所以它通过在全局 window 中暴露任意 API 来增强渲染器,以便你的网页内容使用。

虽然预加载脚本与其所附着的渲染器在共享着一个全局 window 对象,但并不能从中直接附加任何变动到 window 之上,因为 上下文隔离 是默认的。
上下文隔离 意味着预加载脚本与渲染器的主要运行环境是隔离开来的,以避免泄漏任何具特权的 API 到网页内容代码中。

在预加载脚本中可以使用 contextBridge 模块来安全地实现预加载脚本与渲染器的交互。

UtilityProcess

每个 Electron 应用程序都可以使用 UtilityProcess API 从主进程生成多个子进程。该 UtilityProcess 进程在 Node.js 环境中运行,这意味着它能够 require 模块并使用所有 Node.js API。该 UtilityProcess 进程可用于托管例如:不受信任的服务、CPU 密集型任务或易崩溃组件,这些组件以前会托管在 Node.js child_process.fork API 派生的主进程或进程中。UtilityProcess 进程和 Node.js child_process 模块生成的进程之间的主要区别在于,UtilityProcess 进程可以使用 MessagePorts 与渲染器进程建立通信通道。当需要从主进程 fork 子进程时,Electron 应用程序总是更喜欢 UtilityProcess API 而不是 Node.js child_process.fork API。

上下文隔离

上下文隔离功能将确保预加载脚本 和 Electron 的内部逻辑 运行在所加载的 webcontent 网页 之外的另一个独立的上下文环境里。 这对安全性很重要,因为它有助于阻止网站访问 Electron 的内部组件 和 预加载脚本可访问的高等级权限的 API 。
这意味着,实际上,预加载脚本访问的 window 对象并不是网站所能访问的对象。
Electron 提供的 contextBridge 模块可以用来安全地从独立运行、上下文隔离的预加载脚本中暴露 API 给正在运行的渲染进程。

进程间通信

进程间通信 (IPC) 是在 Electron 中构建功能丰富的桌面应用程序的关键部分之一。 由于主进程和渲染器进程在 Electron 的进程模型具有不同的职责,因此 IPC 是执行许多常见任务的唯一方法,例如从 UI 调用原生 API 或从原生菜单触发 Web 内容的更改。

在 Electron 中,进程使用 ipcMain 和 ipcRenderer 模块,通过开发人员定义的“通道”传递消息来进行通信。 这些通道是自定义和双向的。

渲染器进程到主进程-单向 IPC
要将单向 IPC 消息从渲染器进程发送到主进程,可以使用 ipcRenderer.send API 发送消息,然后使用 ipcMain.on API 接收。通常使用此模式从 Web 内容调用主进程 API。

渲染器进程到主进程-双向 IPC
双向 IPC 的一个常见应用是从渲染器进程代码调用主进程模块并等待结果。 这可以通过将 ipcRenderer.invoke 与 ipcMain.handle 搭配使用来完成。

主进程到渲染器进程-单向 IPC
将消息从主进程发送到渲染器进程时,需要指定是哪一个渲染器接收消息。 消息需要通过其 WebContents 实例发送到渲染器进程。 此 WebContents 实例包含一个 send 方法,其使用方式与 ipcRenderer.send 相同。

渲染器进程到渲染器进程
没有直接的方法可以使用 ipcMain 和 ipcRenderer 模块在 Electron 中的渲染器进程之间发送消息。 有两种替代选项:
将主进程作为渲染器之间的消息代理。 这需要将消息从一个渲染器发送到主进程,然后主进程将消息转发到另一个渲染器。
从主进程将一个 MessagePort 传递到两个渲染器。 这将允许在初始设置后渲染器之间直接进行通信。

进程沙盒化

Chromium 的一个关键安全特性是,进程可以在沙箱中执行。沙盒通过限制对大多数系统资源的访问来限制恶意代码可能造成的危害——沙盒进程只能自由使用 CPU 周期和内存。为了执行需要额外权限的操作,沙盒进程使用专用通信通道将任务委托给更具权限的进程。
在 Chromium 中,沙盒化应用于主进程以外的大多数进程。 其中包括渲染器进程,以及功能性进程,如音频服务、GPU 服务和网络服务。

从 Electron 20 开始,渲染进程默认启用了沙盒,无需进一步配置。

当 Electron 中的渲染进程被沙盒化时,它们的行为与常规 Chrome 渲染器一样。 一个沙盒化的渲染器不会有一个 Node.js 环境。

因此,在沙盒中,渲染进程只能透过 IPC 委派任务给主进程的方式, 来执行需权限的任务 (例如:文件系统交互,对系统进行更改或生成子进程) 。

MessagePort

MessagePort 是一个允许在不同上下文之间传递消息的 Web 功能。 就像 window.postMessage, 但是在不同的通道上。
以上为来自官方文档的介绍和核心概念
 
 
查看 npm 指定的源地址

npm config get registry
npm config set registry https://registry.npm.taobao.org/
npm config set registry https://registry.npmjs.org/

# 清除缓存

npm cache clean # 卸载包

npm uninstall package
npm uninstall package --save-dev
npm uninstall package -D

# 创建项目

npm init

# 安装 electron

npm install electron --save-dev

# 在开发模式下打开应用

npm start

# 打包并分发应用程序

npm install --save-dev @electron-forge/cli
npx electron-forge import

npm run make
 index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'" />
    <title>Hello World!</title>
  </head>
  <body>
    <h1>Hello World!</h1>
    We are using Node.js <span id="node-version"></span>, Chromium <span id="chrome-version"></span>, and Electron <span id="electron-version"></span>.
    <br />

    <input id="title" type="text" />
    <button id="setTitle" type="button">Set Title</button>

    <p>
      FilePath: <span id="filePath"></span><br />
      Ping:<span id="ping">ping</span>
    </p>
    <button type="button" id="openFile">Open a File</button>

    <p>Counter: <span id="counter">0</span></p>

    <script src="./renderer.js"></script>
  </body>
</html>

index.js

const { app, BrowserWindow, ipcMain, Menu, dialog } = require("electron/main");
// 导入 Node.js 的 path 模块
const path = require("node:path");

async function handleFileOpen() {
  const { canceled, filePaths } = await dialog.showOpenDialog();
  if (!canceled) {
    return filePaths[0];
  }
}

// app 模块,它控制应用程序的事件生命周期。
// BrowserWindow 模块,它创建和管理应用程序 窗口。

//创建一个窗口,并加载页面
const createWindow = () => {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    // 将预加载脚本附加到渲染器流程,
    webPreferences: {
      preload: path.join(__dirname, "preload.js"), //__dirname 字符串指向当前正在执行脚本的路径
    },
  });

  // 菜单
  const menu = Menu.buildFromTemplate([
    {
      label: "Counter",
      submenu: [
        {
          click: () => win.webContents.send("update-counter", 1),
          label: "Increment",
        },
        {
          click: () => win.webContents.send("update-counter", -1),
          label: "Decrement",
        },
      ],
    },
  ]);
  Menu.setApplicationMenu(menu);

  // 加载页面
  win.loadFile("index.html");

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

  //监听用户事件
  win.on("maximize", function (event) {
    console.log("maximize");
  });

  // 打开开发工具
  win.webContents.openDevTools();

  // 主进程监听渲染器进程到主进程单向IPC通道
  ipcMain.on("set-title", (event, title) => {
    const webContents = event.sender;
    const win = BrowserWindow.fromWebContents(webContents);
    win.setTitle(title);
  });

  // 主进程监听渲染器进程到主进程双向IPC通道,并返回结果
  ipcMain.handle("dialog:openFile", handleFileOpen);
  ipcMain.handle("ping", (event) => "pong");

  // 主进程中接收端口对象
  ipcMain.on("port", (event) => {
    // 当我们在主进程中接收到 MessagePort 对象, 它就成为了
    // MessagePortMain.
    const port = event.ports[0];
    console.log(port);

    // MessagePortMain 使用了 Node.js 风格的事件 API, 而不是
    // web 风格的事件 API. 因此使用 .on('message', ...) 而不是 .onmessage = ...
    port.on("message", (event) => {
      // 收到的数据是: { answer: 42 }
      const data = event.data;
    });

    // MessagePortMain 阻塞消息直到 .start() 方法被调用
    port.start();
  });
};

// 强制沙盒化所有渲染器;
app.enableSandbox();

// 在 Electron 中,只有在 app 模块的 ready 事件被触发后才能创建浏览器窗口。
app.whenReady().then(() => {
  createWindow();

  // macOS 应用通常即使在没有打开任何窗口的情况下也继续运行,并且在没有窗口可用的情况下激活应用时会打开新的窗口。
  app.on("activate", () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

// 在Windows和Linux上,关闭所有窗口通常会完全退出一个应用程序。
app.on("window-all-closed", () => {
  if (process.platform !== "darwin") app.quit();
});

 preload.js

const { contextBridge, ipcRenderer } = require("electron/renderer");

contextBridge.exposeInMainWorld("eAPI", {
  setTitle: (title) => {
    // 渲染器进程到主进程的单向IPC
    ipcRenderer.send("set-title", title);
  },
  ping: () => {
    // 渲染器进程到主进程的双向IPC
    return ipcRenderer.invoke("ping");
  },
  openFile: () => {
    // 渲染器进程到主进程的双向IPC
    return ipcRenderer.invoke("dialog:openFile");
  },
  onUpdateCounter: (callback) => {
    // 主进程到渲染器进程的单向IPC,渲染器进程监听IPC通道
    ipcRenderer.on("update-counter", (_event, value) => callback(value));
  },
});

// 预加载脚本在渲染器进程加载之前加载,并有权访问window 和 document 渲染器全局和 Node.js 环境。
// 预加载脚本访问 Node的全局 process 对象,并将相应信息放到dom对象中
window.addEventListener("DOMContentLoaded", () => {
  const replaceText = (selector, text) => {
    const element = document.getElementById(selector);
    if (element) element.innerText = text;
  };

  for (const dependency of ["chrome", "node", "electron"]) {
    replaceText(`${dependency}-version`, process.versions[dependency]);
  }
});

renderer.js

const setTitleButton = document.getElementById("setTitle");
const titleInput = document.getElementById("title");
setTitleButton.addEventListener("click", () => {
  const title = titleInput.value;
  window.eAPI.setTitle(title);
});

const openFileButton = document.getElementById("openFile");
openFileButton.addEventListener("click", async () => {
  //返回promise
  const text = await window.eAPI.openFile();
  document.getElementById("filePath").innerHTML = text;
});

async function ping() {
  document.getElementById("ping").innerHTML = await window.eAPI.ping();
}
ping();

const counter = document.getElementById("counter");
window.eAPI.onUpdateCounter((value) => {
  const oldValue = Number(counter.innerText);
  const newValue = oldValue + value;
  counter.innerText = newValue.toString();
});

默认菜单

自定义菜单

 

posted @   carol2014  阅读(89)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
历史上的今天:
2022-12-28 vue3封装axios并使用拦截器处理错误
2022-12-28 vue3使用sweetalert2替代默认的alert/confirm框
2022-12-28 vue3使用bootstrap的简单加载遮罩层
2022-12-28 vue3使用vue-router构建SPA
2022-12-28 laravel使用fortify和Sanctum为SPA提供注册登录和api身份验证
点击右上角即可分享
微信分享提示