背景
在编写Alemon桌面端的时候,遇到这么两个需求:
-
运行alemon实例需要单独运行一个js(运行环境是独立的),并且要实现多个实例互不冲突
-
对各种插件(Github仓库的克隆)进行依赖环境的安装
上面两个需求都需要考虑到客户机没有安装NodeJS。
问题
我们又知道Electron是内嵌了一个NodeJS环境,只不过:
-
没有npm包管理
-
默认不能在外部通过直接调用可执行文件的方法调用NodeJS
突破点
也就是说,我们只需要解决上面两个问题,就能通过Electron自带的NodeJS功能在客户机没有安装NodeJS的情况下使用大部分的独立NodeJS功能。
我们知道,NodeJS使用子进程通常使用child_process
的fork
方法进行创建:
import { join, resolve } from 'node:path';
import { fork } from 'child_process';
const defaultDir = app.isPackaged ? resolve(installDir, './resources') : resolve(process.cwd(), './out'); // 判断是否为生产环境,开发环境与生产环境的执行目录不同
const child = fork(join(resolve(defaultDir), '/example/index.js'), [], {
execArgv: [],
});
Electron也提供了子进程:utilityProcess
,官网给的解释是这样的:
utilityProcess
使用 Node.js 和 Message 端口创建了一个子进程。 它提供一个相当于 Node.js 的child_process.fork
API,但使用 Chromium 的 Services API 代替来执行子进程。
也就是说,Electron提供的创建子进程方法与NodeJS自带的方法的唯一区别是:Electron使用的是Chromium的Services API。
我们的业务进程目前用不到Chromium的Services API,因此我们还是选择使用NodeJS的child_process
。
子进程执行JS
注意:以下仅以alemonjs为例子介绍,方法是通用的,只不过alemonjs比较典型。
AlemonJS就是通过NodeJS调用js文件创建机器人实例的。也就是说我们需要编写一个js文件来配置并运行机器人。我们通过Alemon官网给出的配置方法写了一个运行文件,并且考虑到不同实例的配置也不同,我们还需要主进程-子进程通信进行数据的传递。
子进程
在运行子进程时,我们通过进程通信进行配置的参数传递。
在实际业务开发中,我们应该等待主进程传递配置后再进行操作,我们可以通过execArgv
进行参数传递,但是我不推荐这样进行配置参数的传递,我使用的是主进程-子进程通信,只有子进程接收到启动消息(启动消息携带了配置参数),才会创建一个机器人实例。
// 子进程js(alemon启动js文件)
process.on('message', async (message: IMessage) => {
// 判断消息类型是否为“启动”机器人
if ('type' in message && message?.type === 'start') {
//如果是启动机器人,通常会携带启动参数(账号密码等),在这里面进行判断并且创建机器人实例
//根据配置创建机器人实例
}
// 其他类型消息,建议拆分写
});
主进程
在主进程中,通过child_process
的fork
方法创建一个子进程:
const child = fork(join(resolve(defaultDir), '/alemon/index.js'), [], {
execArgv: [],
cwd: resolve(defaultDir, './root/'),
//....子进程其他配置
});
其中,fork方法的参数可以参考NodeJS文档:Child process | Node.js v21.4.0 Documentation
上面的代码执行了我们刚才编写的alemon启动js文件,并且将执行目录设置到了./root
目录。
为什么要修改执行目录?
在实际的开发中,我们的alemon运行需要引入alemonjs依赖,也就是说要在一个包环境下(包含node_modules),而我们的
./root
目录就是我们所指定的包含包环境的文件夹,我们的插件也放在这个目录的子目录下,共用一个包环境,也方便后面依赖的安装和管理。
同时我们也可以指定运行参数,在这里就不加演示了,运行参数直接为空。
通过上面的子进程js可以看出,只有当父进程对子进程发送消息,并且type
为start
,才会启动机器人,在业务中我们也应该等待配置传入到子进程再创建实例。
因此,主进程要在子进程启动的时候就发送启动消息:
const sendData = {
type: 'start',
data: bot,
};
child.send(sendData);
这样,我们就通过子进程运行了一个js文件,如果我们运行多个js实例,只需要写一个公共的方法进行创建和管理。这里看个人喜好。
当然,光创建还是不够的,我们还要对子进程进行管理,包括消息监听和进程(异常)退出监听。
这里面就不再进行演示,可以参考一下官方文档。
child.on('message', (message: IBotMessage) => {
switch (message.type) {
case MessageType.console:
// 如果是控制台打印
break;
case MessageType.lackOfPackage:
// 如果是缺失依赖
break;
// 其他情况
}
});
child.on('error', error => {
// 错误处理
});
child.on('exit', () => {
// 进程退出
});
child.on('close', () => {
// 进程关闭
});
// 其他事件
在子进程alemonjs中,我对控制台console进行了拦截,并且判断打印信息和类型,通过父子进程通信传递给主进程,在主进程中接收子进程的消息,如上面的代码所示,我对父子进程通信的数据结构进行了统一,通过type判断消息类型,对之进行相应的处理。
当然,既然可以通过子进程运行一个我们写好的js,也可以让用户运行自己写的js,以替代NodeJS的作用,我们可以写一个方法接收Electron可执行文件的运行参数(其中就包括了js文件路径),然后进行处理,子进程运行。不过在业务中这样是不推荐的,容易造成bug漏洞,被有心人所利用。这样问题2也就解决了,需求1也实现了。
包管理
Electron内嵌的NodeJS是没有包管理的功能的,我们可以自己提取包管理的文件运行。原理同上面介绍的子进程运行js。
以yarn
为例:
首先到yarn的发布页下载最新版yarn.js:Releases · yarnpkg/yarn · GitHub
然后在主进程中进行调用:
import { fork } from 'child_process';
import { join } from 'node:path';
/**
* yarn 安装依赖
* @param dir 路径
* @returns 子进程实例
*/
export const installDependencies = (dir: string) => {
// 新建子进程,执行 yarn install
const child = fork(join(__dirname, '../../yarn/yarn.js'),
['install'], // 运行命令,此处相当于 yarn install
{
execArgv: [],
// 运行目录
cwd: dir, // 设置yarn执行目录
});
// 返回子进程实例
return child;
};
/**
* yarn 安装 <包名>
* @param dir 路径
* @param packageName 包名
* @returns 子进程实例
*/
export const installPackage = (packageName: string, dir: string) => {
// 新建子进程,执行 yarn add <包名>
const child = fork(
join(__dirname, '../../yarn/yarn.js'),
['add', packageName], // 此处相当于 yarn add packageName(传入的参数)
{
execArgv: [],
// 运行目录
cwd: dir,
}
);
// 返回子进程实例
return child;
};
经过试验,这种方法是可行的,并且在没有安装NodeJS的客户机上也能够正常进行包管理。
上面封装成了两个函数,可以进行调用,也能够实现对插件依赖的安装和管理。