在之前的文章中,实现了Electron-vue在不同系统打包成安装程序。但这只是前端build/package之后的文件打包,虽然服务端的编译之后的exe文件也可以放到一起打包,并且可以去启动服务端程序。然而不能与服务端通信的话,那么这个程序存在的意义就不大。所以在这片文章中会讲一下怎么在安装之后,启动应用程序调用服务端程序,同时获取服务端的输出值/返回值。

由于不是传统意义上的前后端通信(常见的前后端通信,只需要使用http/https进行通信即可,request发送请求,response返回请求结果,然后解析一顿操作即可),然而当服务端打成exe可执行文件时,就需要使用command去通信,同时需要借助进程之间的通信方式来处理。

2.nodejs实现exe调用

2.1主进程-子进程-渲染进程

我们知道在现在的前端项目构建中,尤其是工程化的项目创建,都会依赖 nodejs ,而 nodejs 本身也足够强大提供一些常用的方法来为js服务。而在我们的 Electron-vue 项目中,也是需要使用 nodejs 提供的一些服务去实现进程之间的通信。

在这里,主要分为主进程,子进程和渲染进程。

主进程与子进程之间的通信(也就是exe的调用和执行),使用的是 nodejs 提供的 child_process 实现。而主进程和渲染进程之间的通信使用的是electron中的 BrowserWindow 来实现。

主要的逻辑处理是:

  1. 主进程通过nodejs提供的child_process来实现启动子进程,并且获取子进程的输出值
  2. 主进程获取子进程输出值,将输出值传给渲染进程
  3. 渲染进程获取到主进程的数据值,将其呈现在页面上,比如子进程是否启动成功等

2.2主进程启动子进程

主进程启动子进程可借助于 nodejs child_process 提供的方法。
child_process 提供了七中方式去创建子进程

  1. child_process.exec(command[, options][, callback])
  2. child_process.execFile(file[, args][, options][, callback])
  3. child_process.fork(modulePath[, args][, options])
  4. child_process.spawn(command[, args][, options])
  1. child_process.execFileSync(file[, args][, options])
  2. child_process.execSync(command[, options])
  3. 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 而异)需要被相应地处理

2.2.1 exec执行exe文件

child_process.exec(command[, options][, callback])

上面的一些参数可以参考exec command。在这里我只强调两个属性值,那就是timeoutmaxBuffer

为什么要强调这个属性呢?是因为我在使用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命令呢?先插个眼,等以后有时间了再好好研究

2.2.2 spawn执行exe文件

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,
     116,
     121,
     112,
     101,
     101,
     114,
     114,
     109,
     115,
     103,
     231,
     155,
     184,
     229,
     133,
     179,
     230,
     156,
     141,
     229,
     138,
     161,
     230,
     156,
     170,
     229,
     144,
     175,
     229,
     138,
     168,
     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文件执行起来确实是这样的
在这里插入图片描述
确实是拿到了我想要的结果。

2.2.3 将主进程的数据传递给渲染进程

子啊说上面的代码中其实就可以看到我关于主进程和渲染进程之间是如何沟通的,对就是在执行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的坑里,跌倒了起来,起来了再跌倒,也应验了那句话千锤百炼终成钢。有问题欢迎留言。
在这里插入图片描述

.现如今前端框架数不胜数,尤其是angular、vue吸引一大批前端开发者,在这个高新技术快速崛起的时代,自然少不了各种框架的结合使用。接下来是介绍electron+vue的结合使用。 2.Electron是什么?? 对于我来说Electron相当于一个浏览器的外壳,可以把网页程序嵌入到壳里面,可以运行在桌面上的一个程序,可以把网页打包成一个在桌面运行的程序,通俗来说就是软件,比如像Q... 说明:写这部分的'原因'是shell本身功能'太单一',无法像'python'一样对'字符串'、'文件解析'、'正则表达式'-->'游刃有余' 补充:探讨'python'执行'shell'命令或'脚本'的历史轨迹 +++++++++++'语言应用场景'+++++++++++ 1)'琐碎'任务一次性的任务交给shell 2)注定要'扩展',代码量'不小','要维护'的任务交给python 3)需要'效率'的工作交给C 备注:学习'subprocess'模块类比'lin.. 本来收集整理网络上相关资料后整理: 从python2.4版本开始,可以用subprocess这个模块来产生子进程,并连接到子进程的标准输入/输出/错误中去,还可以得到子进程的返回。 subprocess意在替代其他几个老的模块或者函数,比如:os.system os.spawn* os.popen* popen2.* commands.* 一、subprocess... 子进程的终止 首先来看一段代码: p = subprocess.Popen(['echo','helloworl.py'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) print(p.poll()) print('Exit code:', ... subprocess.Popen(): 在一些复杂场景中,我们需要将一个进程的执行输出作为另一个进程的输入。在另一些场景中,我们需要先进入到某个输入环境,然后再执行一系列的指令等。这个时候我们就需要使用到suprocess的Popen()方法。该方法有以下参数: args:shell命令,可以是字符串,或者序列类型,如list,tuple。 bufsize:缓冲区大小,可不用关心 stdi... 用上面的方法来获取logcat的信息,它的实际原理是另外开启一个cmd命令来运行python demo.py的命令,即使后面用popen.terminate()也只能关闭cmd的命令,cmd命令被kill掉后,python demo.py的线程由系统来托管,杀死不了python demo.py的进程,从而导致python demo.py不能退出.要执行的脚本是一个死循环的脚本,那么我们就需要手动的来关闭这个脚本,而无法等待这个脚本自己结束。python中有一个很好用的方式来开启进程,即。 python 中 subprocess.Popen 总结 subprocess的目的就是启动一个新的进程并且与之通信。 subprocess模块中只定义了一个类: Popen。可以使用Popen来创建进程,并与进程进行复杂的交互。它的构造函数如下: subprocess.Popen(args, bufsize=0, executable=None, stdin=None, 因为vue项目的默认入口文件是index.html,eletron项目的默认入口文件是main.js,所以这里我们就要解决两个框架搭配时,以哪个文件为入口。这个问题的答案可能很简单,但是其实体现的是vueelectron的关系。如果你仔细阅读electron官网并思考过,应该已经理解electron项目运行时,相当于是一个壳,壳里面集成了nodejs、v8引擎等必须的环境。 在项目开发中常需要打印一些log,常规的console.log()只是将信息反映在了浏览器的控制台中,是“一次性”的信息。为了查看历史日志,就需要做日志记录留存。 实际项目是基于nodejsvue项目,使用的日志记录工具是4.1.0版本的log4js。具体需求是为了记录常规日志,剥离错误日志,在有某些功能运行出错时快速通过记录的日志信息定位问题。 二、实现log4js记录日志(以日期划分日志文件的方式) 1、安装log4js // 注意:如果只执行npm install XXX,没有指定安装到那