>> 博客原文链接

I 前言


上一篇文章《基于Electron的smb客户端开发记录》,大抵形容了整个SMB客户端开发的外围性能、实现难点、我的项目打包这些内容,这篇文章呢独自把其中的文件分片上传模块拿进去进行分享,提及一些与Electron主过程、渲染过程和文件上传优化相干的性能点。

II Demo运行


我的项目精简版 DEMO地址,删除了smb解决的多余逻辑,应用文件复制模仿上传流程,可间接运行体验。

demo运行时须要别离开启两个开发环境view -> service,而后能力预览界面,因为没有后端,文件默认上传(复制)到electron数据目录(在Ubuntu上是~/.config/FileSliceUpload/runtime/upload)
# 进入view目录$: npm install$: npm start# 进入service目录$: npm install$: npm start

III Electron过程架构


主过程和渲染过程的区别

Electron 运行 package.json 的 main 脚本的过程被称为主过程。在主过程中运行的脚本通过创立web页面来展现用户界面,一个 Electron 利用总是有且只有一个主过程。
主过程应用 BrowserWindow 实例创立页面,每个 BrowserWindow 实例都在本人的渲染过程里运行页面,当一个 BrowserWindow 实例被销毁后,相应的渲染过程也会被终止。
主过程治理所有的web页面和它们对应的渲染过程,每个渲染过程都是独立的,它只关怀它所运行的 web 页面。

在一般的浏览器中,web页面通常在沙盒环境中运行,并且无法访问操作系统的原生资源。 然而 Electron 的用户在 Node.js 的 API 反对下能够在页面中和操作系统进行一些底层交互。
在页面中调用与 GUI 相干的原生 API 是不被容许的,因为在 web 页面里操作原生的 GUI 资源是十分危险的,而且容易造成资源泄露。 如果你想在 web 页面里应用 GUI 操作,其对应的渲染过程必须与主过程进行通信,申请主过程进行相干的 GUI 操作。

主过程和渲染过程之间的通信

1/2-自带办法,3-内部扩大办法

1.应用remote近程调用

remote模块为渲染过程和主过程通信提供了一种简略办法,应用remote模块, 你能够调用main过程对象的办法, 而不用显式发送过程间音讯。示例如下,代码通过remote近程调用主过程的BrowserWindows创立了一个渲染过程,并加载了一个网页地址:

/* 渲染过程中(web端代码) */const { BrowserWindow } = require('electron').remotelet win = new BrowserWindow({ width: 800, height: 600 })win.loadURL('https://github.com')

留神:remote底层是基于ipc的同步过程通信(同步=阻塞页面),都晓得Node.js的最大个性就是异步调用,非阻塞IO,因而remote调用不适用于主过程和渲染过程频繁通信以及耗时申请的状况,否则会引起重大的程序性能问题。

2.应用ipc信号通信

基于事件触发的ipc双向信号通信,渲染过程中的ipcRenderer能够监听一个事件通道,也能向主过程或其它渲染过程间接发送音讯(须要晓得其它渲染过程的webContentsId),同理主过程中的ipcMain也能监听某个事件通道和向任意一个渲染过程发送音讯。

/* 主过程 */ipcMain.on(channel, listener) // 监听信道 - 异步触发ipcMain.once(channel, listener) // 监听一次信道,监听器触发后即删除 - 异步触发ipcMain.handle(channel, listener) // 为渲染过程的invoke函数设置对应信道的监听器ipcMain.handleOnce(channel, listener) // 为渲染过程的invoke函数设置对应信道的监听器,触发后即删除监听browserWindow.webContents.send(channel, args); // 显式地向某个渲染过程发送信息 - 异步触发/* 渲染过程 */ipcRenderer.on(channel, listener); // 监听信道 - 异步触发ipcRenderer.once(channel, listener); // 监听一次信道,监听器触发后即删除 - 异步触发ipcRenderer.sendSync(channel, args); // 向主过程一个信道发送信息 - 同步触发ipcRenderer.invoke(channel, args); // 向主过程一个信道发送信息 - 返回Promise对象期待触发ipcRenderer.sendTo(webContentsId, channel, ...args); // 向某个渲染过程发送音讯 - 异步触发ipcRenderer.sendToHost(channel, ...args) // 向host页面的webview发送音讯 - 异步触发

3. 应用electron-re进行多向通信

electron-re 是之前开发的一个解决electron过程间通信的工具,曾经公布为npm组件。次要性能是在Electron已有的Main Process主过程 和 Renderer Process渲染过程概念的根底上独立出一个独自的==service==逻辑。service即不须要显示界面的后盾过程,它不参加UI交互,独自为主过程或其它渲染过程提供服务,它的底层实现为一个容许node注入remote调用的渲染窗口过程。

比方在你看过一些Electron最佳实际中,消耗cpu的操作是不倡议被放到主过程中解决的,这时候咱们就能够将这部分消耗cpu的操作编写成一个独自的js文件,而后应用service构造函数以这个js文件的地址path为参数结构一个service实例,并通过electron-re提供的MessageChannel通信工具在主过程、渲染过程、service过程之间任意发送音讯,能够参考以下示例代码:

  • 1)main process
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 processapp.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();    // send data to a service - like the build-in ipcMain.send    MessageChannel.send('app', 'channel1', { value: 'test1' });    // send data to a service and return a Promise - extension method    MessageChannel.invoke('app', 'channel2', { value: 'test1' }).then((response) => {      console.log(response);    });    // listen a channel, same as ipcMain.on    MessageChannel.on('channel3', (event, response) => {      console.log(response);    });    // handle a channel signal, same as ipcMain.handle    // you can return data directly or return a Promise instance    MessageChannel.handle('channel4', (event, response) => {      console.log(response);      return { res: 'channel4-res' };    });});
  • 2)app.service.js
const { ipcRenderer } = require('electron');const { MessageChannel } = require('electron-re');// listen a channel, same as ipcRenderer.onMessageChannel.on('channel1', (event, result) => {  console.log(result);});// handle a channel signal, just like ipcMain.handleMessageChannel.handle('channel2', (event, result) => {  console.log(result);  return { response: 'channel2-response' }});// send data to another service and return a promise , just like ipcRenderer.invokeMessageChannel.invoke('app2', 'channel3', { value: 'channel3' }).then((event, result) => {  console.log(result);});// send data to a service - like the build-in ipcRenderer.sendMessageChannel.send('app', 'channel4', { value: 'channel4' });
  • 3)app2.service.js
// handle a channel signal, just like ipcMain.handleMessageChannel.handle('channel3', (event, result) => {  console.log(result);  return { response: 'channel3-response' }});// listen a channel, same as ipcRenderer.onceMessageChannel.once('channel4', (event, result) => {  console.log(result);});// send data to main process, just like ipcRenderer.sendMessageChannel.send('main', 'channel3', { value: 'channel3' });// send data to main process and return a Promise, just like ipcRenderer.invokeMessageChannel.invoke('main', 'channel4', { value: 'channel4' });
  • 4)renderer process window
const { ipcRenderer } = require('electron');const { MessageChannel } = require('electron-re');// send data to a serviceMessageChannel.send('app', ....);MessageChannel.invoke('app2', ....);// send data to main processMessageChannel.send('main', ....);MessageChannel.invoke('main', ....);

IV 文件上传架构


文件上传次要逻辑管制局部是前端的JS脚本代码,位于主窗口所在的render渲染过程,负责用户获取系统目录文件、生成上传工作队列、动静展现上传工作列表详情、工作列表的增删查改等;主过程Electron端的Node.js代码次要负责响应render过程的管制命令进行文件上传工作队列数据的增删查改、上传工作在内存和磁盘的同步、文件系统的交互、零碎原生组件调用等。

文件上传源和上传指标

  • 在用户界面上应用Input组件获取到的FileList(HTML5 API,用于web端的简略文件操作)即为上传源;
  • 上传指标地址是远端集群某个节点的smb服务,因为Node.js NPM生态对smb的反对无限,目前并未发现一个能够反对通过smb协定进行文件分片上传的npm库,所以思考应用Node.js的FS API进行文件分段读取而后将分片数据逐渐增量写入指标地址来模仿文件分片上传过程,从而实现在界面上单个大文件上传工作的启动、暂停、终止和续传等操作,所以这里的解决方案是应用Windows UNC命令连贯后端共享后,能够像拜访本地文件系统一样拜访近程一个近程smb共享门路,比方文件门路\\[host]\[sharename]\file1上的file1在执行了unc连贯后就能够通过Node.js FS API进行操作,跟操作本地文件完全一致。整个必须依赖smb协定的上传流程即精简为将本地拿到的文件数据复制到能够在本地拜访的另一个smb共享门路这一流程,而这所有都得益于Windows UNC命令。
/* 应用unc命令连贯近程smb共享 */_uncCommandConnect_Windows_NT({ host, username, pwd }) {    const { isThirdUser, nickname, isLocalUser } = global.ipcMainProcess.userModel.info;    const commandUse = `net use \\\\${host}\\ipc$ "${pwd}" /user:"${username}"`;    return new Promise((resolve) => {      this.sudo.exec(commandUse).then((res) => {        resolve({          code: 200,        });      }).catch((err) => {        resolve({          code: 600,          result: global.lang.upload.unc_connection_failed        });      });    });  }

上传流程概述

下图形容了整个前端局部的管制逻辑:

  1. 页面上应用<Input />组件拿到FileList对象(Electron环境下拿到的File对象会额定附加一个path属性指明文件位于零碎的绝对路径)
  2. 缓存拿到的FileList,期待点击上传按钮后开始读取FileList列表并生成自定义的File文件对象数组用于存储上传工作列表信息
  3. 页面调用init申请附带上选中的文件信息初始化文件上传工作
  4. Node.js拿到init申请附带的文件信息后,将所有信息存入长期寄存在内存中的文件上传列表中,并尝试关上待上传文件的文件描述符用于行将开始的文件切片分段上传工作,最初返回给页面上传工作ID,Node.js端实现初始化解决
  5. 页面拿到init申请胜利的回调后,存储返回的上传工作ID,并将该文件退出文件待上传队列,在适合的机会开始上传,开始上传的时候向Node.js端发送upload申请,同时申请附带上工作ID和以后的分片索引值(示意须要上传第几个文件分片)
  6. Node.js拿到upload申请后依据携带的工作ID读取内存中的上传工作信息,而后应用第二步关上的文件描述符和分片索引对本地磁盘中的指标文件进行分片切割,最初应用FS API将分片递增写入指标地位,即本地可间接拜访的SMB共享门路
  7. upload申请胜利后页面判断是否曾经上传完所有分片,如果实现则向Node.js发送complete申请,同时携带上工作ID
  8. Node.js依据工作ID获取文件信息,敞开文件描述符,更新文件上传工作为上传实现状态
  9. 界面上传工作列表全副实现后,向后端发送sync申请,把当前任务上传列表同步到历史工作(磁盘存储)中,表明以后列表中所有工作曾经实现
  10. Node.js拿到sync申请后,把内存中存储的所有文件上传列表信息写入磁盘,同时开释内存占用,实现一次列表工作上传

Node.js实现的文件分片治理工厂

  • 文件初始化的时候调用open办法长期存储文件描述符和文件绝对路径的映射关系;
  • 文件上传的时候调用read办法依据文件读取地位、读取容量大小进行分片切割;
  • 文件上传实现的时候调用close敞开文件描述符;

三个办法均通过文件绝对路径path参数建设关联:

/**  * readFileBlock [读取文件块]  */exports.readFileBlock = () => {  const fdStore = {};  const smallFileMap = {};  return {    /* 关上文件描述符 */    open: (path, size, minSize=1024*2) => {      return new Promise((resolve) => {        try {          // 小文件不关上文件描述符,间接读取写入          if (size <= minSize) {            smallFileMap[path] = true;            return resolve({              code: 200,              result: {                fd: null              }            });          }          // 关上文件描述符,倡议绝对路径和fd的映射关系          fs.open(path, 'r', (err, fd) => {            if (err) {              console.trace(err);              resolve({                code: 601,                result: err.toString()              });            } else {              fdStore[path] = fd;              resolve({                code: 200,                result: {                  fd: fdStore[path]                }              });            }          });        } catch (err) {          console.trace(err);          resolve({            code: 600,            result: err.toString()          });        }      })    },      /* 读取文件块 */    read: (path, position, length) => {      return new Promise((resolve, reject) => {        const callback = (err, data) => {          if (err) {            resolve({              code: 600,              result: err.toString()            });          } else {            resolve({              code: 200,              result: data            });          }        };        try {          // 小文件间接读取,大文件应用文件描述符和偏移量读取          if (smallFileMap[path]) {            fs.readFile(path, (err, buffer) => {              callback(err, buffer);            });          } else {            // 空文件解决            if (length === 0) return callback(null, '');            fs.read(fdStore[path], Buffer.alloc(length), 0, length, position, function(err, readByte, readResult){              callback(err, readResult);            });          }        } catch (err) {          console.trace(err);          resolve({            code: 600,            result: err.toString()          });        }      });    },    /* 敞开文件描述符 */    close: (path) => {      return new Promise((resolve) => {        try {          if (smallFileMap[path]) {            delete smallFileMap[path];            resolve({              code: 200            });          } else {            fs.close(fdStore[path], () => {              resolve({code: 200});              delete fdStore[path];            });          }        } catch (err) {          console.trace(err);          resolve({            code: 600,            result: err.toString()          });        }      });    },    fdStore  }}

V 基于Electron的文件上传卡顿优化踩坑


优化是一件头大的事儿,因为你须要先通过很多测试手法找到现有代码的性能瓶颈,而后编写优化解决方案。我感觉找到性能瓶颈这一点就特地难,因为是本人写的代码所以容易陷入一些先入为主的刻板思考模式。不过最最次要的一点还是你如果本人都弄不分明你应用的技术栈的话,那就无从谈起优化,所以后面有很大篇幅剖析了Electron过程方面的常识以及梳理了整个上传流程。

应用Electron自带的Devtools进行性能剖析

在文件上传过程中关上性能检测工具Performance进行录制,剖析整个流程:

在文件上传过程中关上内存工具Memory进行快照截取剖析一个时刻的内存占用状况:

第一次尝试解决问题:替换Antd Table组件

在编写实现文件上传模块后,初步进行了压力测试,后果发现增加1000个文件上传工作到工作队列,且同时上传的文件上传工作数量为6时,高低滑动查看文件上传列表时呈现了卡顿的状况,这种卡顿不局限于某个界面组件的卡顿,而且以后窗口的所有操作都卡了起来,初步狐疑是Antd Table组件引起的卡顿,因为Antd Table组件是个很简单的高阶组件,在解决大量的数据时可能会有性能问题,遂我将Antd Table组件换成了原生的table组件,且Table列表只显示每个上传工作的工作名,其余的诸如上传进度这些都不予显示,从而想避开这个问题。令人吃惊的是测试后果是即便换用了原生Table组件,卡顿状况依然毫无改善!

第二次尝试解决问题:革新Electron主进程同步阻塞代码

先看下chromium的架构图,每个渲染过程都有一个全局对象RenderProcess,用来治理与父浏览器过程的通信,同时保护着一份全局状态。浏览器过程为每个渲染过程保护一个RenderProcessHost对象,用来治理浏览器状态和与渲染过程的通信。浏览器过程和渲染过程应用Chromium的IPC零碎进行通信。在chromium中,页面渲染时,UI过程须要和main process一直的进行IPC同步,若此时main process忙,则UIprocess就会在IPC时阻塞。

综上所述:如果主过程继续进行耗费CPU工夫的工作或阻塞同步IO的工作的话,主过程就会在肯定水平上阻塞,从而影响主过程和各个渲染过程之间的IPC通信,IPC通信有提早或是碰壁,天然渲染界面的UI绘制和更新就会出现卡顿的状态。

我剖析了一下Node.js端的文件工作治理的代码逻辑,把一些操作诸如获取文件大小、获取文件类型和删除文件这类的同步阻塞IO调用都换成了Node.js提倡的异步调用模式,即FS callback或Fs Promise链式调用。改变后发现卡顿状况改善不显著,遂进行了第三次尝试。

第三次尝试解决问题:编写Node.js过程池拆散上传工作治理逻辑

这次是大改????

1. 简略实现了node.js过程池
源码:ChildProcessPool.class.js,次要逻辑是应用Node.js的child_process模块(具体应用请看文档) 创立指定数量的多个子过程,内部通过过程池获取一个可用的过程,在过程中执行须要的代码逻辑,而在过程池外部其实就是依照程序顺次将曾经创立的多个子过程中的某一个返回给内部调用即可,从而防止了其中某个过程被适度应用,省略代码如下:

...class ChildProcessPool {  constructor({ path, max=6, cwd, env }) {    this.cwd = cwd || process.cwd();    this.env = env || process.env;    this.inspectStartIndex = 5858;    this.callbacks = {};    this.pidMap = new Map();    this.collaborationMap = new Map();    this.forked = [];    this.forkedPath = path;    this.forkIndex = 0;    this.forkMaxIndex = max;  }  /* Received data from a child process */  dataRespond = (data, id) => { ... }  /* Received data from all child processes */  dataRespondAll = (data, id) => { ... }  /* Get a process instance from the pool */  getForkedFromPool(id="default") {    let forked;    if (!this.pidMap.get(id)) {      // create new process      if (this.forked.length < this.forkMaxIndex) {        this.inspectStartIndex ++;        forked = fork(          this.forkedPath,          this.env.NODE_ENV === "development" ? [`--inspect=${this.inspectStartIndex}`] : [],          {            cwd: this.cwd,            env: { ...this.env, id },          }        );        this.forked.push(forked);        forked.on('message', (data) => {          const id = data.id;          delete data.id;          delete data.action;          this.onMessage({ data, id });        });      } else {        this.forkIndex = this.forkIndex % this.forkMaxIndex;        forked = this.forked[this.forkIndex];      }      if(id !== 'default')        this.pidMap.set(id, forked.pid);      if(this.pidMap.values.length === 1000)        console.warn('ChildProcessPool: The count of pidMap is over than 1000, suggest to use unique id!');      this.forkIndex += 1;    } else {      // use existing processes      forked = this.forked.filter(f => f.pid === this.pidMap.get(id))[0];      if (!forked) throw new Error(`Get forked process from pool failed! the process pid: ${this.pidMap.get(id)}.`);    }    return forked;  }  /**    * onMessage [Received data from a process]    * @param  {[Any]} data [response data]    * @param  {[String]} id [process tmp id]    */  onMessage({ data, id }) {...}  /* Send request to a process */  send(taskName, params, givenId="default") {    if (givenId === 'default') throw new Error('ChildProcessPool: Prohibit the use of this id value: [default] !')    const id = getRandomString();    const forked = this.getForkedFromPool(givenId);    return new Promise(resolve => {      this.callbacks[id] = resolve;      forked.send({action: taskName, params, id });    });  }  /* Send requests to all processes */  sendToAll(taskName, params) {...}}
  • 1)应用sendsendToAll办法向子过程发送音讯,前者是向某个过程发送,如果申请参数指定了id则表明须要明确应用之前与此id建设过映射的某个过程,并冀望拿到此过程的回应后果;后者是向过程池中的所有过程发送信号,并冀望拿到所有过程返回的后果(供调用者内部调用)。
  • 2)其中dataResponddataRespondAll办法对应下面的两个信号发送办法的过程返回数据回调函数,前者拿到过程池中指定的某个过程的回调后果,后者拿到过程池中所有过程的回调后果(过程池外部办法,调用者无需关注)。
  • 3)getForkedFromPool办法是从过程池中拿到一个过程,如果过程池还没有一个子过程或是曾经创立的子过程数量小于设置的可创立子过程数最大值,那么会优先新创建一个子过程放入过程池,而后返回这个子过程以供调用(过程池外部办法,调用者无需关注)。
  • 4)getForkedFromPool办法中值得注意的是这行代码:this.env.NODE_ENV === "development" ? [`--inspect=${this.inspectStartIndex}`] : [],应用Node.js运行js脚本时加上- -inspect=端口号 参数能够开启所运行过程的近程调试端口,多过程程序状态追踪往往比拟艰难,所以采取这种形式后能够应用浏览器Devtools独自调试每个过程(具体能够在浏览器输出地址:chrome://inspect/#devices而后关上调试配置项,配置咱们这边指定的调试端口号,最初点击蓝字Open dedicated DevTools for Node就能关上一个调试窗口,能够对代码过程断点调试、单步调试、步提高出、运行变量查看等操作,非常便当!)。

2. 分离子过程通信逻辑和业务逻辑
另外被作为子过程执行文件载入的js文件中能够应用我封装的ProcessHost.class.js,我把它称为过程事务管理中心,次要性能是应用api诸如 - ProcessHost.registry(taskName, func)来注册多种工作,而后在主过程中能够间接应用过程池获取某个过程后向某个工作发送申请并获得Promise对象以拿到过程回调返回的数据,从而防止在咱们的子过程执行文件中编写代码时适度关注过程之间数据的通信。
如果不应用过程事务管理中心的话咱们就须要应用process.send来向一个过程发送音讯并在另一个过程中应用process.on('message', processor)解决音讯。须要留神的是如果注册的task工作是异步的则须要返回一个Promise对象而不是间接return数据,简略代码如下:

  • 1)registry用于子过程向事务核心注册本人的工作
  • 2)unregistry用于勾销工作注册
  • 3)handleMessage解决过程接管到的音讯并依据action参数调用某个工作
class ProcessHost {  constructor() {    this.tasks = { };    this.handleEvents();    process.on('message', this.handleMessage.bind(this));  }  /* events listener */  handleEvents() {...}  /* received message */  handleMessage({ action, params, id }) {    if (this.tasks[action]) {      this.tasks[action](params)      .then(rsp => {        process.send({ action, error: null, result: rsp || {}, id });      })      .catch(error => {        process.send({ action, error, result: error || {}, id });      });    } else {      process.send({        action,        error: new Error(`ProcessHost: processor for action-[${action}] is not found!`),        result: null,        id,      });    }  }  /* registry a task */  registry(taskName, processor) {    if (this.tasks[taskName]) console.warn(`ProcesHost: the task-${taskName} is registered!`);    if (typeof processor !== 'function') throw new Error('ProcessHost: the processor must be a function!');    this.tasks[taskName] = function(params) {      return new Promise((resolve, reject) => {        Promise.resolve(processor(params))          .then(rsp => {            resolve(rsp);          })          .catch(error => {            reject(error);          });      })    }    return this;  };  /* unregistry a task */  unregistry(taskName) {...};  /* disconnect */  disconnect() { process.disconnect(); }  /* exit */  exit() { process.exit(); }}global.processHost = global.processHost || new ProcessHost();module.exports = global.processHost;

3. ChildProcessPool和ProcessHost的配合应用
具体应用请查看上文残缺demo
1)main.js (in main process)
主过程中引入过程池类,并创立过程池实例

  • |——path参数为可执行文件门路
  • |——max指明过程池创立的最大子过程实例数量
  • |——env为传递给子过程的环境变量
/* main.js */...const ChildProcessPool = require('path/to/ChildProcessPool.class');global.ipcUploadProcess = new ChildProcessPool({  path: path.join(app.getAppPath(), 'app/services/child/upload.js'),  max: 3, // process instance  env: { lang: global.lang, NODE_ENV: nodeEnv }});...

2)service.js (in main processs) 例子:应用过程池来发送初始化分片上传申请

 /**    * init [初始化上传]    * @param  {[String]} host [主机名]    * @param  {[String]} username [用户名]    * @param  {[Object]} file [文件形容对象]    * @param  {[String]} abspath [绝对路径]    * @param  {[String]} sharename [共享名]    * @param  {[String]} fragsize [分片大小]    * @param  {[String]} prefix [指标上传地址前缀]    */  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 global.ipcUploadProcess.send(            /* 过程事务名 */            'init-works',            /* 携带的参数 */            {              username, host, sharename, pre, prefix, size: file.size, name: file.name, abspath, fragsize, record:               {                host, // 主机                filename: path.join(prefix, file.name), // 文件名                size, // 文件大小                fragsize, // 分片大小                abspath, // 绝对路径                startime: getTime(new Date().getTime()), // 上传日期                endtime: '', // 上传日期                uploadId, // 工作id                index: 0,                total: Math.ceil(size / fragsize),                status: 'uploading' // 上传状态              }            },            /* 指定一个过程调用id */            uploadId          )        })      .then((rsp) => {        resolve({          code: rsp.error ? 600 : 200,          result: rsp.result,        });      }).catch(err => {        resolve({          code: 600,          result: err.toString()        });      });    });  }

3)child.js (in child process) 应用事务管理中心解决音讯
child.js即为创立过程池时传入的path参数所在的nodejs脚本代码,在此脚本中咱们注册多个工作来解决从过程池发送过去的音讯。
这段代码逻辑被独自拆散到子过程中解决,其中:

  • uploadStore - 次要用于在内存中保护整个文件上传列表,对上传工作列表进行增删查改操作(cpu耗时操作)
  • fileBlock - 利用FS API操作文件,比方关上某个文件的文件描述符、依据描述符和分片索引值读取一个文件的某一段Buffer数据、敞开文件描述符等等。尽管都是异步IO读写,对性能影响不大,不过为了整合nodejs端上传解决流程也将其一起纳入了子过程中治理,具体能够查看源码进行理解:源码
  const fs = require('fs');  const fsPromise = fs.promises;  const path = require('path');  const utils = require('./child.utils');  const { readFileBlock, uploadRecordStore, unlink } = utils;  const ProcessHost = require('./libs/ProcessHost.class');  // 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);    })    .registry('close', (params) => {      return close(params);    })    .registry('record-set', (params) => {      uploadStore.set(params);      return { result: null };    })    .registry('record-get', (params) => {      return uploadStore.get(params);    })    .registry('record-get-all', (params) => {      return (uploadStore.getAll(params));    })    .registry('record-update', (params) => {      uploadStore.update(params);      return ({result: null});    })    .registry('record-remove', (params) => {      uploadStore.remove(params);      return { result: null };    })    .registry('record-reset', (params) => {      uploadStore.reset(params);      return { result: null };    })    .registry('unlink', (params) => {      return unlink(params);    });  /* *************** upload logic *************** */  /* 上传初始化工作 */  function initWorks({username, host, sharename, pre, prefix, name, abspath, size, fragsize, record }) {    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 = {            ...record,            size, // 文件大小            remotePath,            username,            host,            sharename,            startime: utils.getTime(new Date().getTime()), // 上传日期            total: Math.ceil(size / fragsize),          };          uploadStore.set(newRecord);          return newRecord;        } else {          throw new Error(rsp.result);        }     })     .then(resolve)     .catch(error => {      reject(error.toString());     });    })  }  ...

第四次尝试解决问题:从新扫视渲染过程前端代码

  • 很遗憾,第三次优化对卡顿的改善仍然不显著,我开始狐疑是否是前端代码间接影响的渲染过程卡顿,毕竟前端并非采纳懒加载模式进行文件载入上传的(这一狐疑之前被我否定,因为前端代码齐全沿用了之前浏览器端对象存储文件分片上传开发时的逻辑,而在对象存储文件上传中并未察觉到界面卡顿,属实奇怪)。摒弃了先入为主的思维,其实Electron跟浏览器环境还是有些不同,不能排除前端代码就没有问题。
  • 在具体查看了可能消耗CPU计算的代码逻辑后,发现有一段对于刷新上传工作的函数refreshTasks,次要逻辑是遍历所有未经上传文件原始对象数组,而后选取固定某个数量的文件(数量取决于设置的同时上传工作个数)放入待上传文件列表中,我发现如果待上传文件列表的文件数量 = 设置的同时上传工作个数 的状况下就不必持续遍历剩下的文件原始对象数组了。就是少写了这个判断条件导致refreshTasks这个频繁操作的函数在每次执行时可能多执行数千遍for循环内层判断逻辑(具体执行次数呈O(n)次增长,n为当前任务列表工作数量)。
  • 加上一行检测逻辑代码后,之前1000个上传工作增长到10000个左右都不会太卡了,尽管还有稍微卡顿,但没有到不能应用的水平,后续还有优化空间!

总结


第一次把Electron技术利用到理论我的项目中,踩了挺多坑:render过程和主过程通信的问题、跨平台兼容的问题、多平台打包的问题、窗口治理的问题... 总之取得了很多教训,也整顿出了一些通用解决办法。
Electron当初利用的我的项目还是挺多的,是前端同学跨足桌面软件开发畛域的又一里程碑,不过须要转换一下思维模式,单纯写前端代码多是解决一些简略的界面逻辑和大量的数据,波及到文件、零碎操作、过程线程、原生交互方面的常识比拟少,能够多理解一下计算机操作系统方面的常识、把握代码设计模式和一些根本的算法优化方面的常识能让你更加胜任Electron桌面软件开发工作!