Electron应用开发浅谈

前言

本人在学校经常会控制不住自己的手,一有空就某宝,然后经常乱花钱,搞到生活费月月光,买了iPad之后,看到iOS上有款应用叫Pennis但是要花25元才能使用,反正又不是不会写,便自己写了个带UI的应用,即MMM,Month Money Manager,并基于Apache License 2.0许可证发布,项目地址。本文重点谈谈我在写这个东西时遇到的坑和相关问题。

基本架构

技术栈:bootstrap、vue.js、electron

辅助工具:layoutit,electron-forge

目录结构参考我的Github repo

下面简单讲讲数据的处理流程:

1.每次应用一打开,就会通过nodejs的fs模块检测用户数据文件是否存在,是则读取文件并设置两个属性:exists和data,前者表示用户数据是否存在,后者是数据的具体内容(一个JS Object),当exists为false的时候,通过函数生成一个带初始值的object,否则使用用户数据文件里的源数据object,同时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
ipcMain.on('RTOM', (event, arg) => {
switch (arg.head) {
// renderer query current cache every loading time, send a empty body
case 'query': {
event.sender.send('MTOR', {
exists: userDataExists,
data: userDataCache,
});
break;
}
case 'update': {
updateUserData(arg.body);
updateCache();
event.sender.send('MTORUpdateView', {
exists: userDataExists,
data: arg.body,
});
break;
}
default:
// empty
}
});

在主进程里主要是靠ipcMain来收发信息,在RTOM上监听渲染端的数据,数据是一个JS object,当数据头部为query的时候,则把前面读取的文件是否存在和数据object发过去,ipcMain通过event.sender.send() 函数发送数据,并且可以指定频道,这里我用的是MTOR。同时ipcMain也要负责监听来自渲染端的更新请求,然后将渲染端发来的数据object写入本地文件,然后把那些数据通过MTORUpdateView频道再发回渲染端用于更新用户界面。上面的代码均为异步消息。

2.应用首先加载index.html,index.html先通过queryCache() 函数向main.js发送请求,发送请求后进行监听直到获取数据后才初始化创建vue实例:

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
queryCache();
ipcRenderer.on('MTOR', (event, arg) => {
if (arg.exists) {
vueData.exists = true;
vueData.userCache = arg.data;
initalWork();
if (typeof vueData.userCache === 'object') {
const vm0 = new Vue({
el: "#indexView",
data: vueData,
computed: {
imgUrl: function () {
return vueData.needTips ? './localimg/arrow.png' : " ";
}
},
methods: {
appendDayCost: function () {
applyCostExec();
alert("已追加");
},
fixRemain: function () {
fixRemainExec();
alert("已修正");
}
}
});
}
} else {
vueData.exists = false;
vueData.userCache = arg.data;
alert(emptyData);
const allBtn = document.getElementsByClassName('btnBlock');
for (let i = 0; i < allBtn.length; i++) {
allBtn[i].setAttribute('disabled', 'disabled');
}
const vmfake = new Vue({
el: "#indexView",
data: vueData,
computed: {
imgUrl: function () {
return vueData.needTips ? './localimg/arrow.png' : " ";
}
},
methods: {
appendDayCost: function () {
// do nothing
},
fixRemain: function () {
// do nothing
}
}
});
}
});

要分析上面的代码,先从queryCache开始看:

1
2
3
4
function queryCache() {
// 发送请求查询cache,不能直接用sendSync,ipcMain需要设置event.returnValue
ipcRenderer.send('RTOM', generateCommu('query'));
}

ipcRenderer负责为渲染进程收发信息,ipcRenderer发送信息有两种方式:异步和同步,异步用send,同步用sendSync,但是要注意,sendSync发送的数据,ipcMain只有在设置了event.returnValue的值的时候才会响应听同步消息,不设置的话就是渲染页面无法正常显示。

当ipcRenderer接受到来自ipcMain的query应答,就检查数据中的exists为true还是false,若为true,则执行initialWork() ,initialWork的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function initalWork() {
const cacheMeta = vueData.userCache.meta;
if (cacheMeta.recordYear === TODAY.year && cacheMeta.recordMonth === TODAY.month) {
if (cacheMeta.recordDay < TODAY.day) {
// 此处满足同年同月但不同日且今天比记录日后
updateCache(vueData.userCache);
writeCache();
cacheTOVueData();
dontNeedTips();
} else if (cacheMeta.recordDay === TODAY.day) {
cacheTOVueData();
dontNeedTips();
} else {
alert(outOfDate);
}
} else {
alert(outOfDate);
}
}

对main.js返回的文件内容进行检查,若同年同月且此刻是记录日期的第二天,则将部分用户数据重新计算并通过writeCache() 函数发送update消息到ipcMain,将数据写回本地,然后把数据更新到先前创建(在queryCache函数执行前)的vueData中,这样就向用户展示来自数据文件的数值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let vueData = {
// 基本属性
exists: false,
userCache: {name: 'jack'},
needTips: true, // 判断要不要提醒用户设置预算
// ----交换属性----
newTotal: 0, // 覆盖原来的total,最终同步到userCache
addCost: 0, // 每次输入的额,要加到todayCost
// 使用计算属性 todayCredit
// ----被动属性----
deposit: 0,
todayCredit: 0,
alCond: 'alert-success',
cacheRemain: 0, // 从userCache获取
remain_days: 0, // 从userCache获取
al: 'alert',
alDis: 'alert-dismissable',
};

当用户执行追加本日开支和余额修正时,程序分别通过applyCostExec() 和 fixRemain() 进行操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function applyCostExec() {
vueData.userCache.user.dayCost += parseFloat(vueData.addCost);
writeCache();
vueData.addCost = 0;
ipcRenderer.on('MTORUpdateView', (event, arg) => {
vueData.exists = arg.exists;
vueData.userCache = arg.data;
initalWork();
});
}
function fixRemainExec() {
vueData.userCache.user.total = vueData.newTotal;
vueData.userCache.user.remain = vueData.newTotal - vueData.userCache.user.deposit;
vueData.userCache.user.dayAverage = vueData.userCache.user.remain / vueData.userCache.user.daysRemain;
writeCache();
vueData.newTotal = 0;
ipcRenderer.on('MTORUpdateView', (event, arg) => {
vueData.exists = arg.exists;
vueData.userCache = arg.data;
initalWork();
});
}

以applyCostExec为例,首先把vueData中v-model的绑定的变量的值同步到userCache中,然后调用writeCache() 写入本地文件,重设v-model对应变量的值为0,然后监听来自ipcMain的数据并再次执行initialWork() ,重复index.html刚加载的时候的界面初始化流程,使用vue来更新用户界面主要是靠双向绑定减少重复工作,若使用jQuery实现同样的功能,代码量将会显著增加,而fixRemain的流程和applyCostExec也基本一致。

回到前面的分析,若main.js在index.html初始化的时候返回给index.html的数据中exists为false,则通过alert提醒用户未设置预算计划,同时通过vue的计算属性在用户界面展示一个箭头提醒用户设置计划。同时通过setAttribute禁用主界面的特定按钮,直到用户设置好计划为止。

至于config.html的逻辑则简单得多,只需要处理用户保存变更和查看about和help的动作就行了。js代码的结构与index.html差不多,一个vue数据对象负责双向绑定,当用户保存变更的时候就读取数据对象的值构造一个object发送给main.js保存在本地文件,然后用户点击“返回”的时候就让index.html执行前面介绍的流程,更新用户界面等。

遇上的坑和关注点

  1. 异步通讯,根据前面描述的逻辑,vue负责更新用户界面,但是前提是本地文件储存的数据要先更新到vueData中。如果创建vue实例的代码在和ipcRenderer监听的代码处于同一命名空间的话,就会导致渲染网页的时候vue获取不了来自异步通讯的数据而是直接读取页面初始化时生成的vueData,从而报错导致页面刷不出来,而且这个阶段只有开发环境版的vue.js能提醒问题出在什么地方。建议在开发阶段使用vue的开发环境版本,并且开启devtools用于调试。vue实例必须在异步数据到达之后才创建,不然会报错。或者使用同步通讯,但是要注意ipcMain的returnValue的设置。

  2. 生成的程序,窗口标题由不同html的title决定,而对话框的标题,由package.json的productName决定。

  3. 建议使用electron-forge初始化一个新的electron项目,免去手动配置boilerplate。

  4. electron-forge的图标设定不会在编译前起效,即你执行electron-forge start的时候,你设置的icon是不会显示的,因为icon的设置是在程序打包的时候才设置的,所以要看到图标的效果只能make完再查看。

  5. Apache License的使用,除了在根目录放LICENSE(通常能通过github直接创建),还要在源码的头部放上一份包含自己信息的声明:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Copyright [yyyy] [name of copyright owner]
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at
    http://www.apache.org/licenses/LICENSE-2.0
    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.

    方括号内的内容由自己填写,不保留方括号。关于Apache License的使用可以参考这个:教程

    如果包含第三方库,我参考了Apache Spark项目的做法,在LICENSE后追加说明:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    ------------------------------------------------------------------------------------
    This product bundles various third-party components under other open source licenses.
    This section summarizes those components and their licenses. See licenses/
    for text of these licenses.
    MIT License
    -----------
    local-lib/bootstrap.min.css
    local-lib/bootstrap.min.js
    local-lib/jquery.slim.min.js
    local-lib/popper.min.js
    local-lib/vue.js

    然后在根目录创建一个licenses目录,里面放上对应项目的license原文,并且根目录的NOTICE也要加上相关说明:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Month Money Manager
    Copyright 2018 Hochikong
    Bootstrap
    Copyright (c) 2011-2018 Twitter, Inc.
    Copyright (c) 2011-2018 The Bootstrap Authors
    jQuery
    Copyright JS Foundation and other contributors, https://js.foundation/
    popper.js
    Copyright © 2016 Federico Zivolo and contributors
    Vue.js
    Copyright (c) 2013-present, Yuxi (Evan) You

    其他人的实践例子:链接

  6. 建议直接用npm处理问题,比如npm install安装依赖,npm run make编译,虽然实际上调用的是electron-forge make。npm install途中可能会报错,删掉node_modules再重试几遍会比较好。

  7. bootstrap的部分界面特效(比如弹窗)无法在electron app中使用。

总结

Electron+Vue+Bootstrap的GUI应用开发难度应该是比Qt等库更加简单,而且具备跨平台的能力,如果熟悉了Electron中的套路,开发第二第三个electron app应该就更容易了。最重要还是要熟悉node.js那些操作,才能实现和本地软件的交互。

在此特别感谢鳗驼螺 ,他的这篇文章算是个非常经典的electron app开发教程,无论是上下文菜单还是窗口菜单、对话框等,都有详细的介绍,感谢他的贡献,我才能顺利开发这个electron 应用。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2017 - 2020 HOCHIKONG's WAPORIZer All Rights Reserved.

访客数 : | 访问量 :