Electron
-
简介
- Electron 是一个使用 JavaScript、 HTML 和 CSS 构建桌面应用程序的框架。通过将 Chromium 和 Node.js 嵌入到它的二进制文件中,Electron 允许你维护一个 JavaScript 代码库,并创建可以在 Windows、 macOS 和 Linux 上运行的跨平台应用程序ーー不需要本地开发经验
-
发展历程
- Electron 的前身是 Atom Shell,最开始的设计目标就是为 Atom 编辑器而生,实际上 Electron 的作者之前也是 node-webkit 的核心贡献者,而 node-webkit 正是 NW.js 的前身。在作者离开英特尔公司(当初大力支持 node-webkit)后,就加入了 Github 项目组,孵化了 Atom Shell 这个项目,再到后来(2014 年)正式更名为 Electron,发展至今。
-
框架选择
- Electron 和众多类似的产品目标非常简单,他们将 chromium(这里不同的框架选择不同,比如 Tauri 选择了原生的 WebView,而 WebView2 选择了 Edge 内核)和 Node.js(这里不同框架采取策略也不同,比如 Webview2 不提供默认的运行时,而 Tauri 选择了 Rust 等等)利用 C++等原生语言集成起来,提供了一整套基于 Web 的运行环境,并提供了与底层 OS 交互的便捷 API,目的就是为了让大家使用 Web 的技术栈去开发客户端原生应用,从而实现不同操作系统之间的跨平台开发。基于 V8 和 LibUV 构建的 Node.js 让前端迅速发展到一个不可思议的地步,Electron 也同样依赖于这样的技术。
-
前景
- 越是底层的语言,所需要软件开发人员的专业素质越高,维护成本也就越高,因此在互联网高速发展的时代,系统开发相关的人才不断减少,更多涌现出大量的应用开发人员,进入 Web 高速发展的阶段。由于互联网业务的高速发展、软硬件技术的不断迭代更新,形成的一种趋势。因此 Electron、React Native、Flutter、uniApp、Taro 这类跨端解决方案越来越受大家欢迎也是必然的结果。
-
架构
-
electron 的架构设计很大程度上是受到 Chromium 的启发。Chromium 是 Google 开源的浏览器内核代码,Chrome、Edge、Opera 等等浏览器都是基于 Chromium 来实现的。Chromium 堪称是全世界最复杂的软件应用
net
模块,实现了主机解析,cookies,网络改变探测,SSL,资源缓存,ftp,HTTP, OCSP 实现,代理 (SOCKS 和 HTTP) 配置,解析,脚本获取(包括各种不同系统下实现),QUIC,socket 池,SPDY,WebSockets 等等,每套协议都是十分复杂的。v8
模块,包括字节码解析器,JIT 编译器,多代 GC,inspector (调试支持),内存和 CPU 的 profiler(性能统计),WebAssembly 支持,两种 post-mortem diagnostics 的支持,启动快照,代码缓存、代码热点分析等等。Skia
模块,用点画出各种图。然而里面包括十几种矢量的绘制,文字绘制、GPU 加速、矢量的指令录制以及回放(还要能支持线程安全)、各种图像格式的编解码、GPU 渲染优化等等。Blink
内核,这个更复杂,HTML 和 CSS 规范就得近万页了,要将它们完全实现是一个巨大的工作量,再加上实现 Layout 的成本,涉及到非常庞大的计算,还要考虑极致的性能,保证浏览器的渲染能够流畅快速。- 还有音视频相关、沙箱、插件、UI 等等,就不赘述了,总之称之为巨型软件应用也不为过
-
browser architecture
在浏览器实现方面是没有标准规范的,这取决于不同浏览器内部的实现细节。在最早期,浏览器渲染都是在同一个进程里面完成的,后来 Chrome 采用了多进程架构,演变成了这种模式:
- 浏览器主进程,实现浏览器的主要 UI、负责和文件、网络等等操作系统底层接口对接通信
- 渲染进程,每一个 tab 独立开一个渲染进程,核心进行 web 代码解析和渲染工作
- 插件进程,负责浏览器插件的控制
- GPU 进程,独立的进程负责处理 GPU 图像的渲染绘制
- 优点
- 保证每个 tab 独立进程,这样在某一个页面 crash 的时候,仅仅影响当前的 Tab,而不至于让整个浏览器崩溃。这提升了软件的健壮性和用户体验。
- 有利于实现沙箱隔离的安全机制,基于进程可以很方便地控制不同页面之间的安全访问策略,确保每个 renderer 进程在自己单独的沙箱环境内安全地运行。
- 缺点
- 内存消耗,每一个进程需要开辟独立的内存空间,不同进程之间的内存很难做到共享
-
Electron 本身参考了这个架构的实现,将各个 GUI 窗口通过 renderer 进程实现,交由 chromium 来加载渲染,主进程集成 Node.js,负责与系统 API 交互,处理核心事务
- 源码结构
Electron ├── build/ - 构建相关 ├── buildflags/ - feature flag ├── chromium_src/ - chromium的一份拷贝(源码仅包含build文件) ├── default_app/ - 默认启动时的app程序 ├── docs/ -文档 ├── lib/ - 使用JS/TS编写的模块 | ├── browser/ - 主进程初始化相关 | | ├── api/ - 主进程暴露的API,通过_linkedBinding调用C++模块 | ├── common/ - 主进程和渲染进程共用代码 | | └── api/ - 主进程和渲染进程共同暴露的API | ├── renderer/ - 渲染进程初始化相关 | | ├── api/ - 渲染进程API | | └── web-view/ - webview相关逻辑 ├── patches/ - 关于依赖的一些patch,主要是chromium、node、v8的 ├── shell/ - C++编写的模块 | ├── app/ - 入口 | ├── browser/ - 主进程相关 | | ├── ui/ - 系统UI组件的一些实现 | | ├── api/ - 主进程API实现 | | ├── net/ - 网络相关实现 | | ├── mac/ - Mac系统下的一些实现(OC实现) | ├── renderer/ - 渲染进程相关 | | └── api/ - 渲染进程API实现 | └── common/ - 主进程渲染进程通用实现 | └── api/ - 主进程渲染进程通用API实现 └── BUILD.gn - 构建入口
- Electron 在源码上与 Node.js 实现类似,lib 目录使用 js 实现,通过_linkedBinding 来调用 C++的模块,在一开始会被编译进内存中,通过 C++程序装在启动。C++启动入口在 shell/app/electron_library_main.mm:
int ElectronMain(int argc, char* argv[]) { electron::ElectronMainDelegate delegate; content::ContentMainParams params(&delegate); params.argc = argc; params.argv = const_cast<const char**>(argv); electron::ElectronCommandLine::Init(argc, argv); return content::ContentMain(std::move(params)); }
- 这里依赖的 content 实际上是 chromium 的 content 模块,chromium 将浏览器主进程部分和渲染进程进行了抽象,主进程部分的逻辑在 blink 模块实现,而渲染进程相关的实现则封装在 content 模块,所以这里调用 content 就可以使用 chromium 的 renderer 进程来进行启动渲染了。
-
应用
- vscode源码实现
- VSCode的窗口管理机制
环境搭建
- 安装 Node.js: Electron 基于 Node.js,首先确保已安装 Node.js。
- 安装 Electron: 使用 npm(Node.js 包管理器)全局安装 Electron。
npm install -g electron
- 创建项目 新建一个文件夹作为项目根目录,在该目录下初始化 npm 项目,并安装 Electron 作为开发依赖,创建 main.js 作为主进程入口文件,创建 index.html 作为应用的用户界面
mkdir my-electron-app
cd my-electron-app
npm init -y
npm install electron --save-dev
主进程与渲染进程
-
简介
- 主进程就是使用 electron main.js 的时候,就会开启一个主进程:Electron,一个应用只会有一个主进程,只有主进程才能进行 GUI 的 API 操作
- 渲染进程会在新建窗口后,loadURL 的时候开始渲染进程:Electron Helper,渲染进程才是 GUI 的表现
- loadURL 也可以加载一个 HTML,就跟我们浏览器的 Tab 差不多了
-
渲染进程、窗口、index.html 之间的关系
- 主进程中 new BrowserWindow()会开启一个系统窗口,但不会开启一个独立的进程,窗口由主进程管理
- 窗口可以 loadURL 加载一个页面,此时会开启这个页面的渲染进程
- 当 loadURL 加载一个 index.html 文件的时候,此时渲染进程里运行的就是这个 index.html 的内容
-
webpack 处理
- 不同进程的 js,肯定是不同的入口,因此采用多入口方式打包即可。
主进程 js(就是 electron main.js 中 main.js 这个入口相关的 js),需要一个生产环境 webpack 配置文件。因为开发环境下不需要 webpack 做什么,所以不需要。 - 渲染进程 js(即 index.html 里面加载的 js)开发环境下需要用 webpack-dev-server 做热刷新,所以会单独一个 webpack 配置文件。渲染进程 js 在生产环境下,需要一个 webpack 配置文件。
- 为什么主进程的和渲染进程的 js 生产环境下不使用同一个配置文件,因为处理方式有些不同,因此不再同一个文件里写多入口方式配置而写成两个文件。
- 如果打开两个及以上窗口,即会有很多个渲染进程,则只需要在 render 的配置文件里使用多入口配置就好了
webpack.config.main.prod.js // 主进程生产环境配置文件 webpack.config.render.prod.js // 渲染进程生产环境配置文件 webpack.config.render.dev.js // 渲染进程开发环境配置文件
- 配置
- target: ‘node’
- target: ‘web’:表示的就是,处理 js 的时候采用 node(采用 web)环境去处理,这样,在识别到一些 api 比如 node 的 fs 的时候,就不会报错,知道是什么。同理,我们在处理我们的 js 的时候也需要根据其运行环境去处理,这样就能识别一些语法、api 用法等。
- 主进程设置:target: ‘electron-main’
- 渲染进程设置:target: ‘electron-renderer’
- 不同进程的 js,肯定是不同的入口,因此采用多入口方式打包即可。
-
关系
- 一个标签页死了,另一个标签页不会死,照常运行
- 一个标签页死了,浏览器不会卡死,还是可以开启其他窗口
- 一个标签页死了,浏览器也会死
-
特点
- 渲染进程结束,主进程不会结束(标签页关了,浏览器还在)
- 主进程结束,渲染进程都会结束(浏览器关闭,标签页都会关闭)
- 渲染进程如果太过分,资源用光,也会导致主进程崩溃
-
生命周期
主进程 (main.js)
- 启动应用: 使用
Electron
模块启动应用
const {app, BrowserWindow} = require('electron')
function createWindow() {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
},
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(createWindow)
主进程(Main Process)生命周期事件 app
will-quit
- 触发时机:所有窗口都已关闭,并且应用即将退出前触发
- 用途:执行清理工作,如关闭数据库连接、保存设置等
- 注意:在 Windows 系统中,如果应用程序因系统关机/重启或用户注销而关闭,那么这个事件不会被触发
quit
- 触发时机:应用已经退出,所有窗口已经关闭,且不会有任何其他操作
- 用途:记录应用最后状态或进行最终的清理工作
- 注意:在 Windows 系统中,如果应用程序因系统关机/重启或用户注销而关闭,那么这个事件不会被触发
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') {
app.quit()
}
})
ready
- 触发时机:Electron 及 Chromium 已经初始化完毕,可以开始创建浏览器窗口
- 用途:初始化应用,创建主窗口,加载渲染进程等
app.on('ready', () => {
createWindow()
})
window-all-closed
- 触发时机:所有窗口(包括主窗口和其他子窗口)都已经被关闭
- 用途:决定是否退出应用,Windows 和 Linux 上默认不会退出,macOS 上会退出,除非监听此事件并阻止默认行为
- 注意:如果你没有监听此事件并且所有窗口都关闭了,默认的行为是控制退出程序;但如果你监听了此事件,你可以控制是否退出程序。 如果用户按下了
Cmd + Q
,或者开发者调用了app.quit()
,Electron
会首先关闭所有的窗口然后触发will-quit
事件,在这种情况下window-all-closed
事件不会被触发
const {app} = require('electron')
app.on('window-all-closed', () => {
app.quit()
})
before-quit
- 触发时机:在应用尝试关闭窗口之前触发,通常是在用户点击窗口关闭按钮时,在程序关闭窗口前发信号
- 用途:可以在这里执行一些确认操作,如提示用户是否保存更改
- 注意:如果由
autoUpdater.quitAndInstal()
退出应用程序 ,那么在所有窗口触发 close 之后 才会触发 before-quit 并关闭所有窗口。在 Windows 系统中,如果应用程序因系统关机/重启或用户注销而关闭,那么这个事件不会被触发
web-contents-created
- 触发时机:每当新的 WebContents(即渲染进程)被创建时触发
- 用途:可以用来对新打开的页面进行配置或监控
browser-window-created
- 触发时机:每当新的 BrowserWindow 被创建时
- 用途:可以用来统一配置新窗口的属性或行为
will-finish-launching
- 触发时机:应用程序完成基础的启动
- 用途:在应用程序启动时执行一些初始化任务, 在 Windows 和 Linux 中,
will-finish-launching
事件与ready
事件是相同的; 在 macOS 中,这个事件相当于 NSApplication 中的applicationWillFinishLaunching
提示
activate
- 触发时机:应用被激活时发出
- 用途:首次启动应用程序、尝试在应用程序已运行时或单击应用程序的坞站或任务栏图标时重新激活它
app.on('activate', function () {
console.log('activate')
})
open-url
- 触发时机:应用中要打开一个 URL 网址时发出
app.on('open-url', function () {
console.log('open-url')
})
open-file
- 触发时机:在应用中要打开一个文件时发出
app.on('open-file', function () {
console.log('open-file')
})
did-become-active
- 触发时机:切换到这个应用的时候触发
- 用途:监听没有窗口的应用或者程序第一次启动
did-rele-active
- 触发时机:当应用不再处于活动状态且没有焦点时发出
- 用途:单击另一个应用程序或使用 macOS App 切换器切换到另一个应用程序来触发
continue-activity
- 触发时机:恢复来自其他设备的活动时发出
will-continue-activity
- 触发时机:将要恢复来自其他设备的活动时发出
certificate-error
- 触发时机:当对 url 的 certificate 证书验证失败的时候发出。如果需要信任这个证书,你需要阻止默认行为 event.preventDefault() 并且调用
const {app} = require('electron')
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
if (url === 'https://github.com') {
// Verification logic.
event.preventDefault()
callback(true)
} else {
callback(false)
}
})
17.select-client-certificate
- 触发时机:当一个客户证书被请求的时候发出,url 指的是请求客户端认证的网页地址,调用 callback 时需要传入一个证书列表中的证书
const {app} = require('electron')
app.on('select-client-certificate', (event, webContents, url, list, callback) => {
event.preventDefault()
callback(list[0])
})
gpu-info-update
- 触发时机:每当有 GPU 信息更新时触发
render-process-gone
- 触发时机:当渲染进程崩溃或退出时触发
child-process-gone
- 触发时机:当子进程崩溃或退出时触发, 这种情况通常因为进程崩溃或被杀死。 子进程不包括渲染器进程。
渲染进程 (index.html + renderer.js)
- 在
index.html
中引入renderer.js
进行前端逻辑处理 - 利用
preload.js
进行安全地 Node.js API 访问控制
渲染进程(Render Process)生命周期事件
- 虽然 Electron 没有直接定义渲染进程的生命周期事件,但你可以通过 DOM 事件和 Electron 提供的 IPC 机制来监听和响应相关事件
- DOMContentLoaded
- 触发时机:页面的 DOM 结构已经加载完成,但可能资源还在加载中
- 用途:可以在此时开始操作 DOM 元素
- load
- 触发时机:页面及其所有资源(如图片、脚本等)已经加载完毕
- 用途:执行需要页面完全加载后才能进行的操作
- 通过 IPC 与主进程通信
- 用途:可以在特定事件发生时(如页面加载完成),通过 ipcRenderer.send 向主进程发送消息,实现跨进程的通信
browser-window-blur
- 触发时机:一个 browserWindow 失去焦点时触发
browser-window-focus
- 触发时机:一个 browserWindow 获得焦点时触发
browser-window-created
- 触发时机:一个 browserWindow 创建时触发
web-contents-created
- 触发时机:一个 webContents 创建时触发
login
- 当 webContents 要进行基本身份验证时触发
const {app} = require('electron')
app.on('login', (event, webContents, details, authInfo, callback) => {
event.preventDefault()
callback('username', 'secret')
})
窗口生命周期
- ready: app 初始化完成
- dom-ready: 一个窗口中的文本加载完成
- did-finish-load: 导航完成时触发
- closed: 当窗口关闭时触发,此时应删除窗口引用
- window-all-closed: 所有窗口都被关闭时触发
- before-quit: 在关闭窗口之前触发
- will-quit: 在窗口关闭并且应用退出时触发
- quit: 当所有窗口被关闭时触发
const {app, BrowserWindow} = require('electron')
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
})
win.loadFile('index.html')
win.webContents.on('dom-ready', () => {
console.log('dom ready')
})
win.webContents.on('did-finish-load', () => {
console.log('did finish load')
})
win.on('closed', () => {
console.log('closed')
})
}
app.whenReady().then(() => {
console.log('ready finish')
createWindow()
})
app.on('window-all-closed', () => {
console.log('all window close')
app.quit()
})
app.on('before-quit', () => {
console.log('before quit')
})
app.on('will-quit', () => {
console.log('will quit')
})
app.on('quit', () => {
console.log('quit')
})
/*
运行结果
ready finish
dom ready
did finish load
closed
all window close
before quit
will quit
quit
*/
注意事项
- 应用的关闭逻辑(如是否退出应用)需要根据操作系统和应用需求来定制,特别是在处理
window-all-closed
事件时 - 生命周期事件的监听和处理应该谨慎设计,以避免内存泄漏或不期望的应用行为
使用 IPC 进行进程间通信时,注意数据的安全性和同步问题
主进程和渲染进程的相互通讯
-
IPC:主进程和渲染进程各自拥有一个 IPC 模块
-
由于主进程和渲染进程各自负责不同的任务,而对于需要协同完成的任务,它们需要相互通讯。IPC 就为此而生,它提供了进程间的通讯。但它只能在主进程与渲染进程之间传递信息(即渲染进程之间不能进行直接通讯)
-
渲染进程 TO 主进程
-
ipcRenderer.send + ipcMain.on
-
主进程通过 ipcMain.on 来监听渲染进程的消息,主进程接收到消息后,可以回复消息,也可以不回复。如果回复的话,通过 event.reply 发送另一个事件,渲染进程监听这个事件得到回复结果。如果不回复消息的话,渲染进程将接着执行 ipcRenderer.send 之后的代码。
// 渲染进程 // 点击关闭按钮,就需要渲染进程向主进程发送隐藏主窗口的请求 const electron = require('electron') const {ipcRenderer} = electron closeDom.addEventListener('click', () => { ipcRenderer.send('render-send-to-main', '我是渲染进程通过 send 发送的消息') }) // 主进程 // 主进程通过ipcMain接收消息,ipcMain.on方法的第一个参数也为消息管道的名称,与ipcRenderer.send的名称对应,第二个参数是接收到消息的回调函数 const {ipcMain} = require('electron') ipcMain.on('render-send-to-main', (event, message) => { console.log(`receive message from render: ${message}`) }) // 回复 // render.js const {ipcRenderer} = require('electron') // send 方法发送,并绑定另一个事件接收返回值 function sendMessageToMain() { ipcRenderer.send('render-send-to-main', '我是渲染进程通过 send 发送的消息') } ipcRenderer.on('main-reply-to-render', (event, message) => { console.log('replyMessage', message) // 'replyMessage 主进程通过 reply 回复给渲染进程的消息' }) //主进程 // main.js const {ipcMain} = require('electron') ipcMain.on('render-send-to-main', (event, message) => { console.log(`receive message from render: ${message}`) event.reply('main-reply-to-render', '主进程通过 reply 回复给渲染进程的消息') })
-
-
ipcRenderer.invoke + ipcMain.handle
- 主进程通过 ipcMain.handle 来处理渲染进程发送的消息,主进程接收到消息后,可以回复消息,也可以不回复。如果回复消息的话,可以通过 return 给渲染进程回复消息;如果不回复消息的话,渲染进程将接着执行 ipcRenderer.invoke 之后的代码。渲染进程异步等待主进程的回应, invoke 的返回值是一个 Promise
// 渲染进程 const {ipcRenderer} = require('electron') async function invokeMessageToMain() { const replyMessage = await ipcRenderer.invoke('render-invoke-to-main', '我是渲染进程通过 invoke 发送的消息') console.log('replyMessage', replyMessage) } // 主进程 // main.js const {ipcMain} = require('electron') ipcMain.handle('render-invoke-to-main', async (event, message) => { console.log(`receive message from render: ${message}`) const result = await asyncWork() return result }) const asyncWork = async () => { return new Promise((resolve) => { setTimeout(() => { resolve('延迟 2 秒获取到主进程的返回结果') }, 2000) }) }
-
ipcRenderer.sendSync + ipcMain.on
- 主进程通过 ipcMain.on 来处理渲染进程发送的消息;主进程通过 event.returnValue 回复渲染进程消息;如果 event.returnValue 不为 undefined 的话,渲染进程会等待 sendSync 的返回值才执行后面的代码,请保证 event.returnValue 是有值的,否则会造成非预期的影响。
// render.js 渲染进程 const {ipcRenderer} = require('electron') function sendSyncMessageToMain() { const replyMessage = ipcRenderer.sendSync('render-send-sync-to-main', '我是渲染进程通过 syncSend 发送给主进程的消息') console.log('replyMessage', replyMessage) // '主进程回复的消息' } // main.js 主进程 const { ipcMain } = require('electron'); ipcMain.on('render-send-sync-to-main', (event, message) => { console.log(`receive message from render: ${message}`) event.returnValue = '主进程回复的消息'; }
-
注意
- 跨进程通讯时,需要考虑异步操作的问题,可以使用 Promise 或 Async/Await 来解决
- 在使用 ipcRenderer.on 方法监听事件时,需要注意避免重复监听同一事件,否则可能会导致内存泄漏
- 在使用 ipcMain.on 方法监听事件时,需要注意避免在回调函数中进行长时间的计算或 I/O 操作,否则可能会影响主进程的性能
-
-
-
主进程 TO 渲染进程
- webContents.send + ipcRenderer.on
// 主进程向渲染进程发送消息是通过渲染进程的webContents。在mainWindow渲染进程设定了任务后,会传输给主进程任务信息,当任务时间到了,主进程会创建提醒窗口remindWindow,并通过remindWindow.webContents将任务名称发给remindWindow。 function createRemindWindow(task) { remindWindow = new BrowserWindow({ //options }) remindWindow.loadURL(`file://${__dirname}/src/remind.html`) //主进程发送消息给渲染进程 remindWindow.webContents.send('setTask', task) } // 在remindWindow渲染进程中,通过ipcRenderer.on接受消息 ipcRenderer.on('setTask', (event, task) => { document.querySelector('.reminder').innerHTML = `<span>${decodeURIComponent(task)}</span>的时间到啦!` })
-
渲染进程 TO 渲染进程
- 将主进程作为渲染器之间的消息代理。 这需要将消息从一个渲染器发送到主进程,然后主进程将消息转发到另一个渲染器。
- 从主进程将一个 MessagePort 传递到两个渲染器。 这将允许在初始设置后渲染器之间直接进行通信。
// 渲染进程之间传递消息,可以通过主进程中转,即窗口A先把消息发送给主进程,主进程再把这个消息发送给窗口B,这种非常常见。也可以从窗口A直接发消息给窗口B,前提是窗口A知道窗口B的webContents的id ipcRenderer.sendTo(webContentsId, channel, ...args) // 在渲染进程中Electron可以访问Node.js API,这样做的前提是在创建窗口时配置webPreferences的nodeIntegration: true (是否启用Node integration)和contextIsolation: false(是否在独立 JavaScript 环境中运行 Electron API和指定的preload 脚本)
其他常用 API 事件
app(Main Process) 模块
- 应用初始化与退出
app.whenReady()
:返回 Promise,当 Electron 初始化完成。 可用作检查 app.isReady() 的方便选择,假如应用程序尚未就绪,则订阅 ready 事件app.isReady()
: 判断应用是否初始化完成app.requestSingleInstanceLock([callback])
: 请求单一实例锁,防止应用被多次启动app.releaseSingleInstanceLock()
: 释放单一实例锁app.hide()
:隐藏所有应用窗口,不是最小化app.isHidden()
: 判断应用是否隐藏app.show()
: 显示隐藏的应用窗口app.quit()
: 关闭所有窗口并退出应用app.exit(exitCode)
:立即退出应用,不执行任何清理工作app.relaunch([options])
: 重新启动应用app.setAppLogsPath([path])
: 自定义日志路径,参数必须是绝对路径
- 应用信息与行为
APP.getLocale()
: 获取当前应用程序区域app.setName(name)
: 设置应用名称app.getName()
: 获取应用名称app.getVersion()
: 获取应用版本app.setPath(name, path)
: 重置特定路径,如 userData、logs 等app.addRecentDocument(path)
:添加文件到最近文档列表app.getAppPath()
: 获取应用路径app.getPath(name)
: 获取特定路径- name 参数可选
- home 用户的 home 文件夹(主目录)
- appData 每个用户的应用程序数据目录,默认情况下指向:
- %APPDATA% Windows 中
- $XDG_CONFIG_HOME or ~/.config Linux 中
- ~/Library/Application Support macOS 中
- userData 储存你应用程序配置文件的文件夹,默认是 appData 文件夹附加应用的名称 按照习惯用户存储的数据文件应该写在此目录,同时不建议在这写大文件,因为某些环境会备份此目录到云端存储。
- sessionData 此目录存储由 Session 生成的数据,例如 localStorage,cookies,磁盘缓存,下载的字典,网络 状态,开发者工具文件等。 默认为 userData 目录。 Chromium 可能在此处写入非常大的磁盘缓存,因此,如果您的应用不依赖于浏览器存储(如 localStorage 或 cookie)来保存用户数据,建议将此目录设置为其他位置,以避免污染 userData 目录。
- temp 临时文件夹
- exe 当前的可执行文件
- module The libchromiumcontent 库
- desktop 当前用户的桌面文件夹
- documents 用户文档目录的路径
- downloads 用户下载目录的路径
- music 用户音乐目录的路径
- pictures 用户图片目录的路径
- videos 用户视频目录的路径
- recent 用户最近文件的目录 (仅限 Windows)。
- logs 应用程序的日志文件夹
- crashDumps 崩溃转储文件存储的目录。
- name 参数可选
autoUpdater 应用程序自动更新
error
更新错误checking-for-update
检查更新update-available
发现新版本,更新将自动下载update-not-available
没有发现新版本update-downloaded
更新下载完成,可以重启应用before-quit-for-update
应用程序即将退出,用于更新
- 用途:当此 API 被调用时,会在所有窗口关闭之前发出 before-quit 事件。 因此,如果您希望在关闭窗口进程退出之前执行操作,则应该侦听此事件,以及侦听 before-quit
autoUpdater.setFeedURL(options)
设置检查更新的 url,并且初始化自动更新autoUpdater.getFeedURL()
获取 feedURLautoUpdater.checkForUpdates()
询问服务器是否有更新autoUpdater.quitAndInstall()
重启应用并在下载后安装更新,它只应在发出 update-downloaded 后方可被调用
- 注意:
- 在此机制下,调用 autoUpdater.quitAndInstall() 将首先关闭所有应用程序窗口,并且在所有窗口都关闭之后自动调用 app.quit()
- 严格来讲,执行一次自动更新不一定要调用此方法。因为下载更新文件成功之后,下次应用启动的时候会强制更新。
BaseWindow 创建和控制窗口
const parent = new BaseWindow();const child = new BaseWindow({ parent })
: 创建父子窗口const parent = new BaseWindow();const child = new BaseWindow({ parent, modal: true })
: 创建模态窗口BaseWindow.closed(callback)
: 当窗口被关闭时触发BaseWindow.blur()
: 窗口失去焦点时触发BaseWindow.focus()
: 窗口获得焦点时触发BaseWindow.show()
: 显示窗口BaseWindow.hide()
: 隐藏窗口BaseWindow.maximize()
: 窗口最大化时触发BaseWindow.minimize()
: 窗口最小化时触发BaseWindow.restore()
: 窗口从最小化状态恢复时触发BaseWindow.unmaximize()
: 窗口从最大化状态退出时触发BaseWindow.resize()
: 调整窗口大小后触发BaseWindow.move()
: 调整窗口位置后触发
BrowserView 创建和控制浏览器视图
const view = new BrowserView([options])
: 创建浏览器视图view.webContents
:视图的 WebContents 对象,用于控制浏览器视图的网页内容view.setAutoResize({width:boolean;height:boolean;horizontal :boolean;vertical:boolean})
: 设置视图的自动调整大小选项view.setBounds(bounds)
: 调整视图的大小,并将它移动到窗口边界view.getBounds()
: 获取视图的边界view.setBackgroundColor(color)
: 设置视图的背景颜色
BrowserWindow 创建和控制浏览器窗口
const win = new BrowserWindow([options])
: 创建浏览器窗口ready-to-show
: 优雅地显示窗口- 在加载页面时,渲染进程第一次完成绘制时,如果窗口还没有被显示,渲染进程会发出 ready-to-show 事件 。 在此事件后显示窗口将没有视觉闪烁
- 这个事件通常在 did-finish-load 事件之后发出,但是页面有许多远程资源时,它可能会在 did-finish-load 之前发出事件
const {BrowserWindow} = require('electron')
const win = new BrowserWindow({show: false})
win.once('ready-to-show', () => {
win.show()
})
- 设置 backgroundColor(即使对于使用 ready-to-show 事件的应用,仍建议 设置 backgroundColor,以使应用感觉更接近原生)
const {BrowserWindow} = require('electron')
const win = new BrowserWindow({backgroundColor: '#2e2c29'})
win.loadURL('https://github.com')
- 父子窗口 parent
const {BrowserWindow} = require('electron')
const top = new BrowserWindow()
const child = new BrowserWindow({parent: top})
child.show()
top.show()
- 模态窗口(模态窗口是禁用父窗口的子窗口。 要创建模态窗口,必须同时设置 parent 和 modal 属性)
const {BrowserWindow} = require('electron')
const top = new BrowserWindow()
const child = new BrowserWindow({parent: top, modal: true, show: false})
child.loadURL('https://github.com')
child.once('ready-to-show', () => {
child.show()
})
Electron 中的流程
-
流程模型
-
多进程模型
-
主进程
- 每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点。 主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力。 主进程的主要目的是使用 BrowserWindow 模块创建和管理应用程序窗口。
- 窗口管理
- BrowserWindow 类的每个实例创建一个应用程序窗口,且在单独的渲染器进程中加载一个网页。 您可从主进程用 window 的 webContent 对象与网页内容进行交互。
-
渲染器进程
- 每个 Electron 应用都会为每个打开的 BrowserWindow ( 与每个网页嵌入 ) 生成一个单独的渲染器进程。 洽如其名,渲染器负责 渲染 网页内容。 所以实际上,运行于渲染器进程中的代码是须遵照网页标准的 (至少就目前使用的 Chromium 而言是如此) 。一个浏览器窗口中的所有的用户界面和应用功能,都应与您在网页开发上使用相同的工具和规范来进行攥写。
- 渲染器无权直接访问 require 或其他 Node.js API。 为了在渲染器中直接包含 NPM 模块,您必须使用与在 web 开发时相同的打包工具 (例如 webpack 或 parcel)
-
Preload 预加载脚本
- 预加载(preload)脚本包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码 。 这些脚本虽运行于渲染器的环境中,却因能访问 Node.js API 而拥有了更多的权限。预加载脚本可以在 BrowserWindow 构造方法中的 webPreferences 选项里被附加到主进程
- 因为预加载脚本与浏览器共享同一个全局 Window 接口,并且可以访问 Node.js API,所以它通过在全局 window 中暴露任意 API 来增强渲染器,以便你的网页内容使用
- 虽然预加载脚本与其所附着的渲染器在共享着一个全局 window 对象,但您并不能从中直接附加任何变动到 window 之上,因为 contextIsolation 是默认的
const {BrowserWindow} = require('electron') // ... const win = new BrowserWindow({ webPreferences: { preload: 'path/to/preload.js', }, }) // ...
- 语境隔离(Context Isolation)意味着预加载脚本与渲染器的主要运行环境是隔离开来的,以避免泄漏任何具特权的 API 到您的网页内容代码中,我们將使用 contextBridge 模块来安全地实现交互
// preload.js const {contextBridge} = require('electron') contextBridge.exposeInMainWorld('myAPI', { desktop: true, }) // renderer.js console.log(window.myAPI) // => { desktop: true }
-
效率进程
- 每个 Electron 应用程序都可以使用主进程生成多个子进程 UtilityProcess API。 主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力。 效率进程可用于托管,例如:不受信任的服务, CPU 密集型任务或以前容易崩溃的组件 托管在主进程或使用 Node.jschild_process.fork API 生成的进程中。 效率进程和 Node 生成的进程之间的主要区别.js child_process 模块是实用程序进程可以建立通信 通道与使用 MessagePort 的渲染器进程。 当需要从主进程派生一个子进程时,Electron 应用程序可以总是优先使用 效率进程 API 而不是 Node.js child_process.fork API
-
-
上下文隔离
- 上下文隔离功能将确保预加载脚本 和 Electron 的内部逻辑 运行在所加载的 webcontent 网页 之外的另一个独立的上下文环境里。 这对安全性很重要,因为它有助于阻止网站访问 Electron 的内部组件 和 您的预加载脚本可访问的高等级权限的 API。自 Electron 12 以来,默认情况下已启用上下文隔离
- 禁用上下文隔离
// preload.js 上下文隔离禁用的情况下使用预加载 window.myAPI = { doAThing: () => {}, } // doAThing() 函数可以在渲染进程中直接使用。 // renderer.js在渲染器进程使用导出的 API window.myAPI.doAThing()
- 启用上下文隔离
// preload.js在上下文隔离启用的情况下使用预加载 const {contextBridge} = require('electron') contextBridge.exposeInMainWorld('myAPI', { doAThing: () => {}, }) // renderer.js在渲染器进程使用导出的 API window.myAPI.doAThing()
-
进程间通信(见上文)
-
Electron 中的消息端口
- MessagePort 是一个异步的通信通道,允许在两个上下文之间传递消息。 这意味着,当两个上下文之间发生通信时,它们不会阻塞。 这对于在主进程和渲染器进程之间传递消息非常有用
// main/ipc.js const {ipcMain} = require('electron') // 1、主进程监听一个事件,渲染进程想要发送 port 的话,就能在这里获取到 ipcMain.on('render-post-message-to-main', (event, params) => { console.log('[Main receive]render-post-message-to-main', params) // 2、获取到 port1 const port1 = event.ports[0] // 3、需要调用一下 port1 的 start() port1.start() // 4、port1 绑定事件监听,之后渲染进程一发送的消息都会在这里接收到 port1.on('message', (event) => { const data = event.data console.log('[Main receive]message', data) port1.postMessage('我是主进程通过 port 回复的消息') }) }) // 主进程main/index.js const ipc = require('./ipc') // ...其他代码 // 先用 ipcMain 保证能接收到渲染进程发送过来的 port ,再调用 port1.start() ,然后给它绑定 message 事件,之后渲染进程一发送过来消息都能接收到,也能通过 port1 给渲染进程一发
-
参考
- https://juejin.cn/post/7197246543207432229
- https://juejin.cn/post/7067342993157537822
- https://juejin.cn/post/7078476722223448095
- https://juejin.cn/post/7103689764917755940