文中實(shí)現(xiàn)的部分工具方法正處于早期/測(cè)試階段,仍在持續(xù)優(yōu)化中,僅供參考...
在Ubuntu20.04上進(jìn)行開(kāi)發(fā)/測(cè)試,可直接用于Electron項(xiàng)目,測(cè)試版本:Electron@8.2.0 / 9.3.5
Contents
├── Contents (you are here!)
│
├── I. 前言
├── II. 架構(gòu)圖
│
├── III.electron-re 可以用來(lái)做什么?
│ ├── 1) 用于Electron應(yīng)用
│ └── 2) 用于Electron/Nodejs應(yīng)用
│
├── IV.UI功能介紹
│ ├── 主界面
│ ├── 功能1:Kill進(jìn)程
│ ├── 功能2:一鍵開(kāi)啟DevTools
│ ├── 功能3:查看進(jìn)程日志
│ └── 功能4:查看進(jìn)程CPU/Memory占用趨勢(shì)
│
├── V. 使用&原理
│ ├── 引入
│ ├── 怎樣捕獲進(jìn)程資源占用?
│ ├── 怎樣在主進(jìn)程和UI之間共享數(shù)據(jù)?
│ └── 怎樣在UI窗口中繪制折線圖?
│
├── VI. 存在的已知問(wèn)題
│
├── VII. Next To Do
│
├── VIII. 幾個(gè)實(shí)際使用示例
│ ├── 1) Service/MessageChannel示例
│ ├── 2) ChildProcessPool/ProcessHost示例
│ └── 3) test測(cè)試目錄示例
I. 前言
最近在做一個(gè)多文件分片并行上傳模塊的時(shí)候(基于Electron和React),遇到了一些性能問(wèn)題,主要體現(xiàn)在:前端同時(shí)添加大量文件(1000-10000)并行上傳時(shí)(文件同時(shí)上傳數(shù)默認(rèn)為6),在不做懶加載優(yōu)化的情況下,引起了整個(gè)應(yīng)用窗口的卡頓。所以針對(duì)Electron/Nodejs多進(jìn)程這方面做了一些學(xué)習(xí),嘗試使用多進(jìn)程架構(gòu)對(duì)上傳流程進(jìn)行優(yōu)化。
同時(shí)也編寫了一個(gè)方便進(jìn)行Electron/Node多進(jìn)程管理和調(diào)用的工具electron-re,已經(jīng)發(fā)布為npm組件,可以直接安裝:
$: npm install electron-re --save
# or
$: yarn add electron-re
前文《Electron/Node多進(jìn)程工具開(kāi)發(fā)日記》描述了electron-re的開(kāi)發(fā)背景、針對(duì)的問(wèn)題場(chǎng)景以及詳細(xì)的使用方法,這篇文章不會(huì)對(duì)它的基礎(chǔ)使用做過(guò)多說(shuō)明,主要介紹新特性多進(jìn)程管理UI的開(kāi)發(fā)相關(guān)。UI界面基于electron-re已有的BrowserService/MessageChannel和ChildProcessPool/ProcessHost基礎(chǔ)架構(gòu)驅(qū)動(dòng),使用React17 / Babel7開(kāi)發(fā),主界面:

II. electron-re架構(gòu)圖

III. electron-re 可以用來(lái)做什么?
1. 用于Electron應(yīng)用
BrowserServiceMessageChannelProcessManager
在Electron的一些“最佳實(shí)踐”中,建議將占用cpu的代碼放到渲染過(guò)程中而不是直接放在主過(guò)程中,這里先看下chromium的架構(gòu)圖:

每個(gè)渲染進(jìn)程都有一個(gè)全局對(duì)象RenderProcess,用來(lái)管理與父瀏覽器進(jìn)程的通信,同時(shí)維護(hù)著一份全局狀態(tài)。瀏覽器進(jìn)程為每個(gè)渲染進(jìn)程維護(hù)一個(gè)RenderProcessHost對(duì)象,用來(lái)管理瀏覽器狀態(tài)和與渲染進(jìn)程的通信。瀏覽器進(jìn)程和渲染進(jìn)程使用Chromium的IPC系統(tǒng)進(jìn)行通信。在chromium中,頁(yè)面渲染時(shí),UI進(jìn)程需要和main process不斷的進(jìn)行IPC同步,若此時(shí)main process忙,則UIprocess就會(huì)在IPC時(shí)阻塞。所以如果主進(jìn)程持續(xù)進(jìn)行消耗CPU時(shí)間的任務(wù)或阻塞同步IO的任務(wù)的話,就會(huì)在一定程度上阻塞,從而影響主進(jìn)程和各個(gè)渲染進(jìn)程之間的IPC通信,IPC通信有延遲或是受阻,渲染進(jìn)程窗口就會(huì)卡頓掉幀,嚴(yán)重的話甚至?xí)ㄗ〔粍?dòng)。
因此electron-re在Electron已有的Main Process主進(jìn)程和Renderer Process渲染進(jìn)程邏輯的基礎(chǔ)上獨(dú)立出一個(gè)單獨(dú)的Service概念。Service即不需要顯示界面的后臺(tái)進(jìn)程,它不參與UI交互,單獨(dú)為主進(jìn)程或其它渲染進(jìn)程提供服務(wù),它的底層實(shí)現(xiàn)為一個(gè)允許node注入和remote調(diào)用的渲染窗口進(jìn)程。
這樣就可以將代碼中耗費(fèi)cpu的操作(比如文件上傳中維護(hù)一個(gè)數(shù)千個(gè)上傳任務(wù)的隊(duì)列)編寫成一個(gè)單獨(dú)的js文件,然后使用BrowserService構(gòu)造函數(shù)以這個(gè)js文件的地址path為參數(shù)構(gòu)造一個(gè)Service實(shí)例,從而將他們從主進(jìn)程中分離。如果你說(shuō)那這部分耗費(fèi)cpu的操作直接放到渲染窗口進(jìn)程可以嘛?這其實(shí)取決于項(xiàng)目自身的架構(gòu)設(shè)計(jì),以及對(duì)進(jìn)程之間數(shù)據(jù)傳輸性能損耗和傳輸時(shí)間等各方面的權(quán)衡,創(chuàng)建一個(gè)Service的簡(jiǎn)單示例:
const { BrowserService } = require('electron-re');
const myServcie = new BrowserService('app', path.join(__dirname, 'path/to/app.service.js'));
如果使用了BrowserService的話,要想在主進(jìn)程、渲染進(jìn)程、service進(jìn)程之間任意發(fā)送消息就要使用electron-re提供的MessageChannel通信工具,它的接口設(shè)計(jì)跟Electron內(nèi)建的ipc基本一致,也是基于ipc通信原理來(lái)實(shí)現(xiàn)的,簡(jiǎn)單示例如下:
/* ---- main.js ---- */
const { BrowserService } = require('electron-re');
// 主進(jìn)程中向一個(gè)service-app發(fā)送消息
MessageChannel.send('app', 'channel1', { value: 'test1' });
2. 用于Electron/Nodejs應(yīng)用
ChildProcessPoolProcessHost
此外,如果要?jiǎng)?chuàng)建一些不依賴于Electron運(yùn)行時(shí)的子進(jìn)程(相關(guān)參考nodejs child_process),可以使用electron-re提供的專門為nodejs運(yùn)行時(shí)編寫的進(jìn)程池ChildProcessPool類。因?yàn)閯?chuàng)建進(jìn)程本身所需的開(kāi)銷很大,使用進(jìn)程池來(lái)重復(fù)利用已經(jīng)創(chuàng)建了的子進(jìn)程,將多進(jìn)程架構(gòu)帶來(lái)的性能效益最大化,簡(jiǎn)單示例如下:
const { ChildProcessPool } = require('electron-re');
global.ipcUploadProcess = new ChildProcessPool({
path: path.join(app.getAppPath(), 'app/services/child/upload.js'), max: 6
});
一般情況下,在我們的子進(jìn)程執(zhí)行文件中(創(chuàng)建子進(jìn)程時(shí)path參數(shù)指定的腳本),如要想在主進(jìn)程和子進(jìn)程之間同步數(shù)據(jù),可以使用process.send('channel', params)和process.on('channel', function)來(lái)實(shí)現(xiàn)(前提是進(jìn)程以以fork方式創(chuàng)建或者手動(dòng)開(kāi)啟了ipc通信)。但是這樣在處理業(yè)務(wù)邏輯的同時(shí)也強(qiáng)迫我們?nèi)リP(guān)注進(jìn)程之間的通信,你需要知道子進(jìn)程什么時(shí)候能處理完畢,然后再使用process.send再將數(shù)據(jù)返回主進(jìn)程,使用方式繁瑣。
electron-re引入了ProcessHost的概念,我稱之為"進(jìn)程事務(wù)中心"。實(shí)際使用時(shí)在子進(jìn)程執(zhí)行文件中只需要將各個(gè)任務(wù)函數(shù)通過(guò)ProcessHost.registry('task-name', function)注冊(cè)成多個(gè)被監(jiān)聽(tīng)的事務(wù),然后配合進(jìn)程池的ChildProcessPool.send('task-name', params)來(lái)觸發(fā)子進(jìn)程事務(wù)邏輯的調(diào)用即可,ChildProcessPool.send()同時(shí)會(huì)返回一個(gè)Promise實(shí)例以便獲取回調(diào)數(shù)據(jù),簡(jiǎn)單示例如下:
/* --- 主進(jìn)程中 --- */
...
global.ipcUploadProcess
.send('task1', params)
.then(rsp => console.log(rsp));
/* --- 子進(jìn)程中 --- */
const { ProcessHost } = require('electron-re');
ProcessHost
.registry('task1', (params) => {
return { value: 'task-value' };
})
.registry('init-works', (params) => {
return fetch(url);
});
IV. UI功能介紹
II 描述了electron-re的主要功能,基于這些功能來(lái)實(shí)現(xiàn)多進(jìn)程監(jiān)控UI面板
主界面
UI參考
electron-process-manager設(shè)計(jì)
預(yù)覽圖:

主要功能如下:
- 展示Electron應(yīng)用中所有開(kāi)啟的進(jìn)程,包括主進(jìn)程、普通的渲染進(jìn)程、Service進(jìn)程(由electron-re引入)、ChildProcessPool創(chuàng)建的子進(jìn)程(由electron-re引入)。
- 進(jìn)程列表中顯示各個(gè)進(jìn)程進(jìn)程號(hào)、進(jìn)程標(biāo)識(shí)、父進(jìn)程號(hào)、內(nèi)存占用大小、CPU占用百分比等,所有進(jìn)程標(biāo)識(shí)分為:main(主進(jìn)程)、service(服務(wù)進(jìn)程)、renderer(渲染進(jìn)程)、node(進(jìn)程池子進(jìn)程),點(diǎn)擊表格頭可以針對(duì)對(duì)某項(xiàng)進(jìn)行遞增/遞減排序。
- 選中某個(gè)進(jìn)程后可以Kill此進(jìn)程、查看進(jìn)程控制臺(tái)Console數(shù)據(jù)、查看1分鐘內(nèi)進(jìn)程CPU/內(nèi)存占用趨勢(shì),如果此進(jìn)程是渲染進(jìn)程的話還可以通過(guò)
DevTools按鈕一鍵打開(kāi)內(nèi)置調(diào)試工具。 - ChildProcessPool創(chuàng)建的子進(jìn)程暫不支持直接打開(kāi)DevTools進(jìn)行調(diào)試,不過(guò)由于創(chuàng)建子進(jìn)程時(shí)添加了
--inspect參數(shù),可以使用chrome的chrome://inspect進(jìn)行遠(yuǎn)程調(diào)試。
功能1:Kill進(jìn)程

功能2:一鍵開(kāi)啟DevTools

功能3:查看進(jìn)程日志

功能3:查看進(jìn)程CPU/Memory占用趨勢(shì)


V.使用&原理
引入
- 在Electron主進(jìn)程入口文件中引入:
const {
MessageChannel, // must required in main.js even if you don't use it
ProcessManager
} = require('electron-re');
- 開(kāi)啟進(jìn)程管理窗口UI
ProcessManager.openWindow();
怎樣捕獲進(jìn)程資源占用?
1.使用ProcessManager監(jiān)聽(tīng)多個(gè)進(jìn)程號(hào)
- 1)在Electron窗口創(chuàng)建事件中將窗口進(jìn)程id放入ProcessManager監(jiān)聽(tīng)列表
/* --- src/index.js --- */
...
app.on('web-contents-created', (event, webContents) => {
webContents.once('did-finish-load', () => {
const pid = webContents.getOSProcessId();
if (
exports.ProcessManager.processWindow &&
exports.ProcessManager.processWindow.webContents.getOSProcessId() === pid
) { return; }
exports.ProcessManager.listen(pid, 'renderer');
webContents.once('closed', function(e) {
exports.ProcessManager.unlisten(this.pid);
}.bind({ pid }));
...
})
});
- 2)在進(jìn)程池fork子進(jìn)程時(shí)將進(jìn)程id放入監(jiān)聽(tīng)列表
/* --- src/libs/ChildProcessPool.class.js --- */
...
const { fork } = require('child_process');
class ChildProcessPool {
constructor({ path, max=6, cwd, env }) {
...
this.event = new EventEmitter();
this.event.on('fork', (pids) => {
ProcessManager.listen(pids, 'node');
});
this.event.on('unfork', (pids) => {
ProcessManager.unlisten(pids);
});
}
/* Get a process instance from the pool */
getForkedFromPool(id="default") {
let forked;
...
forked = fork(this.forkedPath, ...);
this.event.emit('fork', this.forked.map(fork => fork.pid));
...
return forked;
}
...
}
- 3)在Service進(jìn)程注冊(cè)時(shí)監(jiān)聽(tīng)進(jìn)程id
BrowserService進(jìn)程創(chuàng)建時(shí)會(huì)向主進(jìn)程MessageChannel發(fā)送registry請(qǐng)求來(lái)全局注冊(cè)一個(gè)Service服務(wù),此時(shí)將進(jìn)程id放入監(jiān)聽(tīng)列表即可:
/* --- src/index.js --- */
...
exports.MessageChannel.event.on('registry', ({pid}) => {
exports.ProcessManager.listen(pid, 'service');
});
...
exports.MessageChannel.event.on('unregistry', ({pid}) => {
exports.ProcessManager.unlisten(pid)
});
2.使用兼容多平臺(tái)的pidusage庫(kù)每秒采集一次進(jìn)程的負(fù)載數(shù)據(jù):
/* --- src/libs/ProcessManager.class.js --- */
...
const pidusage = require('pidusage');
class ProcessManager {
constructor() {
this.pidList = [process.pid];
this.typeMap = {
[process.pid]: 'main',
};
...
}
/* -------------- internal -------------- */
/* 設(shè)置外部庫(kù)采集并發(fā)送到UI進(jìn)程 */
refreshList = () => {
return new Promise((resolve, reject) => {
if (this.pidList.length) {
pidusage(this.pidList, (err, records) => {
if (err) {
console.log(`ProcessManager: refreshList -> ${err}`);
} else {
this.processWindow.webContents.send('process:update-list', { records, types: this.typeMap });
}
resolve();
});
} else {
resolve([]);
}
});
}
/* 設(shè)置定時(shí)器進(jìn)行采集 */
setTimer() {
if (this.status === 'started') return console.warn('ProcessManager: the timer is already started!');
const interval = async () => {
setTimeout(async () => {
await this.refreshList()
interval(this.time)
}, this.time)
}
this.status = 'started';
interval()
}
...
3.監(jiān)聽(tīng)進(jìn)程輸出來(lái)采集進(jìn)程日志
進(jìn)程池創(chuàng)建的子進(jìn)程可以通過(guò)監(jiān)聽(tīng)
stdout標(biāo)準(zhǔn)輸出流來(lái)進(jìn)行日志采集;Electron渲染窗口進(jìn)程則可以通過(guò)監(jiān)聽(tīng)ipc通信事件console-message來(lái)進(jìn)行采集;
/* --- src/libs/ProcessManager.class.js --- */
class ProcessManager {
constructor() {
...
}
/* pipe to process.stdout */
pipe(pinstance) {
if (pinstance.stdout) {
pinstance.stdout.on(
'data',
(trunk) => {
this.stdout(pinstance.pid, trunk);
}
);
}
}
...
}
/* --- src/index.js --- */
app.on('web-contents-created', (event, webContents) => {
webContents.once('did-finish-load', () => {
const pid = webContents.getOSProcessId();
...
webContents.on('console-message', (e, level, msg, line, sourceid) => {
exports.ProcessManager.stdout(pid, msg);
});
...
})
});
怎樣在主進(jìn)程和UI之間共享數(shù)據(jù)?
基于Electron原生
ipc異步通信
1.使用ProcessManager向UI渲染窗口發(fā)送日志數(shù)據(jù)
1秒之內(nèi)采集到的所有進(jìn)程的console數(shù)據(jù)會(huì)被臨時(shí)緩存到數(shù)組中,默認(rèn)每秒鐘向UI進(jìn)程發(fā)送一次數(shù)據(jù),然后清空臨時(shí)數(shù)組。
在這里需要注意的是ChildProcessPool中的子進(jìn)程是通過(guò)Node.js的child_process.fork()方法創(chuàng)建的,此方法會(huì)衍生shell,且創(chuàng)建子進(jìn)程時(shí)參數(shù)stdio會(huì)被指定為'pipe',指明在子進(jìn)程和父進(jìn)程之間創(chuàng)建一個(gè)管道,從而讓父進(jìn)程中可以直接監(jiān)聽(tīng)子進(jìn)程對(duì)象上的 stdout.on('data')事件來(lái)拿到子進(jìn)程的標(biāo)準(zhǔn)輸出流。
/* --- src/libs/ProcessManager.class.js --- */
class ProcessManager {
constructor() {
...
}
/* pipe to process.stdout */
pipe(pinstance) {
if (pinstance.stdout) {
pinstance.stdout.on(
'data',
(trunk) => {
this.stdout(pinstance.pid, trunk);
}
);
}
}
/* send stdout to ui-processor */
stdout(pid, data) {
if (this.processWindow) {
if (!this.callSymbol) {
this.callSymbol = true;
setTimeout(() => {
this.processWindow.webContents.send('process:stdout', this.logs);
this.logs = [];
this.callSymbol = false;
}, this.time);
} else {
this.logs.push({ pid: pid, data: String.prototype.trim.call(data) });
}
}
}
...
}
2.使用ProcessManager向UI渲染窗口發(fā)送進(jìn)程負(fù)載信息
/* --- src/libs/ProcessManager.class.js --- */
class ProcessManager {
constructor() {
...
}
/* 設(shè)置外部庫(kù)采集并發(fā)送到UI進(jìn)程 */
refreshList = () => {
return new Promise((resolve, reject) => {
if (this.pidList.length) {
pidusage(this.pidList, (err, records) => {
if (err) {
console.log(`ProcessManager: refreshList -> ${err}`);
} else {
this.processWindow.webContents.send('process:update-list', { records, types: this.typeMap });
}
resolve();
});
} else {
resolve([]);
}
});
}
...
}
3.UI窗口拿到數(shù)據(jù)后處理并臨時(shí)存儲(chǔ)
import { ipcRenderer, remote } from 'electron';
...
ipcRenderer.on('process:update-list', (event, { records, types }) => {
console.log('update:list');
const { history } = this.state;
for (let pid in records) {
history[pid] = history[pid] || { memory: [], cpu: [] };
if (!records[pid]) continue;
history[pid].memory.push(records[pid].memory);
history[pid].cpu.push(records[pid].cpu);
// 存儲(chǔ)最近的60條進(jìn)程負(fù)載數(shù)據(jù)
history[pid].memory = history[pid].memory.slice(-60);
history[pid].cpu = history[pid].cpu.slice(-60);
}
this.setState({
processes: records,
history,
types
});
});
ipcRenderer.on('process:stdout', (event, dataArray) => {
console.log('process:stdout');
const { logs } = this.state;
dataArray.forEach(({ pid, data })=> {
logs[pid] = logs[pid] || [];
logs[pid].unshift(`[${new Date().toLocaleTimeString()}]: ${data}`);
});
// 存儲(chǔ)最近的1000個(gè)日志輸出
Object.keys(logs).forEach(pid => {
logs[pid].slice(0, 1000);
});
this.setState({ logs });
});
怎樣在UI窗口中繪制折線圖?
1.注意使用React.PureComponent,會(huì)自動(dòng)在屬性更新進(jìn)行淺比較,以減少不必要的渲染
/* *************** ProcessTrends *************** */
export class ProcessTrends extends React.PureComponent {
componentDidMount() {
...
}
...
render() {
const { visible, memory, cpu } = this.props;
if (visible) {
this.uiDrawer.draw();
this.dataDrawer.draw(cpu, memory);
};
return (
<div className={`process-trends-container ${!visible ? 'hidden' : 'progressive-show' }`}>
<header>
<span className="text-button small" onClick={this.handleCloseTrends}>X</span>
</header>
<div className="trends-drawer">
<canvas
width={document.body.clientWidth * window.devicePixelRatio}
height={document.body.clientHeight * window.devicePixelRatio}
id="trendsUI"
/>
<canvas
width={document.body.clientWidth * window.devicePixelRatio}
height={document.body.clientHeight * window.devicePixelRatio}
id="trendsData"
/>
</div>
</div>
)
}
}
2.使用兩個(gè)Canvas畫布分別繪制坐標(biāo)軸和折線線段
設(shè)置兩個(gè)畫布相互重疊以盡可能保證靜態(tài)的坐標(biāo)軸不會(huì)被重復(fù)繪制,我們需要在組件掛載后初始化一個(gè)坐標(biāo)軸繪制對(duì)象
uiDrawer和一個(gè)數(shù)據(jù)折線繪制對(duì)象dataDrawer
...
componentDidMount() {
this.uiDrawer = new UI_Drawer('#trendsUI', {
xPoints: 60,
yPoints: 100
});
this.dataDrawer = new Data_Drawer('#trendsData');
window.addEventListener('resize', this.resizeDebouncer);
}
...
以下是Canvas相關(guān)的基礎(chǔ)繪制命令:
this.canvas = document.querySelector(selector);
this.ctx = this.canvas.getContext('2d');
this.ctx.strokeStyle = lineColor; // 設(shè)置線段顏色
this.ctx.beginPath(); // 創(chuàng)建一個(gè)新的路徑
this.ctx.moveTo(x, y); // 移動(dòng)到初始坐標(biāo)點(diǎn)(不進(jìn)行繪制)
this.ctx.lineTo(Math.floor(x), Math.floor(y)); // 描述從上一個(gè)坐標(biāo)點(diǎn)到(x, y)的一條直線
this.ctx.stroke(); // 開(kāi)始繪制
繪制類的源代碼可以查看這里Drawer,大概原理是:設(shè)置Canvas畫布寬度width和高度height鋪滿窗口,設(shè)定橫縱坐標(biāo)軸到邊緣的padding值為30,Canvas坐標(biāo)原點(diǎn)[0,0]為繪制區(qū)域左上角頂點(diǎn)。這里以繪制折線圖縱軸坐標(biāo)為例,縱軸表示CPU占用0%-100%或內(nèi)存占用0-1GB,我們可以將縱軸劃分為100個(gè)基礎(chǔ)單位,但是縱軸坐標(biāo)點(diǎn)不用為100個(gè),可以設(shè)置為10個(gè)方便查看,所以每個(gè)坐標(biāo)點(diǎn)就可以表示為[0, (height-padding) - ((height-(2*padding)) / index) * 100 ],index依次等于0,10,20,30...90,其中(height-padding)為最下面那個(gè)坐標(biāo)點(diǎn)位置,(height-(2*padding))為整個(gè)縱軸的長(zhǎng)度。
VI. 存在的已知問(wèn)題
1. 生產(chǎn)環(huán)境下ChildProcessPool未按預(yù)期工作
Electron生產(chǎn)環(huán)境下,如果app被安裝到系統(tǒng)目錄,那么ChildProcessPool不能按照預(yù)期工作,解決辦法有:將app安裝到用戶目錄或者把進(jìn)程池用于創(chuàng)建子進(jìn)程的腳本(通過(guò)path參數(shù)指定)單獨(dú)放到Electron用戶數(shù)據(jù)目錄下(Ubuntu20.04上是~/.config/[appname])。
2. UI界面未監(jiān)聽(tīng)主進(jìn)程Console數(shù)據(jù)
主進(jìn)程暫未支持此功能,正在尋找解決方案。
VII. Next To Do
讓Service支持代碼更新后自動(dòng)重啟
添加ChildProcessPool子進(jìn)程調(diào)度邏輯
優(yōu)化ChildProcessPool多進(jìn)程console輸出
添加可視化進(jìn)程管理界面
增強(qiáng)ChildProcessPool進(jìn)程池功能
增強(qiáng)ProcessHost事務(wù)中心功能
VIII. 幾個(gè)實(shí)際使用示例
-
electronux - 我的一個(gè)Electron項(xiàng)目,使用了
BrowserService/MessageChannel,并且附帶了ChildProcessPool/ProcessHost使用demo。 -
file-slice-upload - 一個(gè)關(guān)于多文件分片并行上傳的demo,使用了
ChildProcessPoolandProcessHost,基于 Electron@9.3.5開(kāi)發(fā)。 - 也查看
test目錄下的測(cè)試樣例文件,包含了完整的細(xì)節(jié)使用。