2

Electron简介

Electron是干什么的? 简单来讲,Electron 使用 JavaScript,HTML 和 CSS,来构建跨平台的桌面应用程序。

按照官方的说法:如果你可以建一个网站,你就可以建一个桌面应用程序。

和传统的桌面应用相比,使用Electron开发更容易上手,开发效率更高。并且,web技术应用广泛、生态繁荣,Electron可以使用几乎所有的Web生态领域及Node.js生态领域的组件和技术方案。

与网页应用相比,Electron基于Chromium 和 Node.js,可以避免令人头痛的浏览器兼容问题。而Web前端受限访问的文件系统、系统托盘、系统通知等,开发Electron应用时可以自由地使用。

简单理解Electron工作机制

使用Electron开发的桌面应用,类似于简易版的、定制版的Chrome浏览器,当然这个浏览器中的页面不能通过输入网址打开,而是由开发者写好的。

image.png
和浏览器架构类似,Electron应用程序区分主进程和渲染进程。

主进程负责控制应用程序的生命周期、创建和管理应用程序窗口,有着多种控制原生桌面功能的模块,例如菜单、对话框以及托盘图标。

渲染进程负责完成渲染界面、接收用户输入、响应用户的交互等工作。

一个Electron应用只有一个主进程,但可以有多个渲染进程。

在之前的文章中,我们有讲过浏览器中的进程,可略作参考。

案例入门

下面我们将从一个任务管理的案例入门,了解electron的整体开发流程和一些基本的细节知识。

案例效果

mainWindow.gif

功能分析:

1、记录待完成任务和已完成任务

2、通过新建,添加待完成任务,并设置时间

3、点击完成任务,跳转到已完成界面;点击删除,可以删除任务

4、点击右上角的 × 按钮,可以关闭主界面,要再次打开主界面,可以通过系统托盘

5、设定的时间到了,会在右下角弹出提醒框,如下图所示。

image.png

初始化项目

项目是由原生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

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  hello world
</body>
</html>

(3)启动程序。

修改package.jsonscripts,如下:

image.png

然后在项目根目录运行:npm run start

这样我们的hello world应用程序就跑起来了。入门Electron,就是这么简单!

image.png

简单的基础调试

1、主进程运行时的一些提示信息会在命令行显示,比如,在index.js加入console.log

app.on('ready', () => {
  console.log('just test console.log')
  const mainWindow = new BrowserWindow()
  mainWindow.loadFile('./src/main.html')
})

就可以在命令行看到打印的值:

image.png

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,或者按它对应的快捷键,就会出现我们熟悉的开发者工具界面。

image.png

当页面内容发生变化,可以点击View --- Reload,或者快捷键ctrl+r,刷新页面内容。页面热更新会后续讲到。

开始coding

项目目录结构如下,其中src文件夹存放的就是web页面相关的内容。

image.png
项目有两个窗口:主窗口和提醒窗口。在主窗口中管理任务,当任务设定时间到,会在屏幕右下角出现提醒窗口。

应用不涉及服务端数据,任务数据主要使用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的内容很简单,有兴趣的童鞋可以去看源码,这里就不贴了。

image.png

默认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、当点击自定义的窗口关闭按钮,我们并不希望退出程序,只是将窗口隐藏,可以通过系统托盘再次打开窗口。
image.png

系统托盘

程序启动时,将应用程序加入系统托盘。在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通信了。

image.png
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 = 
      `<span>${decodeURIComponent(task)}</span>的时间到啦!`
})

image.png

渲染进程 TO 渲染进程

渲染进程之间传递消息,可以通过主进程中转,即窗口A先把消息发送给主进程,主进程再把这个消息发送给窗口B,这种非常常见。

也可以从窗口A直接发消息给窗口B,前提是窗口A知道窗口B的webContents的id。

ipcRenderer.sendTo(webContentsId, channel, ...args)

值得注意的是,我们在页面的js代码中使用了require,这也是Electron的一大特点,在渲染进程中可以访问Node.js API。这样做的前提是在创建窗口时配置webPreferencesnodeIntegration: truecontextIsolation: 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

系列文章

Electron应用的打包和自动更新

Electron + Vue3 开发跨平台桌面应用【从项目搭建到打包完整过程】


Alaso
44 声望7 粉丝