Electron简介
Electron是干什么的? 简单来讲,Electron 使用 JavaScript,HTML 和 CSS,来构建跨平台的桌面应用程序。
按照官方的说法:如果你可以建一个网站,你就可以建一个桌面应用程序。
和传统的桌面应用相比,使用Electron开发更容易上手,开发效率更高。并且,web技术应用广泛、生态繁荣,Electron可以使用几乎所有的Web生态领域及Node.js生态领域的组件和技术方案。
与网页应用相比,Electron基于Chromium 和 Node.js,可以避免令人头痛的浏览器兼容问题。而Web前端受限访问的文件系统、系统托盘、系统通知等,开发Electron应用时可以自由地使用。
简单理解Electron工作机制
使用Electron开发的桌面应用,类似于简易版的、定制版的Chrome浏览器,当然这个浏览器中的页面不能通过输入网址打开,而是由开发者写好的。
和浏览器架构类似,Electron应用程序区分主进程和渲染进程。
主进程负责控制应用程序的生命周期、创建和管理应用程序窗口,有着多种控制原生桌面功能的模块,例如菜单、对话框以及托盘图标。
渲染进程负责完成渲染界面、接收用户输入、响应用户的交互等工作。
一个Electron应用只有一个主进程,但可以有多个渲染进程。
在之前的文章中,我们有讲过浏览器中的进程,可略作参考。
案例入门
下面我们将从一个任务管理的案例入门,了解electron的整体开发流程和一些基本的细节知识。
案例效果
功能分析:
1、记录待完成任务和已完成任务
2、通过新建,添加待完成任务,并设置时间
3、点击完成任务,跳转到已完成界面;点击删除,可以删除任务
4、点击右上角的 ×
按钮,可以关闭主界面,要再次打开主界面,可以通过系统托盘
5、设定的时间到了,会在右下角弹出提醒框,如下图所示。
初始化项目
项目是由原生js开发,在后面的文章中,我们会再探讨electron和vue、react这些前端框架的结合。
mkdir tasky
cd tasky
npm init
安装electron
npm install electron --S
创建一个hello world应用程序
(1)第一步,在项目根目录下,创建index.js
,作为应用程序的入口文件。因为Electron是基于Node.js,所以入口文件使用Node.js语法。内容如下:
//引入两个模块:app 和 BrowserWindow
//app 模块,控制整个应用程序的事件生命周期。
//BrowserWindow 模块,它创建和管理程序的窗口。
const { app, BrowserWindow } = require('electron')
//在 Electron 中,只有在 app 模块的 ready 事件被激发后才能创建浏览器窗口
app.on('ready', () => {
//创建一个窗口
const mainWindow = new BrowserWindow()
//窗口加载html文件
mainWindow.loadFile('./src/main.html')
})
(2)第二步,创建窗口需要加载的html文件。
为了方便后面的文件管理,我们新建一个 src
文件夹,用于存放web页面资源,比如html、css、js、图片等。
// ./src/main.html
Document
hello world
(3)启动程序。
修改package.json
的scripts
,如下:
然后在项目根目录运行:npm run start
。
这样我们的hello world应用程序就跑起来了。入门Electron,就是这么简单!
简单的基础调试
1、主进程运行时的一些提示信息会在命令行显示,比如,在index.js
加入console.log
app.on('ready', () => {
console.log('just test console.log')
const mainWindow = new BrowserWindow()
mainWindow.loadFile('./src/main.html')
})
就可以在命令行看到打印的值:
当index.js
中的内容发生改变,默认要手动重启,比较麻烦。这里我们加入nodemon
,它可以监控node.js
源代码的变化,并自动重启应用。
安装: npm i nodemon --D
修改scripts
:"start": "nodemon --watch index.js --exec electron ."
再次运行npm run start
,当index.js
内容变化时,就会自动重新执行electron .
来重启应用。
2、窗口页面的调试方法和chrome浏览器类似。点击菜单栏的View --- Toggle Developer Tools
,或者按它对应的快捷键,就会出现我们熟悉的开发者工具界面。
当页面内容发生变化,可以点击View --- Reload
,或者快捷键ctrl+r
,刷新页面内容。页面热更新会后续讲到。
开始coding
项目目录结构如下,其中src
文件夹存放的就是web页面相关的内容。
项目有两个窗口:主窗口和提醒窗口。在主窗口中管理任务,当任务设定时间到,会在屏幕右下角出现提醒窗口。
应用不涉及服务端数据,任务数据主要使用localStorage
来存储。
创建主窗口:
const iconPath = path.join(__dirname, './src/img/icon.png') //应用运行时的标题栏图标
let mainWindow
app.on('ready', () => {
mainWindow = new BrowserWindow({
resizable: false, //不允许用户改变窗口大小
width: 800, //设置窗口宽高
height: 600,
icon: iconPath, //应用运行时的标题栏图标
webPreferences:{
backgroundThrottling: false, //设置应用在后台正常运行
nodeIntegration:true, //设置能在页面使用nodejs的API
contextIsolation: false,
preload: path.join(__dirname, './preload.js')
}
})
mainWindow.loadURL(`file://${__dirname}/src/main.html`)
}
main.html
的内容很简单,有兴趣的童鞋可以去看源码,这里就不贴了。
默认Electron应用顶部有标题栏和菜单栏。纵观各个桌面应用程序,基本都是定制的顶部控制区域。对于我们这个应用,暂时只要一个关闭按钮,所以我们将去掉这一部分,将窗口关闭按钮写在main.html
页面中。
无边框窗口
要创建无边框窗口,只需在 BrowserWindow 的 options 中将 frame 设置为 false:
mainWindow = new BrowserWindow({
frame: false,
...
})
标题栏和菜单栏消失了,但也会有几个问题:
1、虽然菜单栏消失了,但是依然可以通过快捷键进行菜单操作,比如ctrl+shift+i
打开开发者工具,为避免这种情况,我们需要去掉菜单栏:
mainWindow.removeMenu()
2、默认情况下,无边框窗口是不可拖拽的。应用程序需要在 CSS 中指定 -webkit-app-region: drag 来告诉 Electron 哪些区域是可拖拽的。
html,body {
height: 100%;
width: 100%;
}
body{
-webkit-app-region: drag;
}
如果用上面的属性使整个窗口都可拖拽,则必须将其中的按钮标记为不可拖拽,否则按钮将无法点击。
.enable-click {
-webkit-app-region: no-drag;
}
3、当点击自定义的窗口关闭按钮,我们并不希望退出程序,只是将窗口隐藏,可以通过系统托盘再次打开窗口。
系统托盘
程序启动时,将应用程序加入系统托盘。在Electron中,借助Tray模块实现。
const { app, BrowserWindow, Tray, Menu } = electron
const iconPath = path.join(__dirname, './src/img/icon.png')
let mainWindow, tray
app.on('ready', () => {
mainWindow = new BrowserWindow({
//... options
})
mainWindow.loadURL(`file://${__dirname}/src/main.html`)
tray = new Tray(iconPath) //实例化一个tray对象,构造函数的唯一参数是需要在托盘中显示的图标url
tray.setToolTip('Tasky') //鼠标移到托盘中应用程序的图标上时,显示的文本
tray.on('click', () => { //点击图标的响应事件,这里是切换主窗口的显示和隐藏
if(mainWindow.isVisible()){
mainWindow.hide()
}else{
mainWindow.show()
}
})
tray.on('right-click', () => { //右键点击图标时,出现的菜单,通过Menu.buildFromTemplate定制,这里只包含退出程序的选项。
const menuConfig = Menu.buildFromTemplate([
{
label: 'Quit',
click: () => app.quit()
}
])
tray.popUpContextMenu(menuConfig)
})
})
IPC通信
回到上一个问题。点击页面内的按钮怎样隐藏窗口?这就需要用到IPC通信了。
IPC(Inter-Process Communication),就是进程间通信。Electron应用程序区分主进程和渲染进程,有时候,两者之间需要通信,传输一些数据、发送一些消息。
渲染进程 TO 主进程
比如,点击关闭按钮,就需要渲染进程向主进程发送隐藏主窗口的请求。
渲染进程使用Electron内置的ipcRenderer模块向主进程发送消息,ipcRenderer.send
方法的第一个参数是消息管道名称。
//页面的js代码:
const electron = require('electron')
const { ipcRenderer } = electron
closeDom.addEventListener('click', () => {
ipcRenderer.send('mainWindow:close')
})
主进程通过ipcMain接收消息,ipcMain.on
方法的第一个参数也为消息管道的名称,与ipcRenderer.send
的名称对应,第二个参数是接收到消息的回调函数。
//入口文件index.js
ipcMain.on('mainWindow:close', () => {
mainWindow.hide()
})
主进程 TO 渲染进程
主进程向渲染进程发送消息是通过渲染进程的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 =
`${decodeURIComponent(task)}的时间到啦!`
})
渲染进程 TO 渲染进程
渲染进程之间传递消息,可以通过主进程中转,即窗口A先把消息发送给主进程,主进程再把这个消息发送给窗口B,这种非常常见。
也可以从窗口A直接发消息给窗口B,前提是窗口A知道窗口B的webContents的id。
ipcRenderer.sendTo(webContentsId, channel, ...args)
值得注意的是,我们在页面的js代码中使用了require
,这也是Electron的一大特点,在渲染进程中可以访问Node.js API。这样做的前提是在创建窗口时配置webPreferences
的nodeIntegration: true
和contextIsolation: false
:
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences:{
nodeIntegration: true,
contextIsolation: false
}
})
窗口位置
当任务时间到,提醒窗口会在屏幕右下角出现。怎样设定窗口位置呢?
function createRemindWindow (task) {
//创建提醒窗口
remindWindow = new BrowserWindow({
//...options
})
//获取屏幕尺寸
const size = screen.getPrimaryDisplay().workAreaSize
//获取托盘位置的y坐标(windows在右下角,Mac在右上角)
const { y } = tray.getBounds()
//获取窗口的宽高
const { height, width } = remindWindow.getBounds()
//计算窗口的y坐标
const yPosition = process.platform === 'darwin' ? y : y - height
//setBounds设置窗口的位置
remindWindow.setBounds({
x: size.width - width, //x坐标为屏幕宽度 - 窗口宽度
y: yPosition,
height,
width
})
//当有多个应用时,提醒窗口始终处于最上层
remindWindow.setAlwaysOnTop(true)
remindWindow.loadURL(`file://${__dirname}/src/remind.html`)
}
关闭窗口
提醒窗口会在一段时间后关闭,可以通过remindWindow.close()
来关闭窗口。
当窗口关闭后,我们可以设置remindWindow = null
来回收分配给该渲染进程的资源。
remindWindow.on('closed', () => { remindWindow = null })
结语
这篇文章主要是介绍electron的一些基础知识,下一篇文章,我们将探讨electron的打包问题,下次见。
凡能用JavaScript实现的,注定会被用JavaScript实现。
---Jeff Atwood