在之前的文章中,实现了Electron-vue在不同系统打包成安装程序。但这只是前端build/package之后的文件打包,虽然服务端的编译之后的exe文件也可以放到一起打包,并且可以去启动服务端程序。然而不能与服务端通信的话,那么这个程序存在的意义就不大。所以在这片文章中会讲一下怎么在安装之后,启动应用程序调用服务端程序,同时获取服务端的输出值/返回值。
由于不是传统意义上的前后端通信(常见的前后端通信,只需要使用http/https进行通信即可,request发送请求,response返回请求结果,然后解析一顿操作即可),然而当服务端打成exe可执行文件时,就需要使用command去通信,同时需要借助进程之间的通信方式来处理。
我们知道在现在的前端项目构建中,尤其是工程化的项目创建,都会依赖nodejs
,而nodejs
本身也足够强大提供一些常用的方法来为js服务。而在我们的Electron-vue
项目中,也是需要使用nodejs
提供的一些服务去实现进程之间的通信。
在这里,主要分为主进程,子进程和渲染进程。
主进程与子进程之间的通信(也就是exe的调用和执行),使用的是nodejs
提供的child_process
实现。而主进程和渲染进程之间的通信使用的是electron中的BrowserWindow
来实现。
主要的逻辑处理是:
- 主进程通过nodejs提供的child_process来实现启动子进程,并且获取子进程的输出值
- 主进程获取子进程输出值,将输出值传给渲染进程
- 渲染进程获取到主进程的数据值,将其呈现在页面上,比如子进程是否启动成功等
主进程启动子进程可借助于nodejs
的child_process
提供的方法。
child_process
提供了七中方式去创建子进程
异步方式
- child_process.exec(command[, options][, callback])
- child_process.execFile(file[, args][, options][, callback])
- child_process.fork(modulePath[, args][, options])
- child_process.spawn(command[, args][, options])
同步方式
- child_process.execFileSync(file[, args][, options])
- child_process.execSync(command[, options])
- child_process.spawnSync(command[, args][, options])
我们知道同步方式会导致进程阻塞,所以我们一般会使用异步进程的方式去处理。
从命令的方式也可以看出其中exec
,spawn
的参数是command
也就是命令行的形式去执行,而我的需求也是使用命令行启动一个exe
文件基本上是start xxx.exe
。所以在这里也只讲一下exec
和spawn
。
从官方的文档可以看得出,spawn
是最基本的,而exec
是对于spawn
的一种封装或者说是扩展,意思是说spawn
会基于命令command
直接执行,而exec
会衍生 shell
,然后在 shell
中执行 command
,并缓冲任何产生的输出。 传给 exec
函数的 command
字符串会被 shell
直接处理,特殊字符(因 shell 而异)需要被相应地处理
child_process.exec(command[, options][, callback])
上面的一些参数可以参考exec command。在这里我只强调两个属性值,那就是timeout
和maxBuffer
。
为什么要强调这个属性呢?是因为我在使用exec执行命令的时候,exe
文件可以执行,但是无论如何怎么都进不去callback
函数,除非你设置timeout
参数不为0
。
为了开发方便便于debug
主程序的运行,借助于electron-log
输出主程序的日志文件,以便定位执行exe
程序时是否有输出返回值。
1、配置日志文件
安装electron-log
,因为日志只是为了方便我们在开发环境下做问题定位,所以,需要开发依赖,而不是生产依赖。
npm install electron-log --save-dev
2、在main/index.js
中配置日志
import log from 'electron-log'
//配置输出路径,这里为了方便我就直接输出到开发项目根目录下
log.transports.file.file = "D://my-project/agent.log";
3、创建exec方法
在main/index.js
文件中创建执行程序的方法
const cp = require('child_process')
function execPrograme() {
log.info("开始执行-----------------------------")
cp.exec(`start ${__dirname}/main.exe`,(error, stdout, stderr) => {
if (error) {
log.error(`执行的错误: ${error}`);
return;
}
log.info(`stdout: ${stdout}`);
log.error(`stderr: ${stderr}`);
})
log.info("结束执行-----------------------------")
}
然后观察一下控制台会发现vc code终端输出的数据仅有开始和结束的输出,而没有callback
中的输出,得不到我想要的exe执行结果。
而在日志文件中也可以看到:
这个问题我搜了半天一直在关注为什么exec
命令为什么走不进去callback
函数中,在网上找到一大堆方法,结果没有可行的,后来还是在官网api上找到了解决办法:
timeout 默认值: 0。
为0是什么意思呢?官网没有给出解释,而是给出了大于0的时候的解释
如果 timeout 大于 0,则当子进程运行时间超过 timeout 毫秒时,父进程会发送由 killSignal 属性(默认为 ‘SIGTERM’)标识的信号
看了看上面的解释,似乎还是没明白。大于0会发出killSignal
的信号,这个意思难道就是杀死进程?为0的话进程就一直在执行,导致没法进入到callback
回调函数中。
那如果我设置一个大于0的值呢?
function execPrograme() {
log.info("开始执行-----------------------------")
cp.exec(`start ${__dirname}/main.exe`,{ timeout: 3000 }, (error, stdout, stderr) => {
if (error) {
log.error(`执行的错误: ${error}`);
return;
}
log.info(`stdout: ${stdout}`);
log.error(`stderr: ${stderr}`);
})
log.info("结束执行-----------------------------")
}
再次查看输出结果和日志文件
从上面可以看出,确实是进入到callback
中了,并且输出了。但是很遗憾值没有拿到。这就回到了对于killSignal
的理解,如果真的是杀死进程或者其他的,那么程序就相当于终止了,回到回调函数中时就什么也拿不到?
我不知道这么理解对不对,或许有别的其他的解释,但是始终没有找到解决办法。
研究exec
执行文件无果,就转而想尝试一下spawn
的方式去执行。
如果从官网的解释来说,exec
会衍生出shell,那么是不是需要换成shell去执行command命令呢?先插个眼,等以后有时间了再好好研究
child_process.spawn() 方法使用给定的 command 衍生新的进程,并传入 args 中的命令行参数。 如果省略 args,则其默认为空数组。
详细参数信息可以参考spawn执行exe
可以看出在spawn
中并没有timeout
参数,那就你不用担心这个问题了。
官网给出的方法:
child_process.spawn(command[, args][, options])
其中command
就是我们的命令,args
指的是字符串参数的列表,没有的话默认是空[]
,而options
是可选项,比如我们执行ping www.baidu.com
的时候,其命令应该是
child_process.spawn('ping',['www.baidu.com'])
这种方式可以用以实时像服务器获取数据的形式,而我的需求就是调用一次exe文件并取得返回结果,就不用这么配置。其中main.exe
文件和主进程index.js
文件在同一文件夹main
下。
const cp = require('child_process')
// 执行exe程序并获取输出值
function execPrograme() {
log.info("开始执行-----------------------------")
let message
let child = cp.spawn(`${__dirname}/main.exe`)
child.on('error',console.error)
child.stdout.on('data',(data)=>{
log.info('data=',data)
})
child.stderr.on('data',(data)=>{
log.info('data=',data)
})
// log.info(child.stdout)
setTimeout(()=>{
mainWindow.webContents.send('asynchronous-message',JSON.stringify(message))
},5000);
log.info("结束执行-----------------------------")
}
查看一下输出结果:
[2020-09-25 18:26:04.462] [info] 开始执行-----------------------------
[2020-09-25 18:26:04.490] [info] 结束执行-----------------------------
[2020-09-25 18:26:04.768] [info] data= { type: 'Buffer',
data:
[ 123,
34,
116,
121,
112,
101,
34,
58,
34,
101,
114,
114,
34,
44,
32,
34,
109,
115,
103,
34,
58,
34,
231,
155,
184,
229,
133,
179,
230,
156,
141,
229,
138,
161,
230,
156,
170,
229,
144,
175,
229,
138,
168,
34,
125,
10 ] }
可以看到确实是有输出结果的,但是输出的结果明显是一个Buffer
,我们需要将Buffer
转为字符串,我们来尝试修改一下代码:
function execPrograme() {
log.info("开始执行-----------------------------")
let message
let child = cp.spawn(`${__dirname}/main.exe`)
child.on('error',console.error)
child.stdout.on('data',(data)=>{
let logs = data.toString().split('\n').filter(x => x);
logs.forEach(el => {
log.info(`${el}\n\n`)
message = `${el}\n\n`
});
})
child.stderr.on('data',(data)=>{
let logs = data.toString().split('\n').filter(x => x);
logs.forEach(el => {
log.info(`${el}\n\n`)
message = `${el}\n\n`
});
})
// log.info(child.stdout)
setTimeout(()=>{
mainWindow.webContents.send('asynchronous-message',JSON.stringify(message))
},5000);
log.info("结束执行-----------------------------")
}
输出结果如下:
[2020-09-25 18:30:32.555] [info] 开始执行-----------------------------
[2020-09-25 18:30:32.582] [info] 结束执行-----------------------------
[2020-09-25 18:30:32.838] [info] {"type":"err", "msg":"相关服务未启动"}
好像是我们期待的结果,而实际上我的main.exe
文件执行起来确实是这样的
确实是拿到了我想要的结果。
子啊说上面的代码中其实就可以看到我关于主进程和渲染进程之间是如何沟通的,对就是在执行exe
函数中通过mainWindow
将数据发送给子进程的。
main/index.js
文件中的execPrograme
函数
function execPrograme() {
log.info("开始执行-----------------------------")
let message //定义存储输出值的变量
let child = cp.spawn(`${__dirname}/main.exe`)
child.on('error',console.error)
child.stdout.on('data',(data)=>{
let logs = data.toString().split('\n').filter(x => x);
logs.forEach(el => {
log.info(`${el}\n\n`)
message = `${el}\n\n` //将返回值赋值给message
});
})
child.stderr.on('data',(data)=>{
let logs = data.toString().split('\n').filter(x => x);
logs.forEach(el => {
log.info(`${el}\n\n`)
message = `${el}\n\n` //将返回值赋值给message
});
})
// log.info(child.stdout)
setTimeout(()=>{
//将主进程数据传递给子进程
mainWindow.webContents.send('asynchronous-message',JSON.stringify(message))
},5000);
log.info("结束执行-----------------------------")
}
然后我们需要在渲染进程中去监听一下:
在渲染进程render/App.vue
文件中监听
<template>
<div id="app">
<router-view> </router-view>
</div>
</template>
<script>
import { ipcRenderer } from 'electron'
export default {
name: "egent",
data() {
return {
result: "",
};
},
created() {
console.log("初始化项目");
ipcRenderer.on("asynchronous-message", function (event, message) {
console.log(event);
console.log("222222"+ message);
});
},
};
</script>
<style>
/* CSS */
</style>
我们来看一下项目的控制台输出,确实将主进程调用exe返回的数据传递给了渲染进程,那么一个整套的主进程->子进程->渲染进程直接的通信就算是打通了。研究了好长时间,反正在Electron-vue的坑里,跌倒了起来,起来了再跌倒,也应验了那句话千锤百炼终成钢
。有问题欢迎留言。