安装以及运行
当前node版本18+,按照官网提供操作,npm init
进行初始化操作,将index.js
修改为main.js
,执行npm install --save-dev electron
。(这里我挂梯子下载成功了。),添加如下代码至package.json
"scripts": {"start": "electron ."},
新建一个index.html
文件,内容随意。新建一个main.js
文件,内容如下。主要意思是将一个界面加载到一个应用窗口中。createWindow()
方法来将index.html加载进一个新的BrowserWindow实例。在 app 模块的 ready 事件被激发后才能创建浏览器窗口。 可以通过使用 app.whenReady()
API来监听此事件。 在whenReady()
成功后调用createWindow()函数
来打开窗口。
- app 模块,它控制应用程序的事件生命周期。
- BrowserWindow 模块,它创建和管理应用程序 窗口。
const { app, BrowserWindow } = require("electron");const createWindow = () => {const win = new BrowserWindow({width: 800,height: 600,});win.loadFile("index.html");
};app.whenReady().then(() => {createWindow();
});
添加关闭应用窗口事件,若输出有乱码则参考该博客输出乱码解决
app.on("window-all-closed", () => {if (process.platform !== "darwin") {console.log("关闭");app.quit();}
});
添加代码至BrowserWindow
中,可以打开控制台操作,应用开启后可以使用ctrl+shift+i
打开控制台
webContents: {openDevTools: true,},
vue3中运行
基于@quick-start/electron
框架快速创建一个基于vue3+electron的项目结构。
npm create @quick-start/electron
最近又新看见一个框架electron-gg
地址,感觉挺好的也是基于vue3
预加载脚本
Electron 的主进程是一个拥有着完全操作系统访问权限的 Node.js 环境。 除了 Electron 模组 之外,您也可以访问 Node.js 内置模块 和所有通过 npm 安装的包。 另一方面,出于安全原因,渲染进程默认跑在网页页面上,而并非 Node.js里。
主进程负责管理整个应用程序的生命周期,包含创建窗口,运行在nodejs环境中,每次打开一个窗口,实际是创建一个渲染进程。主进程和渲染进程之间通过IPC通信。
为了将 Electron 的不同类型的进程桥接在一起,我们需要使用被称为 预加载 的特殊脚本。
BrowserWindow 的预加载脚本运行在具有 HTML DOM 和 Node.js、Electron API 的有限子集访问权限的环境中。预加载脚本默认 沙盒化 ,不再拥有完整 Node.js 环境的访问权。 实际上,这意味着你只拥有一个 polyfilled 的 require 函数,这个函数只能访问一组有限的 API。
预加载脚本在渲染器加载网页之前注入。 如果你想为渲染器添加需要特殊权限的功能,可以通过 contextBridge 接口
定义 全局对象。
创建preload.js文件,该脚本通过 versions 这一全局变量,将 Electron 的 process.versions 对象暴露给渲染器。
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('versions', {node: () => process.versions.node,chrome: () => process.versions.chrome,electron: () => process.versions.electron// 除函数之外,我们也可以暴露变量
})
将脚本附在渲染进程上,在 BrowserWindow 构造器中使用 webPreferences.preload 传入脚本的路径。webPreferences
对象是用来定制渲染进程中的特性,其中preload
属性是在网页运行其他脚本之前执行的脚本,用来提前注入Nodejs api或者其他需要再渲染进程中运行的脚本。
const { app, BrowserWindow } = require('electron')
const path = require('node:path')const createWindow = () => {const win = new BrowserWindow({width: 800,height: 600,webPreferences: {preload: path.join(__dirname, 'preload.js')}})win.loadFile('index.html')
}app.whenReady().then(() => {createWindow()
})
现在渲染器能够全局访问 versions 了,但是还需要将版本等数据渲染到界面中,可以通过window.versions
访问,也可以很简单地使用 versions
来访问。 新建一个 renderer.js 脚本, 使用 document.getElementById DOM API 来替换 id 属性为 info 的 HTML 元素的文本。
const information = document.getElementById('info')
information.innerText = `本应用正在使用 Chrome (v${versions.chrome()}), Node.js (v${versions.node()}), 和 Electron (v${versions.electron()})`
然后再index.html文件中引入
<script src="./renderer.js"></script>
进程之间通信(简略)
Electron 的主进程和渲染进程有着清楚的分工并且不可互换,无论是从渲染进程直接访问 Node.js 接口,亦或者是从主进程访问 HTML 文档对象模型 (DOM),都是不可能的。
解决这一问题的方法是使用进程间通信 (IPC)。可以使用 Electron 的 ipcMain 模块
和 ipcRenderer 模块
来进行进程间通信。 为了从你的网页向主进程发送消息,你可以使用ipcMain.handle
设置一个主进程处理程序(handler),然后在预处理脚本中暴露一个被称为 ipcRenderer.invoke
的函数来触发该处理程序(handler)。
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("versions", {node: () => process.versions.node,chrome: () => process.versions.chrome,electron: () => process.versions.electron,ping: () => ipcRenderer.invoke("ping"),
});
主进程中设置你的 handle 监听器,在 HTML 文件加载之前完成了这些,所以才能保证在你从渲染器发送 invoke 调用之前处理程序能够准备就绪。
渲染主进程中引入const { app, BrowserWindow, ipcMain } = require("electron");
app.whenReady().then(() => {ipcMain.handle("ping", () => "pong");createWindow();
});
将发送器与接收器设置完成之后,现在你可以将信息通过刚刚定义的 ‘ping’ 通道从渲染器发送至主进程当中。修改renderer
中的文件
const func = async () => {const response = await window.versions.ping()console.log(response) // 打印 'pong'
}func()
打包程序
Electron 的核心模块中没有捆绑任何用于打包或分发文件的工具。 如果您在开发模式下完成了一个 Electron 应用,需要使用额外的工具来打包应用程序 (也称为可分发文件) 并分发给用户 。 可分发文件可以是安装程序 (例如 Windows 上的 MSI) 或者绿色软件 (例如 macOS 上的 .app 文件)。Electron Forge
是一个处理 Electron 应用程序打包与分发的一体化工具。 在工具底层,它将许多现有的 Electron 工具 (例如 @electron/packager、 @electron/osx-sign、electron-winstaller 等) 组合到一起。
安装打包工具npm install --save-dev @electron-forge/cli
,然后输入npx electron-forge import
等待初始化完毕,之后package会变为如下图所示
创建一个可分发版本
要创建可分发文件,请使用项目中的 make 脚本,该脚本最终运行了 electron-forge make 命令。npm run make
,注意如果npm init 的时候,author 与 description 可为任意值,但对于应用打包是必填项。如果没有则会出现如下错误
注意,项目打包路径中不能包含中文字符,否则报错。 打包成功如下
npm run make
它将首先运行 electron-forge package ,把您的应用程序 代码与 Electron 二进制包结合起来。- 完成打包的代码将会被生成到一个特定的文件夹中。 然后它将使用这个文件夹为每个 maker 配置生成一个可分发文件。
打包完成后查看.exe文件运行。
代码签名
为了将桌面应用程序分发给最终用户,我们 强烈建议 您对 Electron 应用进行 代码签名。 代码签名是交付桌面应用程序的重要组成部分,并且它对于应用程序的自动更新功能 (将会在教程最后部分讲解) 来说是必需的。
代码签名是一种可用于证明桌面应用程序是由已知来源创建的安全技术。 Windows 和 macOS 拥有其特定的代码签名系统,这将使用户难以下载或启动未签名的应用程序。代码签名在forge.config.js
文件中添加
流程模型
多进程模型:在早期,浏览器通常使用单个进程来处理所有这些功能。 虽然这种模式意味着您打开每个标签页的开销较少,但也同时意味着一个网站的崩溃或无响应会影响到整个浏览器。Chrome 团队决定让每个标签页在自己的进程中渲染, 从而限制了一个网页上的有误或恶意代码可能导致的对整个应用程序造成的伤害。 然后用单个浏览器进程控制这些标签页进程,以及整个应用程序的生命周期。
electron包含两种进程:主进程和渲染进程。每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点。 主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力。主进程的主要目的是使用 BrowserWindow
模块创建和管理应用程序窗口。BrowserWindow 类
的每个实例创建一个应用程序窗口,且在单独的渲染器进程中加载一个网页。 您可从主进程用 window 的 webContent 对象
与网页内容进行交互。
const { BrowserWindow, app } = require("electron");const createWindow = () => {const win = new BrowserWindow({width: 800,height: 800,});win.loadURL("https://github.com"); //网页以地址的形式引入console.log(win.webContents); //主进程中输出
};app.whenReady().then(() => {createWindow();
});
当一个 BrowserWindow 实例被销毁时,与其相应的渲染器进程也会被终止。
渲染进程:每个 Electron 应用都会为每个打开的 BrowserWindow ( 与每个网页嵌入 ) 生成一个单独的渲染器进程。 洽如其名,渲染器负责 渲染 网页内容。 所以实际上,运行于渲染器进程中的代码是须遵照网页标准的 (至少就目前使用的 Chromium 而言是如此) 。渲染器无权直接访问 require 或其他 Node.js API
Preload 脚本:预加载(preload)脚本包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码 。 这些脚本虽运行于渲染器的环境中,却因能访问 Node.js API 而拥有了更多的权限。预加载脚本可以在 BrowserWindow 构造方法
中的 webPreferences .preload
选项里被附加到主进程。预加载脚本与浏览器共享同一个全局 Window 接口,并且可以访问 Node.js API,所以它通过在全局 window 中暴露任意 API 来增强渲染器,以便你的网页内容使用。
虽然预加载脚本与其所附着的渲染器在共享着一个全局 window 对象,但您并不能从中直接附加任何变动到 window 之上,因为 contextIsolation 是默认的,因此下面这段代码在窗口的控制台中输出undefined
。
//preload.js
window.myAPI = {desktop: true
}
//renderer.js
console.log(window.myAPI)
// => undefined
因此需要使用 contextBridge 模块
来安全地实现交互.
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('myAPI', {desktop: true
})
上下文隔离
上下文隔离功能将确保您的 预加载脚本 和 Electron的内部逻辑 运行在所加载的 webcontent网页 之外的另一个独立的上下文环境里。 这对安全性很重要,因为它有助于阻止网站访问 Electron 的内部组件 和 您的预加载脚本可访问的高等级权限的API
这意味着,实际上,您的预加载脚本访问的 window 对象并不是网站所能访问的对象。 例如,如果您在预加载脚本中设置 window.hello = ‘wave’ 并且启用了上下文隔离,当网站尝试访问window.hello对象时将返回 undefined。
自 Electron 12 以来,默认情况下已启用上下文隔离,并且它是 所有应用程序推荐的安全设置。
其实不使用contextBridge
创建预加载脚本就是没有开启上下文的方式。
进程通信
进程通信的表现形式例如 UI 调用原生 API 或从原生菜单触发 Web 内容的更改。
IPC通道
在 Electron 中,进程使用 ipcMain
和ipcRenderer
模块,通过开发人员定义的“通道”传递消息来进行通信。 这些通道是 任意 (您可以随意命名它们)和 双向 (您可以在两个模块中使用相同的通道名称)的。
- 渲染器进程到主进程(单向)
要将单向 IPC 消息从渲染器进程发送到主进程,您可以使用ipcRenderer.send
API 发送消息,然后使用ipcMain.on
API 接收。如下是一个事例,用户输入input内容,点击按钮会修改窗口的标题
index.htmlTitle: <input id="title"/><button id="btn" type="button">Set</button><script src="./renderer.js"></script>
renderer.js
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {const title = titleInput.valuewindow.aaa.setTitle(title) // 渲染主线程使用预加载脚本暴露的通道,使用其提供的方法
})
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("aaa", {//aaa是自定义的通道名//定义一个setTitle函数,通过send方法向主进程发生一个set-title消息,并携带参数setTitle: (title) => ipcRenderer.send("set-title", title), //send方法是一个异步行为
});
main.js
const { BrowserWindow, app, ipcMain } = require("electron");
const path = require("path");function handleSetTitle(e, title) {/* 事件对象 e 中获取发送该事件的 webContents 对象。在 Electron 中,webContents 代表了渲染器进程内的网页内容,每个 Electron 窗口里都有一个与其关联的 webContents 实例*/const webContents = e.sender;/* BrowserWindow.fromWebContents 方法,并传入前面获得的 webContents 对象,可以获取到关联该 webContents 的 BrowserWindow 实例。基本上这行代码是根据渲染器进程(webContents)找到对应的窗口对象(BrowserWindow 实例)。*/const w = BrowserWindow.fromWebContents(webContents);w.setTitle(title);
}const createWindow = () => {const win = new BrowserWindow({width: 800,height: 800,webPreferences: {preload: path.join(__dirname, "preload.js"),},webContents: {openDevTools: true,},});win.loadFile("index.html");
};app.whenReady().then(() => {//ipcMain.on监听set-title行为,箭头从渲染器进程中使用setTitle方法发送而来的set-title消息(可以理解vue的事件监听emit)ipcMain.on("set-title", handleSetTitle);createWindow();
});
其中事件对象的结构如下
- 渲染器进程到主进程(双向):如下是一个事例,用户选中一个文件,显示该文件的路径。
index.html<button type="button" id="btn">Open a File</button>File path: <strong id="filePath"></strong><script src="./renderer.js"></script>
renderer.js
const btn = document.getElementById("btn");
const filePathElement = document.getElementById("filePath");btn.addEventListener("click", async () => {const filePath = await window.electronAPI.openFile();filePathElement.innerText = filePath;
});
preload.js
const { contextBridge, ipcRenderer } = require("electron/renderer");
contextBridge.exposeInMainWorld("electronAPI", {/* 异步发送一个需要响应的消息,并且等待主进程的回复。这是异步操作,但允许以类似于同步调用的方式(使用 async/await 或者基于 Promise 的方法)等待响应。*/openFile: () => ipcRenderer.invoke("dialog:openFile"),
});
const { app, BrowserWindow, ipcMain, dialog } = require("electron/main");
const path = require("node:path");async function handleFileOpen() {/* dialog 模块用于显示原生的对话框,例如文件打开、保存、消息提示等。dialog 模块是 Electron 提供的一个系统对话框接口,可以在主进程中使用,用于与用户进行交互。dialog 模块通过其 showOpenDialog 方法用于显示一个文件选择对话框。这个方法是异步的,它会返回一个 Promise。当用户选择文件并关闭对话框时,Promise 被解决(resolved),并带有一个对象,这个对象包含了两个属性:canceled 和 filePaths。canceled 是一个布尔值,表示用户是否取消了对话框而没有选择任何文件filePaths 是一个数组,包含用户选择的一个或多个文件的完整路径。*/const { canceled, filePaths } = await dialog.showOpenDialog();if (!canceled) {return filePaths[0];}
}function createWindow() {const mainWindow = new BrowserWindow({webPreferences: {preload: path.join(__dirname, "preload.js"),},});mainWindow.loadFile("index.html");
}app.whenReady().then(() => {//会把处理完成的消息返回给渲染进程ipcMain.handle("dialog:openFile", handleFileOpen);createWindow();
});
- 主进程到渲染器进程:将消息从主进程发送到渲染器进程时,需要指定是哪一个渲染器接收消息。 消息需要通过其 ·WebContents 实例·发送到渲染器进程。 此 WebContents 实例包含一个 ·send ·方法,其使用方式与· ipcRenderer.send ·相同。如下实例是一个原生操作系统菜单控制的数字计数器。
index.htmlCurrent value: <strong id="counter">0</strong><script src="./renderer.js"></script>
renderer.js
const counter = document.getElementById("counter");window.electronAPI.onUpdateCounter((value) => {const oldValue = Number(counter.innerText);const newValue = oldValue + value;counter.innerText = newValue.toString();window.electronAPI.counterValue(newValue);
});
preload.js
const { contextBridge, ipcRenderer } = require("electron/renderer");
contextBridge.exposeInMainWorld("electronAPI", {onUpdateCounter: (callback) =>ipcRenderer.on("update-counter", (_event, value) => callback(value)),counterValue: (value) => ipcRenderer.send("counter-value", value),
});
main.js
const { app, BrowserWindow, Menu, ipcMain } = require("electron/main");
const path = require("node:path");function createWindow() {const mainWindow = new BrowserWindow({webPreferences: {preload: path.join(__dirname, "preload.js"),},});const menu = Menu.buildFromTemplate([{label: app.name,submenu: [{click: () => mainWindow.webContents.send("update-counter", 1),label: "Increment",},{click: () => mainWindow.webContents.send("update-counter", -1),label: "Decrement",},],},]);Menu.setApplicationMenu(menu);mainWindow.loadFile("index.html");// Open the DevTools.mainWindow.webContents.openDevTools();
}app.whenReady().then(() => {ipcMain.on("counter-value", (_event, value) => {console.log(value); // 主进程中输出打印});createWindow();
});
- 渲染器进程到渲染器进程:没有直接的方法可以使用 ipcMain 和 ipcRenderer 模块在 Electron 中的渲染器进程之间发送消息。将主进程作为渲染器之间的消息代理。 这需要将消息从一个渲染器发送到主进程,然后主进程将消息转发到另一个渲染器。从主进程将一个 MessagePort 传递到两个渲染器。 这将允许在初始设置后渲染器之间直接进行通信。
进程沙盒化
从 Electron 20 开始,渲染进程默认启用了沙盒,无需进一步配置。