认识目录

使用vue-cli创建的electron工程文件目录大体结构[1]如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
my-project
├─ .electron-vue
│ └─ <build/development>.js files
├─ build
│ └─ icons/
├─ dist
│ ├─ electron/
│ └─ web/
├─ node_modules/
├─ src
│ ├─ main # 主进程
│ │ ├─ index.dev.js # 开发模式
│ │ └─ index.js # 主进程
│ ├─ renderer # 渲染进程
│ │ ├─ components/
│ │ ├─ router/
│ │ ├─ store/
│ │ ├─ App.vue
│ │ └─ main.js
│ └─ index.ejs
├─ static/ # 资源文件
├─ test
│ ├─ e2e
│ │ ├─ specs/
│ │ ├─ index.js
│ │ └─ utils.js
│ ├─ unit
│ │ ├─ specs/
│ │ ├─ index.js
│ │ └─ karma.config.js
│ └─ .eslintrc
├─ .babelrc
├─ .eslintignore
├─ .eslintrc.js
├─ .gitignore
├─ package.json # 配置文件
└─ README.md

electron有多种文件结构,例如将所有应用的代码都放在app目录中,资源文件位于static目录

我们重点关注的是主进程、渲染进程和配置文件,其中开发主要集中在渲染进程中

package.json[^3]

许多Node项目中都会有package.json清单文件,它是electron项目的依赖文件,核心参数之一是应用的主入口。

对于纯electron应用:

1
2
3
4
5
6
{
"main":"./app/main.js",
"scripts":{
"start":"electron ."
}
}

dist目录全称是distribution。在某些框架中,因为开发和发布是的内容或者代码形式是不一样的(比如利用Grunt压缩等等),这时候就需要一个存放最终发布版本的代码,这就是dist文件夹的用处。[2]


上面的代码根据3中对electron应用的讲解,electron-vue框架搭建出来,这里显示的有些不一样,可能涉及到发布/开发模式的问题?不知道以后编译啊之类的会不会就有区别;看PicGo的项目这里差别也很大

主进程 src/main

3的纯electron应用中,它将主进程写入main.js文件中,作为项目的入口;而在electron-vue框架中,主进程在src/main/index.js中。代码的整体结构是相似的。

在main进程中主要需要掌握以下知识

app模块

app可以处理应用的生命周期与配置,是electron应用的骨架。它掌管着整个应用的生命周期钩子,以及很多其他事件钩子。

引用模块

1
2
3
4
// method 1
const {app} = require('electron')
// method 2 (electron-vue框架内置)
import { app } from 'electron'
  • app的常用生命周期钩子如下:

    • will-finish-launching 在应用完成基本启动进程之后触发

    • ready 当electron完成初始化后触发(必须

      1
      2
      3
      4
      5
      6
      7
      8
      function createWindow () { // 创建窗口
      /**
      * Initial window options
      */
      // ...
      }

      app.on('ready', createWindow) // 调用createWindow创建窗口
    • window-all-closed 所有窗口都关闭的时候触发,在windows和linux里,所有窗口都退出的时候通常是应用退出的时候

      1
      2
      3
      4
      5
      app.on('window-all-closed', () => {
      if (process.platform !== 'darwin') { // 当操作系统不是darwin(macOS)的话
      app.quit() // 退出应用
      }
      })
    • before-quit 退出应用之前的时候触发

    • will-quit 即将退出应用的时候触发

    • quit 应用退出的时候触发(用法见window-all-closed)

  • 事件钩子

    • active(仅macOS)当应用处于激活状态时

      1
      2
      3
      4
      5
      app.on('activate', () => {
      if (mainWindow === null) {
      createWindow()
      }
      })
    • browser-window-created 当一个BrowserWindow被创建的时候

    • browser-window-focus 当一个BrowserWindow处于激活状态的时候

    这些钩子需要配合一些具体场景来做出具体的操作。比如当一个BrowserWindow处于激活状态的时候修改窗口的title值。

  • 其他常用的方法:

    • app.quit() 用于退出应用

    • app.getPath(name) 用于获取一些系统目录,对于存放应用的配置文件等很有用

    • app.focus() 用于激活应用,不同系统激活逻辑不一样

而我们通常会在ready的时候执行创建应用窗口、创建应用菜单、创建应用快捷键等初始化操作。而在will-quit或者quit的时候执行一些清空操作,比如解绑应用快捷键。

BrowserWindow渲染器进程

主进程可以使用BrowserWindow模块创建多个独立的、互相隔离的渲染器进程(即应用窗口)。

1
import { app, BrowserWindow } from 'electron'

创建BrowserWindow实例:

1
2
3
let mainWindow = null;
// ...
mainWindow = new BrowserWindow();

常见配置和方法

可以通过添加参数来改变配置

一个BrowserWindow的常用配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let window = null;

function createWindow () {
window = new BrowserWindow({
height: 900, // 高
width: 400, // 宽
show: false, // 创建后是否显示
frame: false, // 是否创建frameless窗口
fullscreenable: false, // 是否允许全屏
center: true, // 是否出现在屏幕居中的位置
backgroundColor: '#fff' // 背景色,用于transparent和frameless窗口
titleBarStyle: 'xxx' // 标题栏的样式,有hidden、hiddenInset、customButtonsOnHover等
resizable: false, // 是否允许拉伸大小
transparent: true, // 是否是透明窗口(仅macOS)
vibrancy: 'ultra-dark', // 窗口模糊的样式(仅macOS)
webPreferences: {
backgroundThrottling: false // 当页面被置于非激活窗口的时候是否停止动画和计时器
}
// ... 以及其他可选配置
})

window.loadURL(url)

window.on('closed', () => { window = null })
}

PicGo主窗口写法参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const createSettingWindow = () => {
const options = {
height: 450,
width: 800,
show: false,
frame: true,
center: true,
fullscreenable: false,
resizable: false,
title: 'PicGo',
vibrancy: 'ultra-dark',
transparent: true,
titleBarStyle: 'hidden',
webPreferences: {
backgroundThrottling: false
}
}
if (process.platform === 'win32') { // 针对windows平台做出不同的配置
options.show = true // 创建即展示
options.frame = false // 创建一个frameless窗口
options.backgroundColor = '#3f3c37' // 背景色
}
settingWindow = new BrowserWindow(options)

settingWindow.loadURL(settingWinURL)

settingWindow.on('closed', () => {
settingWindow = null
})
}

在PicGo项目中,window的配置被保存在main/apis/app/window/windowList.ts中,然后export出去,在windowManager里对窗口进行统一管理

app模块一样,BrowserWindow也有很多常用的事件钩子:

  • closed 当窗口被关闭的时候
  • focus 当窗口被激活的时候
  • show 当窗口展示的时候
  • hide 当窗口被隐藏的时候
  • maxmize 当窗口最大化时
  • minimize 当窗口最小化时
  • ...

当然,也依然有很多实用的方法:

  • BrowserWindow.getFocusedWindow() [静态方法]获取激活的窗口
  • win.close() [实例方法,下同]关闭窗口
  • win.focus() 激活窗口
  • win.show() 显示窗口
  • win.hide() 隐藏窗口
  • win.maximize() 最大化窗口
  • win.minimize() 最小化窗口
  • win.restore() 从最小化窗口恢复
  • ...

比如上述说到的,windows的顶部的操作区(放大、缩小、关闭按钮)就可以通过icon模拟+实例方法来实现

将html文档加载到主窗口

方法一:

1
mainWindow.webContents.loadFile('index.html');

方法二:(electron-vue框架自带)

1
2
3
4
5
6
const winURL = process.env.NODE_ENV === 'development'
? `http://localhost:9080` // 开发模式的话走webpack-dev-server的url
: `file://${__dirname}/index.html`

//...
mainWindow.loadURL(winURL)

__dirname变量是Node全局可用的一个变量,它的值是当前正被执行的Node应用的完整路径。

win.webContents此窗口拥有的 WebContents 对象。 所有与网页相关的事件和操作都将通过它完成。

  • win.loadURL(url[, options])webContents.loadURL(url[, options\]) 相同。

  • win.loadFile(filePath[, options])webContents.loadFile相同, filePath 应该是一个与你的应用程序的根路径相关的HTML文件路径。

两者在用法上有一定差别,可以参考BrowserWindow | Electron (electronjs.org)

优雅地显示窗口

在加载页面时,渲染进程第一次完成绘制时,如果窗口还没有被显示,渲染进程会发出 ready-to-show 事件 。 在此事件后显示窗口将没有视觉闪烁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const { app, BrowserWindow } = require('electron');

let mainWindow = null;

app.on('ready', () => {
mainWindow = new BrowserWindow({ show: false }); // 设置为false

mainWindow.loadURL(`file://${__dirname}/index.html`);

mainWindow.once('ready-to-show', () => {
mainWindow.show();
});

mainWindow.on('closed', () => {
mainWindow = null;
});
});

Tray系统托盘

添加图标和上下文菜单到系统通知区。在windows里,Tray配合上图标之后就是windows右下角的应用图标

windows和macOS里,图标的大小都是16*16px。

官网代码(配合Menu使用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { app, Menu, Tray } = require('electron')

let tray = null
app.whenReady().then(() => {
tray = new Tray('/path/to/my/icon')
const contextMenu = Menu.buildFromTemplate([
{ label: 'Item1', type: 'radio' },
{ label: 'Item2', type: 'radio' },
{ label: 'Item3', type: 'radio', checked: true },
{ label: 'Item4', type: 'radio' }
])
tray.setToolTip('This is my application.')
tray.setContextMenu(contextMenu)
})
  • 在 Windows 上, 建议使用 ICO 图标来获得最佳视觉效果。

如果要在所有平台上保持完全相同的行为, 则不应依赖 click 事件, 并且始终将上下文菜单附加到任务栏图标。

macOS下顶部栏的图标通常都是走黑白路线,所以可以为两种系统分别准备不同的图标。PicGoTray的生成代码大致如下:

1
2
3
4
5
function createTray () {
const menubarPic = process.platform === 'darwin' ? `${__static}/menubar.png` : `${__static}/menubar-nodarwin.png`
tray = new Tray(menubarPic) // 指定图片的路径
// ... 其他代码
}

注意上述代码里有一个${__static}的变量。该变量是electron-vue为我们暴露出来的项目根目录下的static文件夹的路径。通过这个路径,在开发和生产阶段都能很好的定位你的静态资源所在的目录。是个很方便的变量。

Tray支持很多有用的事件。其中最关键的两个是clickright-click。分别对应鼠标左键点击和鼠标右键点击事件。

鼠标左键点击事件

  • 在macOS系统下,鼠标左键点击Tray的icon可能会出现配置菜单,也有可能会出现应用窗口。
  • 在windows下,鼠标左键点击Tray的icon通常会出现应用的窗口。

鼠标右键点击事件

  • 在macOS系统下,鼠标右键点击Tray的icon通常会出现配置菜单。
  • 在windows系统下,同上。

PicGo举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function createTray () {
const menubarPic = process.platform === 'darwin' ? `${__static}/menubar.png` : `${__static}/menubar-nodarwin.png`
tray = new Tray(menubarPic)
const contextMenu = // ...菜单
tray.on('right-click', () => { // 右键点击
window.hide() // 隐藏小窗口
tray.popUpContextMenu(contextMenu) // 打开菜单
})
tray.on('click', () => { // 左键点击
if (process.platform === 'darwin') { // 如果是macOS
toggleWindow() // 打开或关闭小窗口
} else { // 如果是windows
window.hide() // 隐藏小窗口
if (settingWindow === null) { // 如果主窗口不存在就创建一个
createSettingWindow()
settingWindow.show()
} else { // 如果主窗口在,就显示并激活
settingWindow.show()
settingWindow.focus()
}
}
})
}

new Menu()创建新菜单。

主要分两种。

  • 第一种是app的菜单。对于macOS来说就是顶部栏左侧区域的菜单项。对于windows而言就是一个窗口的标题栏下方的菜单区。

    可以通过Menu.setApplicationMenu()来实现。

  • 第二种是类似于右键菜单的菜单。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    const contextMenu = Menu.buildFromTemplate([
    {
    label: '关于',
    click () {
    dialog.showMessageBox({
    title: 'PicGo',
    message: 'PicGo',
    detail: `Version: ${pkg.version}\nAuthor: Molunerfinn\nGithub: https://github.com/Molunerfinn/PicGo`
    })
    }
    },
    {
    label: '打开详细窗口',
    click () {
    if (settingWindow === null) {
    createSettingWindow()
    settingWindow.show()
    } else {
    settingWindow.show()
    settingWindow.focus()
    }
    }
    },
    {
    label: '选择默认图床',
    type: 'submenu',
    submenu: [
    {
    label: '微博图床',
    type: 'radio',
    checked: db.read().get('picBed.current').value() === 'weibo',
    click () {
    db.read().set('picBed.current', 'weibo')
    .write()
    }
    },
    {
    label: '七牛图床',
    type: 'radio',
    checked: db.read().get('picBed.current').value() === 'qiniu',
    click () {
    db.read().set('picBed.current', 'qiniu')
    .write()
    }
    }
    ]
    },
    {
    role: 'quit',
    label: '退出'
    }
    ])

    tray.on('right-click', () => {
    tray.popUpContextMenu(contextMenu)
    })

组成Menu的是一个一个的MenuItem。它们有很多类型:

  1. normal
  2. separator
  3. submenu
  4. checkbox
  5. radio

以及很多角色:

  1. quit
  2. copy
  3. redo
  4. undo
  5. minimize
  6. close
  7. reload

通常来说,配置的菜单项基本从类型里来组合。

如果没有在创建app菜单里指定这些操作的快捷键的话,那么一些常见的快捷操作就无法在你的app里使用了。比如ctrl+c或者command+c复制这个操作,如果你没有通过Menu.setApplicationMenu()来设定这个快捷键的话,那么在你的electron应用里就无法执行复制的操作了。

注意,如果在开发模式下直接只使用如下快捷键的话,一些调试快捷键比如F12或者command+shift+i打开控制台的操作就无法使用了。所以在开发模式下不需要创建这些快捷键菜单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const createMenu = () => {
if (process.env.NODE_ENV !== 'development') {
const template = [{
label: 'Edit',
submenu: [
{ label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' },
{ label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' },
{ type: 'separator' },
{ label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' },
{ label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' },
{ label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' },
{ label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:' },
{
label: 'Quit',
accelerator: 'CmdOrCtrl+Q',
click () {
app.quit()
}
}
]
}]
menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}
}

可以通过accelerator指定你想要的快捷键。诸如ShiftCtrlCmd等键位缩写。如果是组合键,就加上+。尤其注意到,因为macOS和windows键位的差异,所以有一个很好用的键位缩写CmdOrCtrl,即如果是在macOS上就是Cmd,在windows上就是Ctrl

菜单项的点击事件可以直接通过click属性来指定。先通过了Menu.buildFromTemplate()这个方法创建了菜单,然后再在右键点击Tray图标的时候将其弹(PopUp)出来。

当然也有其他构建菜单的方法。可以通过Menu实例的append方法来加入Menu Item

总结

本文主要还是结合了PicGo项目对Electron主进程有了大致的了解

PicGo项目本身对功能的划分比较明确,它的入口是backgroung.js,通过它来调用main/lifeCycle/index.js,在其中来进行ready/quit等,并调用写入其他文件里的窗口创建/菜单创建等。


  1. Electron-vue开发实战0——Electron-vue入门 | MARKSZのBlog (molunerfinn.com) ↩︎

  2. 在一些开源框架中,dist文件夹是什么意思 - SegmentFault 思否 ↩︎