目的: 开发electron
桌面应用,服务端(c++
开发的exe
程序)随着打包客户端一并打包。想要在用户打开客户端的时候,同时将桌面应用的服务端启动,并使服务端exe
程序隐藏运行。并且服务端程序要随着客户端的关闭而关闭。
做法: 启动客户端的同时,使用node
来启动服务端exe
程序。在node.js
中,需要使用child_process
模块来启动一个子进程(服务端exe程序
),阅读文档发现可以使用child_process
中的exec
和spawn
来实现打开服务端exe程序
。
exec
和spawn
的区别?
官方文档:child_process 子进程 | Node.js v20 文档 (nodejs.cn)
-
运行命令参数:
-
配置项:
-
exec
的配置项使用对象的形式传入第二个参数,部分配置:配置项 类型 原理 timeout
<number>
运行超时时间。默认值: 0
maxBuffer
<number>
标准输出或标准错误上允许的最大数据量(单位:字节)。如果超过,则子进程将终止并截断任何输出,默认 1024*1024
,windowsHide
<number>
隐藏通常在 Windows
系统上创建的子进程控制台窗口。默认值:false
。 -
spawn
的配置项使用对象的形式传入第三个参数,部分配置:配置项 类型 原理 detached
<boolean>
准备子进程独立于其父进程允许 windowsHide
<boolean>
隐藏通常在 Windows
系统上创建的子进程控制台窗口。默认值:false
。signal
<AbortSignal>
允许使用 AbortSignal
中止子进程
-
启动服务端exe
程序:
针对打开客户端的时候运行服务端,对exec
和spawn
的第一个参数传入正确的服务端路径就可以成功打开exe服务端程序
,
javascript
const { spawn } = require('child_process')
const path = require('path')
// 服务端路径
const CPLUSPLUS_EXECUTABLE_PATH = path.join(__dirname, '..', 'grpc_server', 'FileTransGRPCServer.exe')
let childProcess = spawn(CPLUSPLUS_EXECUTABLE_PATH)
此时就可以成功的启动指定路径的FileTransGRPCServer.exe
程序,我已将该服务文件,放入electron
客户端开发目录下。
配置子进程:
exe
程序在Windows
上隐藏运行,使用到了spawn
的配置项windowsHide: true
传入spawn
的第三个配置项中- 将
detached
设为true
让子进程独立于父进程运行 - 第二个参数为空,则使用
[]
占位
javascript
const { spawn } = require('child_process')
const path = require('path')
// 服务端路径
const CPLUSPLUS_EXECUTABLE_PATH = path.join(__dirname, '..', 'grpc_server', 'FileTransGRPCServer.exe')
// 配置项
const EXECUTION_OPTIONS = {
windowsHide: true,
detached: true, // 让子进程独立于父进程运行
}
let childProcess = spawn(CPLUSPLUS_EXECUTABLE_PATH, [], EXECUTION_OPTIONS)
此时可以在启动客户端时,在 Windows
系统上隐藏并且独立运行exe
服务端程序。
关闭客户端同时关闭服务端:
使用spawn
创建的子进程,会返回一个ChildProcess
,使用变量接收。
关闭子进程的三种方法:
-
在
spawn
创建子进程中配置signal
来允许使用AbortSignal
中止子进程。-
需要安装
npm i abort-controller
使用
javascript// 1. 导入 const { AbortController } = require('abort-controller') // 2. 创建实例化对象 const controller = new AbortController(); // 3. 解构 spawn 使用的配置项 signal const { signal } = controller; // 4. 配置 signal let childProcess = spawn(CPLUSPLUS_EXECUTABLE_PATH, [], { signal }) // 5. 此时停止子进程 controller.abort();
-
-
使用命令关闭进程:
- 指定
pid
关闭进程,taskkill /pid 10802 -f
,使用childProcess.pid
获取进程pid
(exec
创建的进程除外后续讲到) - 指定进程名称关闭进程,
taskkill /im 进程名称 -f
- 指定
-
使用
ChildProcess.kill()
来关闭子进程
上述为使用spawn
创建的子进程并且关闭子进程的操作。
在这之前,我使用的exec
来去创建子进程,并且尝试关闭启动的子进程,发现无法关闭,发生错误。
关闭exec
创建子进程的问题:
- 问题: 在使用上述方法来关闭进程时,发现报错无法关掉启动的进程。
- 原因:
exec
创建子进程返回的实例,不是真正启动的子进程,而是对子进程的套壳进程。获取的pid
也为套壳进程的pid
,而非子进程的pid
。所以无法使用上述方法1,方法2.1,方法3来关闭子进程。
最终使用child_process
下的spawn
来创建子进程实现子进程(服务端exe
程序)的启动和关闭!
到此,打包之后的客户端是无法访问到服务端来去启动服务端。因为在打包之后,electron
客户端中的服务端被一并打包,需要在打包后的目录下暴露服务端程序,才可在打包后,启动客户端来正确的启动服务端。
暴露服务端:vue.config.js
javascript
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
pluginOptions: {
electronBuilder: {
// *** 省略其他配置 ***
builderOptions: {
extraResources: [
{ "from": "grpc_server", "to": "./grpc_server" } // 打包后暴露服务端程序
]
}
}
}
})
这样就成功了!
完整代码
附上启动和关闭服务端exe
程序文件:
javascript
const { spawn } = require('child_process');
const path = require('path');
const CPLUSPLUS_EXECUTABLE_PATH = path.join(__dirname, '..', 'grpc_server', 'FileTransGRPCServer.exe');
// 配置项
const EXECUTION_OPTIONS = {
windowsHide: true,
detached: true, // 让子进程独立于父进程运行
};
let childProcess;
// 启动服务端程序
function startCPlusPlusService() {
childProcess = spawn(CPLUSPLUS_EXECUTABLE_PATH, [], EXECUTION_OPTIONS, (error, stdout, stderr) => {
if (error) {
console.error(`Error starting C++ service: ${error}`);
return;
}
console.log(`C++ service stdout: ${stdout}`);
console.error(`C++ service stderr: ${stderr}`);
});
}
// 关闭服务端程序
function stopCPlusPlusService() {
console.log(childProcess.pid)
/**
* 注意:想要关闭启动的进程,必须使用 spawn 来创建子进程,
* 使用 exec 创建子进程无法使用 kill 方法关闭子进程:
* 因为 exec 创建子进程的实例实际上不是子进程,而是套壳进程,
* 无法获取子进程的 pid,而是在子进程的外部套壳,获取的是套壳进程的 pid
*/
/**
* @param 1. childProcess.exitCode 标识子进程的退出代码。如果子进程仍在运行,则返回 null
* @param 2. childProcess.kill() 通过 pid 来关闭子进程
*/
if (childProcess.exitCode === null) {
childProcess.kill();
console.log('Attempting to gracefully shut down the C++ service...');
} else {
console.log('C++ service has already exited.');
}
}
module.exports = { startCPlusPlusService, stopCPlusPlusService };
在electorn
桌面应用的客户端主进程中,
- 监听打开桌面应用的方法中调用该文件中的
startCPlusPlusService
方法来一并启动服务 - 监听桌面应用客户端关闭调用
stopCPlusPlusService
一并关闭服务
实现功能!