本文部分内容借鉴了这篇文章入门Electron,手把手教你编写完整实用案例,并会对其中的一些代码进行的重点纠正(可能前端项目迭代较快)。另外,这篇文章中也包括一些我对于Electron的额外见解,如有错误,请斧正。
Electron简介
Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在Windows上运行的跨平台应用 macOS和Linux——不需要本地开发经验。
Electron不同于一般Web应用,它继承了来自 Chromium 的多进程架构,这使得此框架在架构上非常相似于一个现代的网页浏览器。
这个特性更加切合我们使用客户端的需求。有时,除了主窗口外,还需要很多副窗口。比如使用QQ,我们可以打开多个人的聊天窗口。
快速开始
安装Electron包,加入生产环境
// npm
npm install --save-dev electron
// yarn
yarn add --dev electron
配置package.json,通过npm start
或yarn start
运行项目。
// package.json
{
"scripts": {
"start": "electron ."
}
}
进程模型
初次接触Electron,首先需要理解的就是它的进程模型。
Electron将进程主要分为两类:主进程和渲染进程。主进程相当于一个管理员,可以创建与管理各种渲染进程,而渲染进程相当于浏览器中的各个标签页。
进程创建
主进程
主进程在index.js
中默认创建,并且可通过下面监听创建完毕和关闭事件。
// index.js
const { app, BrowserWindow } = require('electron')
app.whenReady().then(() => {
createWindow()
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
渲染进程
创建渲染进程即创建一个窗口,一般一个客户端应该包含一个主渲染进程,我们给其起名为mainWindow
。
// index.js
// create a window
function createMainWindow() {
let mainWindow = new BrowserWindow({
frame: false, // no border
// resizable: false, // no resize
width: 800,
height: 600,
icon: iconPath,
webPreferences: {
backgroundThrottling: false, // run in back
nodeIntegration: true, // use nodeApi
contextIsolation: false, // relate to preload
// preload: preLoadPath
}
});
// load the html
mainWindow.loadFile(indexPath);
mainWindow.removeMenu();
}
该函数只需在app.whenReady
中调用即可创建一个主窗口。
在创建BrowserWindow
中可以定义窗口具备的多个属性,如frame
定义是否具备边框;resizable
定义是否可以拉伸窗口;backgroundThrottling
定义是否能够在后台运行。其中值得着重说明的属性是contextIsolation
定义上下文隔离,我将会在下面提到这个问题。
注意,根据一些其它的入门教程,在这里使用
mainWindow.loalUrl(file://绝对路径)
来加载本地页面文件,以现在的版本来说这种加载方法是失效的,除非额外配置对file协议的处理。而现在的版本中只需要使用loadFile
即可加载本地文件。
进程通信
进程间通信英文Inter-Process Communication,在主进程中通过ipcMain
来调用与其相关的函数。
以下将展示下面三种通信类型。
graph TB
id1(主进程)-->id2(副渲染进程)
id1 --> id3(主渲染进程)
id2 --> id3
渲染进程 to 主进程
ipcMain
是仅存在于主进程中的一个对象,只能在主进程中调用,其上可以挂载多个监听器,面向所有渲染进程来监听事件。正是因此,一个好的习惯是在事件名称的命名为“渲染进程名+事件名”。如以下的mainWindow:close
表示监听主渲染进程的关闭事件。
主进程通过ipcMain.on
来监听事件,渲染进程通过ipcRenderer.send('eventName')
来触发事件。
// index.js
ipcMain.on("mainWindow:close", () => {
mainWindow.hide();
})
// render.js
ipcRenderer.send('mainWindow:close');
主进程监听的事件同时可以获取渲染进程传递的参数,如下(event,task)=>{}
注意,第一个参数一定是event,第二个参数之后才是真正传播的数据。
ipcMain.on("mainWindow:setTaskTimer", (event, task) => {
// ...
})
与一般的事件监听不同,electron的事件监听具备同步或异步获取函数返回值的特点。
其中通过event.returnValue
来处理同步通信,通过event.reply('eventName')
来处理异步通信。但是同步通信会阻塞主进程,官方文档中更建议使用异步。
以下为截取的一部分官网给出的例子。
// index.js
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg) // prints "ping"
event.reply('asynchronous-reply', 'pong')
})
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // prints "ping"
event.returnValue = 'pong'
})
// renderer.js
console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // prints "pong"
ipcRenderer.on('asynchronous-reply', (event, arg) => {
console.log(arg) // prints "pong"
})
ipcRenderer.send('asynchronous-message', 'ping')
主进程 to 渲染进程
主进程主动给渲染进程发送消息是通过渲染进程的webContents
来完成。这里的理解要准确,所说的渲染进程指的是在index.js
中创建的渲染进程对象,并不指的是渲染进程的页面中的对象。
此处需要webContents.on("did-finish-load")
极有必要,因为只有在渲染进程的页面加载完毕后,webContents.send
才能够触发其事件(已测试过send
执行时间早于渲染页面加载JS脚本的时间)
viceWindow.webContents.on("did-finish-load", () => {
viceWindow.webContents.send("event", task);
})
注意,
webContents
中的监听事件无法像ipcMain
一样自定义,对于渲染进程触发的事件,官方文档中给出了明确的列表。“did-finish-load”事件在渲染进程导航加载完成时触发。
渲染进程 to 渲染进程
渲染进程与渲染进程间的通信是通过ipcRenderer.sendTo
来完成。
下面例子中的windowId
代表目标渲染进程的ID,根据个人测试Electron的进程ID会从1开始按顺序递增,主渲染进程为第一个创建的进程,一般其ID为1。但是,如果想确定获取某个进程的ID,只能够在主进程中通过mainWindow.webContents.id
获取,并发送给该渲染进程。
// ipcRenderer.sendTo(windowId, "finishEvent", params);
ipcRenderer.sendTo(1, "finishEvent", params);
这里需要说明的是,如果存在多个窗口,需要处理好各个窗口关系,并存储各个窗口的ID,方便数据传输。
预加载
如果到这里,你还记得前面提到的contextIsolation
,那我将很感谢你在认真阅读。
逻辑结构
如果使用Electron不愿意复杂化,那么按照我们的文件目录大概如下:
- project
- index.js
- src
- index.html
- render.js
- style.js
如果按照我上面所说进行配置,大概不会碰到什么棘手的问题。
但是按照官方给出的quick-start的例子,其文件目录中还包含一个preload.js
脚本,文件目录成为下面这样,这就是预加载,这是因为官放例子中contextIsolation
属性设置为true
(默认为true
)
- project
- index.js
- preload.js
- src
- index.html
- render.js
- style.js
上下文隔离
于是,引出Electron的又一个关键的概念,上下文隔离。
在一般的我们的前端项目中,渲染html页面的js中是运行在浏览器环境中的,而在Electron中,我们会发现Node中模块在其中也可以使用。(同时需要配置nodeIntegration
为true
)
这样的话,在一个复杂项目中,就可以造成污染,重名等问题。于是Electron特意增加了上下文隔离这一概念。
开启上下文隔离的条件是contextIsolation
属性设置为true
,且增加preload
的路径。
function createMainWindow() {
// create a window
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
// nodeIntegration: true,
contextIsolation: false, // make preload success
preload: './preload.js',
}
});
// load the html
mainWindow.loadFile(indexPath);
// mainWindow.removeMenu();
}
你会发现,一旦开启该条件,渲染页面的js中无法引入Electron和Node的各种模块。因此,如果想在其中使用,需要配置preload.js,使用contextBridge
(上下文桥,这个名字不错)来暴露全局接口到渲染页面的脚本中,在渲染脚本中直接通过window.darkMode
来调用preload.js
中的toggle
和system
函数。
以下例子取自官方网站。
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('darkMode', {
toggle: () => ipcRenderer.invoke('dark-mode:toggle'),
system: () => ipcRenderer.invoke('dark-mode:system')
})
// renderer.js
document.getElementById('toggle-dark-mode').addEventListener('click', async () => {
const isDarkMode = await window.darkMode.toggle()
document.getElementById('theme-source').innerHTML = isDarkMode ? 'Dark' : 'Light'
})
document.getElementById('reset-to-system').addEventListener('click', async () => {
await window.darkMode.system()
document.getElementById('theme-source').innerHTML = 'System'
})
于是,electron主进程和渲染进程的处理相互隔离,之间通过一条桥梁连接起来,各干各的事,有需要咱通过“桥”联系。
存储
这里再提一下关于Electron的数据存储。
localStorage
这是我们在前端项目中存储数据的惯用手段,但是该方法在electron中并不是最好的。网上关于其缺点列举的很清楚,在此稍微列举几点:非原子性;容错性低;安全性差等。
electron-store
这是在ELectron中常用的一个存储模块,需要进行安装。
npm install electron-store
使用的话主要是三个函数:
const Store = require("electron-store");
const store = new Store();
store.set('key', 'value');
let value = store.get('key');
let isExist = store.has('key');
该模块配合上下文隔离使用最佳,不容易出现错误。
如果不使用上下文隔离,需要在index.js
中通过下面语句引入该项目。
const Store = require("electron-store");
const store = new Store();
我遇到的报错是它需要index.js
引入下面样子的监听事件,而监听事件已经在store中进行封装,因此只需要引入上面两行即可。
// 以下代码大概封装在electron-store中,需要在index.js中生效
ipcMain.on('electron-store-get', async (event, val) => {
if (store.get(val))
event.returnValue = store.get(val);
else
event.returnValue = [];
});
ipcMain.on('electron-store-set', async (event, property, val) => {
// console.log(val);
store.set(property, val);
});
最后
关于Electron,我之前已有耳闻,出于兴趣,根据开头那篇文章学习了一下它的用法,在这个过程中碰到了很多问题,大费周折才解决,但是因此有了更深入的见解,故作此文来写个人看法,希望也能对大家有帮助。
根据开头推荐的那篇文章,我同样做了一个todoList的简单应用——Eask(Easy Task),项目及打包内容均上传到我的Github:MrPluto0/Easy-task中,可自行查看源码与下载。