Skip to main content

Electron

  • 一款应用广泛的跨平台桌面应用开发框架

  • Electron的本质是结合了ChromiumNode.jsNative API

  • 使用HTMLCSSJSWeb技术构建桌面应用程序。

Electron 流程模型

单一进程

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

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

多进程模型

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

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

主进程

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

窗口管理

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

BrowserWindow 类的每个实例创建一个应用程序窗口,且在单独的渲染器进程中加载一个网页。 您可从主进程用 windowwebContent 对象与网页内容进行交互。

const { BrowserWindow } = require('electron')

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

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

注意:渲染器进程也是为 web embeds 而被创建的,例如 BrowserView 模块。 嵌入式网页内容也可访问 webContents 对象。

由于 BrowserWindow 模块是一个 EventEmitter, 所以您也可以为各种用户事件 ( 例如,最小化 或 最大化您的窗口 ) 添加处理程序。

当一个 BrowserWindow 实例被销毁时,与其相应的渲染器进程也会被终止。

应用程序生命周期

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

这是一个实际的例子,这个 app 来源于快速入门指南,用 app API 创建了一个更原生的应用程序窗口体验。

// quitting the app when no windows are open on non-macOS platforms
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
原生 API

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

渲染器进程

每个 Electron 应用都会为每个打开的 BrowserWindow ( 与每个网页嵌入 ) 生成一个单独的渲染器进程。 洽如其名,渲染器负责 渲染 网页内容。 所以实际上,运行于渲染器进程中的代码是须遵照网页标准的 (至少就目前使用的 Chromium 而言是如此) 。

因此,一个浏览器窗口中的所有的用户界面和应用功能,都应与网页开发上使用相同的工具和规范来进行攥写。

警告:为了方便开发,可以用完整的 Node.js 环境生成渲染器进程。 在历史上,这是默认的,但由于安全原因,这一功能已被禁用。

Preload 脚本

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

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

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

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

虽然预加载脚本与其所附着的渲染器在共享着一个全局 window 对象,但您并不能从中直接附加任何变动到 window 之上,因为 contextIsolation 是默认的。

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

语境隔离(Context Isolation)意味着预加载脚本与渲染器的主要运行环境是隔离开来的,以避免泄漏任何具特权的 API 到您的网页内容代码中。

取而代之,我们將使用 contextBridge 模块来安全地实现交互:

const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('myAPI', {
desktop: true
})
console.log(window.myAPI)
// => { desktop: true }

此功能对两个主要目的来说非常有用:

  • 通过暴露 ipcRenderer 帮手模块于渲染器中,您可以使用 进程间通讯 ( inter-process communication, IPC ) 来从渲染器触发主进程任务 ( 反之亦然 ) 。

  • 如果您正在为远程 URL 上托管的现有 web 应用开发 Electron 封裝,则您可在渲染器的 window 全局变量上添加自定义的属性,好在 web 客户端用上仅适用于桌面应用的设计逻辑 。

创建项目

前提:确保安装了Nodenpm

初始化

npm init

生成package.json文件

  • 入口点 应当是 main.js (您很快就会创建它)
  • authorlicense_description_可为任意值,但对于应用打包是必填项。
{
"name": "electron_test",
"version": "1.0.0",
"description": "Electron测试项目",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "happlay71",
"license": "ISC"
}

安装Electron

npm i electron -D

修改package.json文件

  "scripts": {
"start": "electron ."
}

创建jsonmain键对应的值里的js文件,然后运行

npm start

初始化窗口,在主JS文件里:

// app: 应用,BrowserWindow:窗口对象
const { app, BrowserWindow } = require("electron");

app.on("ready", () => {
const win = new BrowserWindow({
width: 800,
height: 600,
autoHideMenuBar: true,
});
win.loadURL("http://happlay.online"); // 将网页显示在窗口内
});

窗口对象中的参数,更多

  • widthheight:宽和高
  • autoHideMenuBar:设置菜单栏状态,true为隐藏

页面安全策略

在浏览器页面播放流媒体或图片时会报错(但不影响使用),防止报错

htmlhead标签里加入这段代码

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;">

完善窗口管理

windows中当关闭浏览器最后一个网页后,浏览器应用关闭,Mac中则浏览器不关闭

新版main.js

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

function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
autoHideMenuBar: true,
});
// win.loadURL('http://happlay.online')
win.loadFile("./src/index.html");
}

app.on("ready", () => {
createWindow();
// 应用被激活且窗口为0时--为mac准备
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});

// windows--当最后一个页面关闭,关掉进程
app.on("window-all-closed", () => {
// 非mac系统
if (process.platform !== "darwin") app.quit();
});

配置自动重启

安装nodemon

npm i nodemon -D

修改JSON文件里的start的值

  "scripts": {
"start": "nodemon --exec electron ."
}

由于页面内容仍需要手动刷新,所以需要新加nodemon.json配置文件:

{
"ignore": [
"node_modules",
"dist"
],
"restartable": "r",
"watch": ["*.*"],
"ext": "html,js,css"
}
  • 终端输入 r 表示重启

主进程与渲染进程之间通信

在渲染进程中运行预加载脚本(可访问部分Node API

main.js文件中增加:

const path = require("path");

function createWindow() {
const win = new BrowserWindow({
……

webPreferences: {
preload: path.resolve(__dirname, './preload.js')
}
})
// win.loadURL('http://happlay.online')
win.loadFile("./src/index.html")
}

……

preload里的路径必须是绝对路径

在根目录下新建preload.js文件,写入渲染进程不能调用的代码

如:

可以在渲染进程的控制台中查看到版本信息

console.log("preload", process.version);

进程的执行顺序为 主进程->预加载->渲染进程

不止是在预加载脚本中显示,要使渲染进程可以获取版本信息,需要二者桥接在一起

preload.js

const { contextBridge } = require("electron");

// 接收一个key和value
contextBridge.exposeInMainWorld("myAPI", {
version: process.version,
});

render.js

const btn1 = document.getElementById("btn1");

btn1.onclick = () => {
alert(myAPI.version);
};

但这只能解决部分 Node API,还有部分如__dirname之类的无法在渲染进程中使用,因此有了下面的进程通信

进程通信

渲染进程 ➡️ 主进程

将文本框的内容写入磁盘

index.html

<input id="input" type="text" />
<button id="btn2">在根目录下创建hello.txt文件</button>

render.js

const btn2 = document.getElementById("btn2");
const input = document.getElementById("input");

btn2.onclick = () => {
myAPI.saveFile(input.value);
};

preload.js

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

contextBridge.exposeInMainWorld("myAPI", {
version: process.version,
// 渲染进程向主进程通信
saveFile: (data) => {
// send(信道,数据)
ipcRenderer.send("file-save", data);
},
});

main.js

const { app, BrowserWindow, ipcMain } = require("electron");
const path = require("path");
const fs = require("fs");

// 写入文件
function writeFile(_, data) {
fs.writeFileSync(path.resolve(__dirname, "./hello.txt"), data);
}

function createWindow() {
const win = new BrowserWindow({
……
});

// 获取信道,调用方法
ipcMain.on("file-save", writeFile);

win.loadFile("./src/index.html");
}

……

渲染进程 ⬅️➡️ 主进程

读取磁盘中的文件内容

render.js

const btn3 = document.getElementById("btn3");
// 因为预加载脚本中invoke的返回值是Promise,需要转换
btn3.onclick = async () => {
let data = await myAPI.readFile();
alert(data);
};

preload.js

readFile: () => {
return ipcRenderer.invoke("file-read");
};

main.js

function readFile() {
const res = fs.readFileSync(path.resolve(__dirname, './hello.txt'))
return res.toString()
}


……
ipcMain.handle('file-read', readFile)

主进程 ➡️ 渲染进程

类似渲染进程发送给主进程

  1. render.js中仍通过myAPI的方式与预加载脚本联系
window.onload = () => {
myAPI.getMessage(logMessage);
};

function logMessage(event, str) {
console.log(event, str);
}
  1. preload.js中用ipcRenderer.on('信道',回调函数)来接收数据
const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("myAPI", {
/*******/
getMessage: (callback) => {
return ipcRenderer.on("message", callback);
},
});
  1. 主进程使用win.webContents.send('信道',数据)发送数据
// ⽤于创建窗⼝
function createWindow() {
/**********/
// 加载⼀个本地⻚⾯
win.loadFile(path.resolve(__dirname, "./pages/index.html"));
// 创建⼀个定时器
setTimeout(() => {
win.webContents.send("message", "你好啊!");
}, 6000);
}

打包

使用electron-builder打包应用:

  1. 安装
npm install electron-builder -D
  1. package.json 中进⾏相关配置,具体配置如下:
{
"name": "video-tools", // 应⽤程序的名称
"version": "1.0.0", // 应⽤程序的版本
"main": "main.js", // 应⽤程序的⼊⼝⽂件
"scripts": {
"start": "electron .", // 使⽤ `electron .` 命令启动应⽤程序
"build": "electron-builder" // 使⽤ `electron-builder` 打包应⽤程序,⽣成安装包
},
"build": {
"appId": "com.atguigu.video", // 应⽤程序的唯⼀标识符
// 打包windows平台安装包的具体配置
"win": {
"icon": "./logo.ico", //应⽤图标
"target": [
{
"target": "nsis", // 指定使⽤ NSIS 作为安装程序格式
"arch": ["x64"] // ⽣成 64 位安装包
}
]
},
"nsis": {
"oneClick": false, // 设置为 `false` 使安装程序显示安装向导界⾯,⽽不是⼀键安装
"perMachine": true, // 允许每台机器安装⼀次,⽽不是每个⽤户都安装
"allowToChangeInstallationDirectory": true // 允许⽤户在安装过程中选择安装⽬录
}
},
"devDependencies": {
"electron": "^30.0.0", // 开发依赖中的 Electron 版本
"electron-builder": "^24.13.3" // 开发依赖中的 `electron-builder` 版本
},
"author": "tianyu", // 作者信息
"license": "ISC", // 许可证信息
"description": "A video processing program based on Electron" // 应⽤程序的描述
}
  1. 执行打包命令
npm run build

electron-vite

electron-vite 是⼀个新型构建⼯具,旨在为 Electron 提供更快、更精简的体验。主要由五部分组成:

  • ⼀套构建指令,它使⽤ Vite 打包你的代码,并且它能够处理 Electron 的独特环境,包括Node.js 和浏览器环境。

  • 集中配置主进程、渲染器和预加载脚本的 Vite 配置,并针对 Electron 的独特环境进⾏预配置。

  • 为渲染器提供快速模块热替换(HMR)⽀持,为主进程和预加载脚本提供热重载⽀持,极⼤地提⾼了开发效率。

  • 优化 Electron 主进程资源处理。

  • 使⽤ V8 字节码保护源代码。

electron-vite 快速、简单且功能强⼤,旨在开箱即⽤。

官⽹地址:https://cn-evite.netlify.app/