Electron实践

创建第一个项目

最近在做一个流程图编辑工具,已经使用mxGraph实现了线上版本,现在希望做一个本地版本,于是想到了Electron,看一下是不是可以封装一下,直接使用。由于没有用过Electron,所以还是老实点,从头开始,免得掉坑。

首先是创建环境,使用npm进行安装,按照官网得说明,很顺利就安装完成了,输入electron -v看一下版本,v8.0.2,说明安装没有问题。接下来按官网得说明创建一个简单得应用,就三个文件:package.json,index.html,main.js。package.json声明了应用的名称、版本号和主程序入口:

1
2
3
4
5
{
"name" : "my-graph-editor",
"version" : "0.1.0",
"main" : "main.js"
}

main.js创建了electron的主进程,并且调入index.html,剩下的事情就交给渲染进程了。对于大部分工作通过js完成的应用来说,main.js的代码几乎都是相同的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const {app, BrowserWindow} = require('electron')
//声明窗体变量
let graphWin;

//当app完成初始化时,执行窗体的创建。
app.on('ready', createWindow)

function createWindow(){
//构建一个高800,宽1200的窗体。
graphWin = new BrowserWindow({width: 1200, height: 800,
//设置支持node集成
webPreferences: {
nodeIntegration: true
}
})
//窗体中显示的内容是index.html文件中的内容,就是所谓的渲染进程,可以认为在内嵌的google浏览器中执行
//__dirname,表示main.js所在的目录路径
graphWin.loadURL(__dirname + "/index.html")
//当窗体已经关闭时,将win赋值为null。
graphWin.on('closed', () => {
graphWin= null
})
}

这时,在应用目录中执行

1
electrion .

第一个应用就执行了,由于index.html中没有内容,所以有一个空的窗口。试着把在线可以执行的js代码拷贝到Lib\mxGraph目录,并将js引入、初始化等代码拷贝到index.html,试着执行一下,居然成功了。当然,还有很多问题需要解决,文件保存和打开、菜单项的操作等等,但相比从头开始一个做一个桌面应用,已经简单很多了。

打开和保存本地文件

在线编辑流程图的工具通过Ajax访问服务器的文件服务实现打开和保存流程图,在本地应用时,我们需要使用node.js的文件服务来实现这个功能。我们编写一个简单的应用来测试打开和保存本地文件功能。

前面已经提到了,为了使用node.js的功能,需要在main.js初始化窗体时声明nodeIntegration: true:

1
2
3
4
5
6
graphWin = new BrowserWindow({width: 1200, height: 800,
//设置支持node集成
webPreferences: {
nodeIntegration: true
}
})

然后,我们需要声明使用node.js的文件服务:

1
var fs = require("fs");

接下来就可以调用文件服务读写本地的文件了:

1
2
3
4
5
6
7
8
9
fs.readFile(filename, 'utf8', function (err, data) {
console.log(data);
});

fs.writeFile(filename, content, function (err) {
if(err) alert(err);
else alert("保存成功");
});

使用打开文件对话框和保存文件对话框选择文件路径

上一节我们使用node.js的文件服务打开和保存文件,现在我们看如何使用electron的对话框来选择本地文件。可以在主进程操作electron的对话框,如果在渲染进程操作,需要使用remote访问主进程,代码是这样的:

1
const {dialog} = require('electron').remote;

然后,就可以启动对话框选择文件了,启动打开对话框的代码如下:

1
2
3
4
5
6
7
8
9
10
11
let options = {
title : "打开文件",
defaultPath : "",
buttonLabel : "打开",
filters :[
{name: '文本文件', extensions: ['txt']},
{name: 'All Files', extensions: ['*']}
],
properties: ['openFile']
}
let files=dialog.showOpenDialogSync( options);

这里使用的是同步打开,如果使用异步方法,可以使用showOpenDialog,后面加回调函数。还有需要注意的是返回的是数组,下面是打开文件的代码:

1
2
3
4
if(file&&file.length>0)
fs.readFile(file[0], 'utf8', function (err, data) {
console.log(data);
});

保存文件与打开文件类似,只是返回的是文件路径,不是数组了。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let file=dialog.showSaveDialogSync(
{
filters :[
{name: 'Text', extensions: ['txt']},
{name: 'All Files', extensions: ['*']}
],
properties: ['saveFile']
}
);
console.log(file);
if(file)
fs.writeFile(file, txt.value, function (err) {
if(err) alert(err);
else alert("保存成功");
});

菜单

我们现在希望将Electron应用的缺省菜单替换为我们自己的菜单,然后通过菜单实现打开文件和写入文件的功能。调用菜单也需要访问主进程:

1
2
const { remote } = require('electron')
const { Menu, MenuItem } = remote

然后可以定义新的菜单:

1
const menu = new Menu()

使用append添加新的菜单项目,菜单项中label定义了菜单的显示标签,role定义了菜单的缺省功能,比如role:’quit’说明菜单执行退出功能。在菜单项的submenu中定义子菜单。通过click事件自定义菜单功能。
Index.html的代码如下:

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="content"></div>

<input type="text" id="txtContent" />

</body>
<script>
var content = document.getElementById('content');
var txt=document.getElementById('txtContent');
var fs = require("fs");
const {dialog} = require('electron').remote;
const { remote } = require('electron')
const { Menu, MenuItem } = remote

const menu = new Menu()
menu.append(new MenuItem(
{ label: '文件', role:'filemenu',
submenu:[
{
label:'打开', click() {
let options = {
title : "打开文件",
defaultPath : "",
buttonLabel : "打开",
filters :[
{name: '文本文件', extensions: ['txt']},
{name: 'All Files', extensions: ['*']}
],
properties: ['openFile']
}
let files=dialog.showOpenDialogSync( options);
if(file&&file.length>0)
fs.readFile(file[0], 'utf8', function (err, data) {
content.innerText = data;
});
}
},
{
label:'保存', click() {
let file=dialog.showSaveDialogSync(
{
filters :[
{name: 'Text', extensions: ['txt']},
{name: 'All Files', extensions: ['*']}
],
properties: ['saveFile']
}
);
if(file)
fs.writeFile(file, txt.value, function (err) {
if(err) alert(err);
else alert("保存成功");
});
}
},
{label:'退出',role:'quit'}
]
}))

Menu.setApplicationMenu(menu)

</script>
</html>