文中實現(xiàn)的部分工具方法正處于早期/測試階段,仍在持續(xù)優(yōu)化中,僅供參考...
Contents
├── Contents (you are here!)
│
├── electron-re 可以用來做什么?
│ ├── 1) 用于Electron應(yīng)用
│ └── 2) 用于Electron/Nodejs應(yīng)用
│
├── 說明1:Service/MessageChannel
│ ├── Service的創(chuàng)建
│ ├── Service的自動刷新
│ ├── MessageChannel的引入
│ ├── MessageChannel提供的方法
│ └── 對比MessageChannel和原生ipc通信的使用
│ ├── 1) 使用remote遠(yuǎn)程調(diào)用(原生)
│ ├── 2) 使用ipc信號通信(原生)
│ └── 3) 使用MessageChannel進(jìn)行多向通信(擴展)
│
├── 說明2:ChildProcessPool/ProcessHost
│ ├── 進(jìn)程池的創(chuàng)建
│ ├── 進(jìn)程池的實例方法
│ ├── 子進(jìn)程事務(wù)中心
│ └── 進(jìn)程池和子進(jìn)程事務(wù)中心的配合使用
│ ├── 1) 主進(jìn)程中使用進(jìn)程池向子進(jìn)程發(fā)送請求
│ └── 2) 子進(jìn)程中用事務(wù)中心處理消息
│
├── Next To Do
│
├── 幾個實際使用實例
│ ├── 1) Service/MessageChannel示例
│ ├── 2) ChildProcessPool/ProcessHost示例
│ └── 3) test測試目錄示例
I. 前言
最近在做一個多文件分片并行上傳模塊的時候(基于Electron和React),遇到了一些性能問題,主要體現(xiàn)在:前端同時添加大量文件(1000-10000)并行上傳時(文件同時上傳數(shù)默認(rèn)為6),在不做懶加載優(yōu)化的情況下,引起了整個應(yīng)用窗口的卡頓。所以針對Electron/Nodejs多進(jìn)程這方面做了一些學(xué)習(xí),嘗試使用多進(jìn)程架構(gòu)對上傳流程進(jìn)行優(yōu)化。
同時也編寫了一個方便進(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的smb客戶端文件上傳優(yōu)化探索》。
下面來講講主角=> electron-re
II. electron-re 可以用來做什么?
1. 用于Electron應(yīng)用
BrowserService
MessageChannel
在Electron的一些“最佳實踐”中,建議將占用cpu的代碼放到渲染過程中而不是直接放在主過程中,這里先看下chromium的架構(gòu)圖:
每個渲染進(jìn)程都有一個全局對象RenderProcess,用來管理與父瀏覽器進(jìn)程的通信,同時維護(hù)著一份全局狀態(tài)。瀏覽器進(jìn)程為每個渲染進(jìn)程維護(hù)一個RenderProcessHost對象,用來管理瀏覽器狀態(tài)和與渲染進(jìn)程的通信。瀏覽器進(jìn)程和渲染進(jìn)程使用Chromium的IPC系統(tǒng)進(jìn)行通信。在chromium中,頁面渲染時,UI進(jìn)程需要和main process不斷的進(jìn)行IPC同步,若此時main process忙,則UIprocess就會在IPC時阻塞。所以如果主進(jìn)程持續(xù)進(jìn)行消耗CPU時間的任務(wù)或阻塞同步IO的任務(wù)的話,就會在一定程度上阻塞,從而影響主進(jìn)程和各個渲染進(jìn)程之間的IPC通信,IPC通信有延遲或是受阻,渲染進(jìn)程窗口就會卡頓掉幀,嚴(yán)重的話甚至?xí)ㄗ〔粍印?/p>
因此electron-re
在Electron已有的Main Process
主進(jìn)程和Renderer Process
渲染進(jìn)程邏輯的基礎(chǔ)上獨立出一個單獨的Service
概念。Service
即不需要顯示界面的后臺進(jìn)程,它不參與UI交互,單獨為主進(jìn)程或其它渲染進(jìn)程提供服務(wù),它的底層實現(xiàn)為一個允許node注入
和remote調(diào)用
的渲染窗口進(jìn)程。
這樣就可以將代碼中耗費cpu的操作(比如文件上傳中維護(hù)一個數(shù)千個上傳任務(wù)的隊列)編寫成一個單獨的js文件,然后使用BrowserService
構(gòu)造函數(shù)以這個js文件的地址path
為參數(shù)構(gòu)造一個Service
實例,從而將他們從主進(jìn)程中分離。如果你說那這部分耗費cpu的操作直接放到渲染窗口進(jìn)程可以嘛?這其實取決于項目自身的架構(gòu)設(shè)計,以及對進(jìn)程之間數(shù)據(jù)傳輸性能損耗和傳輸時間等各方面的權(quán)衡,創(chuàng)建一個Service
的簡單示例:
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è)計跟Electron內(nèi)建的ipc
基本一致,也是基于ipc
通信原理來實現(xiàn)的,簡單示例如下:
/* ---- main.js ---- */
const { BrowserService } = require('electron-re');
// 主進(jìn)程中向一個service-app發(fā)送消息
MessageChannel.send('app', 'channel1', { value: 'test1' });
2. 用于Electron/Nodejs應(yīng)用
ChildProcessPool
ProcessHost
此外,如果要創(chuàng)建一些不依賴于Electron運行時的子進(jìn)程(相關(guān)參考nodejs child_process
),可以使用electron-re
提供的專門為nodejs運行時編寫的進(jìn)程池ChildProcessPool
類。因為創(chuàng)建進(jìn)程本身所需的開銷很大,使用進(jìn)程池來重復(fù)利用已經(jīng)創(chuàng)建了的子進(jìn)程,將多進(jìn)程架構(gòu)帶來的性能效益最大化,簡單實例如下:
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)程時path參數(shù)指定的腳本),如要想在主進(jìn)程和子進(jìn)程之間同步數(shù)據(jù),可以使用process.send('channel', params)
和process.on('channel', function)
來實現(xiàn)。但是這樣在處理業(yè)務(wù)邏輯的同時也強迫我們?nèi)リP(guān)注進(jìn)程之間的通信,你需要知道子進(jìn)程什么時候能處理完畢,然后再使用process.send
再將數(shù)據(jù)返回主進(jìn)程,使用方式繁瑣。
electron-re
引入了ProcessHost
的概念,我將它稱之為"進(jìn)程事務(wù)中心"。實際使用時在子進(jìn)程執(zhí)行文件中只需要將各個任務(wù)函數(shù)通過ProcessHost.registry('task-name', function)
注冊成多個被監(jiān)聽的事務(wù),然后配合進(jìn)程池的ChildProcessPool.send('task-name', params)
來觸發(fā)子進(jìn)程的事務(wù)邏輯的調(diào)用即可,ChildProcessPool.send()
同時會返回一個Promise實例以便獲取回調(diào)數(shù)據(jù),簡單示例如下:
/* --- 主進(jìn)程中 --- */
...
global.ipcUploadProcess.send('task1', params);
/* --- 子進(jìn)程中 --- */
const { ProcessHost } = require('electron-re');
ProcessHost
.registry('task1', (params) => {
return { value: 'task-value' };
})
.registry('init-works', (params) => {
return fetch(url);
});
III. Service/MessageChannel
用于Electron應(yīng)用中 - Service進(jìn)程分離/進(jìn)程間通信
BrowserService的創(chuàng)建
需要等待app觸發(fā)
ready
事件后才能開始創(chuàng)建Service,創(chuàng)建后如果立即向Service發(fā)送請求可能接收不到,需要調(diào)用service.connected()
異步方法來等待Service準(zhǔn)備完成,支持Promise寫法。
Electron主進(jìn)程main.js文件中:
/* --- in electron main.js entry --- */
const { app } = require('electron');
const {
BrowserService,
MessageChannel // must required in main.js even if you don't use it
} = require('electron-re');
const isInDev = process.env.NODE_ENV === 'dev';
...
// after app is ready in main process
app.whenReady().then(async () => {
const myService = new BrowserService('app', 'path/to/app.service.js');
const myService2 = new BrowserService('app2', 'path/to/app2.service.js');
await myService.connected();
await myService2.connected();
// open devtools in dev mode for debugging
if (isInDev) myService.openDevTools();
...
});
BrowserService的自動刷新
支持Service代碼文件更新后自動刷新Service,簡單設(shè)置兩個配置項即可。
1.需要聲明當(dāng)前運行環(huán)境為開發(fā)環(huán)境
2.創(chuàng)建Service時禁用web安全策略
const myService = new BrowserService('app', 'path/to/app.service.js', {
...options,
// 設(shè)置開發(fā)模式
dev: true,
// 關(guān)閉安全策略
webPreferences: { webSecurity: false }
});
MessageChannel的引入
注意必須在main.js中引入,引入后會自動進(jìn)行初始化。
MessageChannel在主進(jìn)程/Service/渲染進(jìn)程窗口
中的使用方式基本一致,具體請參考下文"對比MessageChannel和原生ipc通信的使用"。
const {
BrowserService,
MessageChannel // must required in main.js even if you don't use it
} = require('electron-re');
MessageChannel提供的方法
1.公共方法,適用于 - 主進(jìn)程/渲染進(jìn)程/Service
/* 向一個Service發(fā)送請求 */
MessageChannel.send('service-name', channel, params);
/* 向一個Servcie發(fā)送請求,并取得Promise實例 */
MessageChannel.invoke('service-name', channel, params);
/* 根據(jù)windowId/webContentsId,向渲染進(jìn)程發(fā)送請求 */
MessageChannel.sendTo('windowId/webContentsId', channel, params);
/* 監(jiān)聽一個信號 */
MessageChannel.on(channel, func);
/* 監(jiān)聽一次信號 */
MessageChannel.once(channel, func);
2.僅適用于 - 渲染進(jìn)程/Service
/* 向主進(jìn)程發(fā)送消息 */
MessageChannel.send('main', channel, params);
/* 向主進(jìn)程發(fā)送消息,并取得Promise實例 */
MessageChannel.invoke('main', channel, params);
3.僅適用于 - 主進(jìn)程/Service
/*
監(jiān)聽一個信號,調(diào)用處理函數(shù),
可以在處理函數(shù)中返回一個異步的Promise實例或直接返回數(shù)據(jù)
*/
MessageChannel.handle(channel, processorFunc);
對比MessageChannel和原生ipc通信的使用
1/2 - 原生方法,3 - 擴展方法
1.使用remote遠(yuǎn)程調(diào)用
remote模塊為渲染進(jìn)程和主進(jìn)程通信提供了一種簡單方法,使用remote模塊, 你可以調(diào)用main進(jìn)程對象的方法, 而不必顯式發(fā)送進(jìn)程間消息。示例如下,代碼通過remote遠(yuǎn)程調(diào)用主進(jìn)程的BrowserWindows創(chuàng)建了一個渲染進(jìn)程,并加載了一個網(wǎng)頁地址:
/* 渲染進(jìn)程中(web端代碼) */
const { BrowserWindow } = require('electron').remote
let win = new BrowserWindow({ width: 800, height: 600 })
win.loadURL('https://github.com')
注意:remote底層是基于ipc的同步進(jìn)程通信(同步=阻塞頁面),都知道Node.js的最大特性就是異步調(diào)用,非阻塞IO,因此remote調(diào)用不適用于主進(jìn)程和渲染進(jìn)程頻繁通信以及耗時請求的情況,否則會引起嚴(yán)重的程序性能問題。
2.使用ipc信號通信
基于事件觸發(fā)的ipc雙向信號通信,渲染進(jìn)程中的ipcRenderer可以監(jiān)聽一個事件通道,也能向主進(jìn)程或其它渲染進(jìn)程直接發(fā)送消息(需要知道其它渲染進(jìn)程的webContentsId),同理主進(jìn)程中的ipcMain也能監(jiān)聽某個事件通道和向任意一個渲染進(jìn)程發(fā)送消息。
Electron進(jìn)程之間通信最常用的一系列方法,但是在向其它子進(jìn)程發(fā)送消息之前需要知道目標(biāo)進(jìn)程的webContentsId
或者能夠直接拿到目標(biāo)進(jìn)程的實例,使用方式不太靈活。
/* 主進(jìn)程 */
ipcMain.on(channel, listener) // 監(jiān)聽信道 - 異步觸發(fā)
ipcMain.once(channel, listener) // 監(jiān)聽一次信道,監(jiān)聽器觸發(fā)后即刪除 - 異步觸發(fā)
ipcMain.handle(channel, listener) // 為渲染進(jìn)程的invoke函數(shù)設(shè)置對應(yīng)信道的監(jiān)聽器
ipcMain.handleOnce(channel, listener) // 為渲染進(jìn)程的invoke函數(shù)設(shè)置對應(yīng)信道的監(jiān)聽器,觸發(fā)后即刪除監(jiān)聽
browserWindow.webContents.send(channel, args); // 顯式地向某個渲染進(jìn)程發(fā)送信息 - 異步觸發(fā)
/* 渲染進(jìn)程 */
ipcRenderer.on(channel, listener); // 監(jiān)聽信道 - 異步觸發(fā)
ipcRenderer.once(channel, listener); // 監(jiān)聽一次信道,監(jiān)聽器觸發(fā)后即刪除 - 異步觸發(fā)
ipcRenderer.sendSync(channel, args); // 向主進(jìn)程一個信道發(fā)送信息 - 同步觸發(fā)
ipcRenderer.invoke(channel, args); // 向主進(jìn)程一個信道發(fā)送信息 - 返回Promise對象等待觸發(fā)
ipcRenderer.sendTo(webContentsId, channel, ...args); // 向某個渲染進(jìn)程發(fā)送消息 - 異步觸發(fā)
ipcRenderer.sendToHost(channel, ...args) // 向host頁面的webview發(fā)送消息 - 異步觸發(fā)
3.使用MessageChannel進(jìn)行多向通信
- 1)main process - 主進(jìn)程中
const {
BrowserService,
MessageChannel // must required in main.js even if you don't use it
} = require('electron-re');
const isInDev = process.env.NODE_ENV === 'dev';
...
// after app is ready in main process
app.whenReady().then(async () => {
const myService = new BrowserService('app', 'path/to/app.service.js');
const myService2 = new BrowserService('app2', 'path/to/app2.service.js');
await myService.connected();
await myService2.connected();
// open devtools in dev mode for debugging
if (isInDev) myService.openDevTools();
MessageChannel.send('app', 'channel1', { value: 'test1' });
MessageChannel.invoke('app', 'channel2', { value: 'test2' }).then((response) => {
console.log(response);
});
MessageChannel.on('channel3', (event, response) => {
console.log(response);
});
MessageChannel.handle('channel4', (event, response) => {
console.log(response);
return { res: 'channel4-res' };
});
});
- 2)app.service.js - 在一個service中
const { ipcRenderer } = require('electron');
const { MessageChannel } = require('electron-re');
MessageChannel.on('channel1', (event, result) => {
console.log(result);
});
MessageChannel.handle('channel2', (event, result) => {
console.log(result);
return { response: 'channel2-response' }
});
MessageChannel.invoke('app2', 'channel3', { value: 'channel3' }).then((event, result) => {
console.log(result);
});
MessageChannel.send('app2', 'channel4', { value: 'channel4' });
- 3)app2.service.js - 在另一個service中
MessageChannel.handle('channel3', (event, result) => {
console.log(result);
return { response: 'channel3-response' }
});
MessageChannel.once('channel4', (event, result) => {
console.log(result);
});
MessageChannel.send('main', 'channel3', { value: 'channel3' });
MessageChannel.invoke('main', 'channel4', { value: 'channel4' });
- 4)renderer process window - 在一個渲染窗口中
const { ipcRenderer } = require('electron');
const { MessageChannel } = require('electron-re');
MessageChannel.send('app', 'channel1', { value: 'test1'});
MessageChannel.invoke('app2', 'channel3', { value: 'test2' });
MessageChannel.send('main', 'channel3', { value: 'test3' });
MessageChannel.invoke('main', 'channel4', { value: 'test4' });
IV. ChildProcessPool/ProcessHost
用于Electron和Nodejs應(yīng)用中 - Node.js進(jìn)程池/子進(jìn)程事務(wù)中心
進(jìn)程池的創(chuàng)建
進(jìn)程池基于nodejs的child_process
模塊,使用fork
方式創(chuàng)建并管理多個獨立的子進(jìn)程。
創(chuàng)建進(jìn)程池時提供最大子進(jìn)程實例個數(shù)
、子進(jìn)程執(zhí)行文件路徑
等參數(shù)即可,進(jìn)程池會自動接管進(jìn)程的創(chuàng)建和調(diào)用。外部可以通過進(jìn)程池向某個子進(jìn)程發(fā)送請求,而在進(jìn)程池內(nèi)部其實就是按照順序依次將已經(jīng)創(chuàng)建的多個子進(jìn)程中的某一個返回給外部調(diào)用即可,從而避免了其中某個進(jìn)程被過度使用。
子進(jìn)程是通過懶加載方式創(chuàng)建的,也就是說如果只創(chuàng)建進(jìn)程池而不對進(jìn)程池發(fā)起請求調(diào)用的話,進(jìn)程池將不會創(chuàng)建任何子進(jìn)程實例。
1.參數(shù)說明
|—— path 參數(shù)為可執(zhí)行文件路徑
|—— max 指明進(jìn)程池創(chuàng)建的最大子進(jìn)程實例數(shù)量
|—— env 為傳遞給子進(jìn)程的環(huán)境變量
2.主進(jìn)程中引入進(jìn)程池類,并創(chuàng)建進(jìn)程池實例
/* main.js */
...
const { ChildProcessPool } = require('electron-re');
const processPool = new ChildProcessPool({
path: path.join(app.getAppPath(), 'app/services/child/upload.js'),
max: 3,
env: { lang: global.lang }
});
...
進(jìn)程池的實例方法
注意task-name即一個子進(jìn)程注冊的任務(wù)名,指向子進(jìn)程的某個函數(shù),具體請查看下面子進(jìn)程事務(wù)中心的說明
1.processPool.send('task-name', params, id)
向某個子進(jìn)程發(fā)送消息,如果請求參數(shù)指定了id則表明需要使用之前與此id建立過映射的某個進(jìn)程(id將在send調(diào)用之后自動綁定),并期望拿到此進(jìn)程的回應(yīng)結(jié)果。
id的使用情況比如:我第一次調(diào)用進(jìn)程池在一個子進(jìn)程里設(shè)置了一些數(shù)據(jù)(子進(jìn)程之間數(shù)據(jù)不共享),第二次時想拿到之前設(shè)置的那個數(shù)據(jù),這時候只要保持兩次send()
請求攜帶的id一致即可,否則將不能保證兩次請求發(fā)送給了同一個子進(jìn)程。
/**
* send [Send request to a process]
* @param {[String]} taskName [task name - necessary]
* @param {[Any]} params [data passed to process - necessary]
* @param {[String]} id [the unique id bound to a process instance - not necessary]
* @return {[Promise]} [return a Promise instance]
*/
send(taskName, params, givenId) {...}
2.processPool.sendToAll('task-name', params)
向進(jìn)程池中的所有進(jìn)程發(fā)送信號,并期望拿到所有進(jìn)程返回的結(jié)果,返回的數(shù)據(jù)為一個數(shù)組。
/**
* sendToAll [Send requests to all processes]
* @param {[String]} taskName [task name - necessary]
* @param {[Any]} params [data passed to process - necessary]
* @return {[Promise]} [return a Promise instance]
*/
sendToAll(taskName, params) {...}
3.processPool.disconnect(id)
銷毀進(jìn)程池的子進(jìn)程,如果不指定id
調(diào)用的話就會銷毀所有子進(jìn)程,指定id
參數(shù)可以單獨銷毀與此id
值綁定過的某個子進(jìn)程,銷毀后再次調(diào)用進(jìn)程池發(fā)送請求時會自動創(chuàng)建新的子進(jìn)程。
需要注意的是id
綁定操作是在processPool.send('task-name', params, id)
方法調(diào)用后自動進(jìn)行的。
4.processPool.setMaxInstanceLimit(number)
除了在創(chuàng)建進(jìn)程池時使用max
參數(shù)指定最大子進(jìn)程實例個數(shù),也能調(diào)用進(jìn)程池的此方法來動態(tài)設(shè)置需要創(chuàng)建的子進(jìn)程實例個數(shù)。
子進(jìn)程事務(wù)中心
ProcessHost
- 子進(jìn)程事務(wù)中心,需要和ChildProcessPool協(xié)同工作,用來分離子進(jìn)程通信邏輯和業(yè)務(wù)邏輯,優(yōu)化子進(jìn)程代碼結(jié)構(gòu)。
主要功能是使用api諸如 - ProcessHost.registry(taskName, func)
來注冊多種任務(wù)
,然后在主進(jìn)程中可以直接使用進(jìn)程池向某個任務(wù)
發(fā)送請求并取得Promise
對象以拿到進(jìn)程回調(diào)返回的數(shù)據(jù),從而避免在我們的子進(jìn)程執(zhí)行文件中編寫代碼時過度關(guān)注進(jìn)程之間數(shù)據(jù)的通信。
如果不使用進(jìn)程事務(wù)管理中心
的話我們就需要使用process.send
來向一個進(jìn)程發(fā)送消息并在另一個進(jìn)程中使用process.on('message', processor)
處理消息。需要注意的是如果注冊的task
任務(wù)是異步的則需要返回一個Promise對象而不是直接return
數(shù)據(jù),實例方法如下:
- 1)registry用于子進(jìn)程向事務(wù)中心注冊自己的任務(wù)(支持鏈?zhǔn)秸{(diào)用)
- 2)unregistry用于取消任務(wù)注冊(支持鏈?zhǔn)秸{(diào)用)
使用說明:
/* in child process */
const { ProcessHost } = require('electron-re');
ProcessHost
.registry('test1', (params) => {
return params;
})
.registry('test2', (params) => {
return fetch(url);
});
ProcessHost
.unregistry('test1')
.unregistry('test2');
進(jìn)程池和子進(jìn)程事務(wù)中心的配合使用
示例:文件分片上傳中,主進(jìn)程中使用進(jìn)程池來發(fā)送初始化分片上傳
請求,子進(jìn)程拿到請求信號處理業(yè)務(wù)然后返回
1.in main processs - 主進(jìn)程中
/**
* init [初始化上傳]
* @param {[String]} host [主機名]
* @param {[String]} username [用戶名]
* @param {[Object]} file [文件描述對象]
* @param {[String]} abspath [絕對路徑]
* @param {[String]} sharename [共享名]
* @param {[String]} fragsize [分片大小]
* @param {[String]} prefix [目標(biāo)上傳地址前綴]
*/
init({ username, host, file, abspath, sharename, fragsize, prefix = '' }) {
const date = Date.now();
const uploadId = getStringMd5(date + file.name + file.type + file.size);
let size = 0;
return new Promise((resolve) => {
this.getUploadPrepath
.then((pre) => {
/* 看這里看這里!look here! */
return processPool.send(
/* 進(jìn)程事務(wù)名 */
'init-works',
/* 攜帶的參數(shù) */
{
username, host, sharename, pre, prefix,
size: file.size, name: file.name, abspath, fragsize
},
/* 指定一個進(jìn)程調(diào)用id */
uploadId
)
})
.then((rsp) => {
resolve({
code: rsp.error ? 600 : 200,
result: rsp.result,
});
}).catch(err => {
resolve({
code: 600,
result: err.toString()
});
});
});
}
2.child.js (in child process)中使用事務(wù)管理中心處理消息
child.js即為創(chuàng)建進(jìn)程池時傳入的
path
參數(shù)所在的nodejs腳本代碼,在此腳本中我們注冊多個任務(wù)來處理從進(jìn)程池發(fā)送過來的消息
其中:
> uploadStore - 主要用于在內(nèi)存中維護(hù)整個文件上傳列表,對上傳任務(wù)列表進(jìn)行增刪查改操作(cpu耗時操作)
> fileBlock - 利用FS API操作文件,比如打開某個文件的文件描述符、根據(jù)描述符和分片索引值讀取一個文件的某一段Buffer數(shù)據(jù)、關(guān)閉文件描述符等等。雖然都是異步IO讀寫,對性能影響不大,不過為了整合整個上傳處理流程也將其一同納入子進(jìn)程中管理。
const fs = require('fs');
const path = require('path');
const utils = require('./child.utils');
const { readFileBlock, uploadRecordStore, unlink } = utils;
const { ProcessHost } = require('electron-re');
// read a file block from a path
const fileBlock = readFileBlock();
// maintain a shards upload queue
const uploadStore = uploadRecordStore();
global.lang = process.env.lang;
/* *************** registry all tasks *************** */
ProcessHost
.registry('init-works', (params) => {
return initWorks(params);
})
.registry('upload-works', (params) => {
return uploadWorks(params);
})
...
/* *************** upload logic *************** */
/* 上傳初始化工作 */
function initWorks({username, host, sharename, pre, prefix, name, abspath, size, fragsize }) {
const remotePath = path.join(pre, prefix, name);
return new Promise((resolve, reject) => {
new Promise((reso) => fsPromise.unlink(remotePath).then(reso).catch(reso))
.then(() => {
const dirs = utils.getFileDirs([path.join(prefix, name)]);
return utils.mkdirs(pre, dirs);
})
.then(() => fileBlock.open(abspath, size))
.then((rsp) => {
if (rsp.code === 200) {
const newRecord = {
...
};
uploadStore.set(newRecord);
return newRecord;
} else {
throw new Error(rsp.result);
}
})
.then(resolve)
.catch(error => {
reject(error.toString());
});
})
}
/* 上傳分片 */
function uplaodWorks(){ ... };
...
V. Next To Do
[?] 讓Service支持代碼更新后自動重啟
[X] 添加ChildProcessPool子進(jìn)程調(diào)度邏輯
[X] 優(yōu)化ChildProcessPool多進(jìn)程console輸出
[X] 增強ChildProcessPool進(jìn)程池功能
[X] 增強ProcessHost事務(wù)中心功能
VI. 一些實際使用示例
electronux - 我的一個Electron項目,使用了
BrowserService
andMessageChannel
。file-slice-upload - 一個關(guān)于多文件分片并行上傳的demo,使用了
ChildProcessPool
andProcessHost
,基于 Electron@9.3.5。查看
test
目錄下的測試樣例文件,包含了完整的細(xì)節(jié)使用。