>> 博客原文連接javascript
上一篇文章《基於Electron的smb客戶端開發記錄》,大體描述了整個SMB客戶端開發的核心功能、實現難點、項目打包這些內容,這篇文章呢單獨把其中的文件分片上傳模塊
拿出來進行分享,說起一些與Electron主進程、渲染進程和文件上傳優化相關的功能點。html
項目精簡版 DEMO地址,刪除了smb處理的多餘邏輯,使用文件複製模擬上傳流程,可直接運行體驗。前端
demo運行時須要分別開啓兩個開發環境view -> service,而後才能預覽界面,因爲沒有後端,文件默認上傳(複製)到electron數據目錄(在Ubuntu上是
~/.config/FileSliceUpload/runtime/upload
)
# 進入view目錄 $: npm install $: npm start # 進入service目錄 $: npm install $: npm start
Electron 運行 package.json 的 main 腳本的進程被稱爲主進程。在主進程中運行的腳本經過建立web頁面來展現用戶界面,一個 Electron 應用老是有且只有一個主進程。
主進程使用 BrowserWindow 實例建立頁面,每一個 BrowserWindow 實例都在本身的渲染進程裏運行頁面,當一個 BrowserWindow 實例被銷燬後,相應的渲染進程也會被終止。
主進程管理全部的web頁面和它們對應的渲染進程,每一個渲染進程都是獨立的,它只關心它所運行的 web 頁面。 java
在普通的瀏覽器中,web頁面一般在沙盒環境中運行,而且沒法訪問操做系統的原生資源。 然而 Electron 的用戶在 Node.js 的 API 支持下能夠在頁面中和操做系統進行一些底層交互。
在頁面中調用與 GUI 相關的原生 API 是不被容許的,由於在 web 頁面裏操做原生的 GUI 資源是很是危險的,並且容易形成資源泄露。 若是你想在 web 頁面裏使用 GUI 操做,其對應的渲染進程必須與主進程進行通信,請求主進程進行相關的 GUI 操做。node
1/2-自帶方法,3-外部擴展方法
1.使用remote遠程調用react
remote模塊爲渲染進程和主進程通訊提供了一種簡單方法,使用remote模塊, 你能夠調用main進程對象的方法, 而沒必要顯式發送進程間消息。示例以下,代碼經過remote遠程調用主進程的BrowserWindows建立了一個渲染進程,並加載了一個網頁地址:git
/* 渲染進程中(web端代碼) */ const { BrowserWindow } = require('electron').remote let win = new BrowserWindow({ width: 800, height: 600 }) win.loadURL('https://github.com')
注意:remote底層是基於ipc的同步進程通訊(同步=阻塞頁面),都知道Node.js的最大特性就是異步調用,非阻塞IO,所以remote調用不適用於主進程和渲染進程頻繁通訊以及耗時請求的狀況,不然會引發嚴重的程序性能問題。github
2.使用ipc信號通訊web
基於事件觸發的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進程之間任意發送消息,能夠參考如下示例代碼:
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(); // 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' }; }); });
const { ipcRenderer } = require('electron'); const { MessageChannel } = require('electron-re'); // listen a channel, same as ipcRenderer.on MessageChannel.on('channel1', (event, result) => { console.log(result); }); // handle a channel signal, just like ipcMain.handle MessageChannel.handle('channel2', (event, result) => { console.log(result); return { response: 'channel2-response' } }); // send data to another service and return a promise , just like ipcRenderer.invoke MessageChannel.invoke('app2', 'channel3', { value: 'channel3' }).then((event, result) => { console.log(result); }); // send data to a service - like the build-in ipcRenderer.send MessageChannel.send('app', 'channel4', { value: 'channel4' });
// handle a channel signal, just like ipcMain.handle MessageChannel.handle('channel3', (event, result) => { console.log(result); return { response: 'channel3-response' } }); // listen a channel, same as ipcRenderer.once MessageChannel.once('channel4', (event, result) => { console.log(result); }); // send data to main process, just like ipcRenderer.send MessageChannel.send('main', 'channel3', { value: 'channel3' }); // send data to main process and return a Promise, just like ipcRenderer.invoke MessageChannel.invoke('main', 'channel4', { value: 'channel4' });
const { ipcRenderer } = require('electron'); const { MessageChannel } = require('electron-re'); // send data to a service MessageChannel.send('app', ....); MessageChannel.invoke('app2', ....); // send data to main process MessageChannel.send('main', ....); MessageChannel.invoke('main', ....);
文件上傳主要邏輯控制部分是前端的JS腳本代碼,位於主窗口所在的render渲染進程,負責用戶獲取系統目錄文件、生成上傳任務隊列、動態展現上傳任務列表詳情、任務列表的增刪查改等;主進程Electron端的Node.js代碼主要負責響應render進程的控制命令進行文件上傳任務隊列數據的增刪查改、上傳任務在內存和磁盤的同步、文件系統的交互、系統原生組件調用等。
Input
組件獲取到的FileList(HTML5 API,用於web端的簡單文件操做)即爲上傳源;\\[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 }); }); }); }
下圖描述了整個前端部分的控制邏輯:
<Input />
組件拿到FileList對象(Electron環境下拿到的File對象會額外附加一個path
屬性指明文件位於系統的絕對路徑)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 } }
優化是一件頭大的事兒,由於你須要先經過不少測試手法找到現有代碼的性能瓶頸,而後編寫優化解決方案。我以爲找到性能瓶頸這一點就特別難,由於是本身寫的代碼因此容易陷入一些先入爲主的刻板思考模式。不過最最主要的一點仍是你若是本身都弄不清楚你使用的技術棧的話,那就無從談起優化,因此前面有很大篇幅分析了Electron進程方面的知識以及梳理了整個上傳流程。
在文件上傳過程當中打開性能檢測工具Performance
進行錄製,分析整個流程:
在文件上傳過程當中打開內存工具Memory
進行快照截取分析一個時刻的內存佔用狀況:
在編寫完成文件上傳模塊後,初步進行了壓力測試,結果發現添加1000個文件上傳任務到任務隊列,且同時上傳的文件上傳任務數量爲6時,上下滑動查看文件上傳列表時出現了卡頓的狀況,這種卡頓不侷限於某個界面組件的卡頓,並且當前窗口的全部操做都卡了起來,初步懷疑是Antd Table組件引發的卡頓,由於Antd Table組件是個很複雜的高階組件,在處理大量的數據時可能會有性能問題,遂我將Antd Table組件換成了原生的table組件,且Table列表只顯示每一個上傳任務的任務名,其他的諸如上傳進度這些都不予顯示,從而想避開這個問題。使人吃驚的是測試結果是即便換用了原生Table組件,卡頓狀況仍然毫無改善!
先看下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鏈式調用。改動後發現卡頓狀況改善不明顯,遂進行了第三次嘗試。
此次是大改😕
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) {...} }
send
和sendToAll
方法向子進程發送消息,前者是向某個進程發送,若是請求參數指定了id則代表須要明確使用以前與此id創建過映射的某個進程,並指望拿到此進程的迴應結果;後者是向進程池中的全部進程發送信號,並指望拿到全部進程返回的結果(供調用者外部調用)。dataRespond
和dataRespondAll
方法對應上面的兩個信號發送方法的進程返回數據回調函數,前者拿到進程池中指定的某個進程的回調結果,後者拿到進程池中全部進程的回調結果(進程池內部方法,調用者無需關注)。getForkedFromPool
方法是從進程池中拿到一個進程,若是進程池尚未一個子進程或是已經建立的子進程數量小於設置的可建立子進程數最大值,那麼會優先新建立一個子進程放入進程池,而後返回這個子進程以供調用(進程池內部方法,調用者無需關注)。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
數據,簡略代碼以下:
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腳本代碼,在此腳本中咱們註冊多個任務來處理從進程池發送過來的消息。
這段代碼邏輯被單獨分離到子進程中處理,其中:
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()); }); }) } ...
refreshTasks
,主要邏輯是遍歷全部未經上傳文件原始對象數組,而後選取固定某個數量的文件(數量取決於設置的同時上傳任務個數)放入待上傳文件列表中,我發現若是待上傳文件列表的文件數量 = 設置的同時上傳任務個數
的狀況下就不用繼續遍歷剩下的文件原始對象數組了。就是少寫了這個判斷條件致使refreshTasks
這個頻繁操做的函數在每次執行時可能多執行數千遍for循環內層判斷邏輯(具體執行次數呈O(n)次增加,n爲當前任務列表任務數量)。
第一次把Electron技術應用到實際項目中,踩了挺多坑:render進程和主進程通訊的問題、跨平臺兼容的問題、多平臺打包的問題、窗口管理的問題... 總之得到了不少經驗,也整理出了一些通用解決方法。 Electron如今應用的項目仍是挺多的,是前端同窗跨足桌面軟件開發領域的又一里程碑,不過須要轉換一下思惟模式,單純寫前端代碼可能是處理一些簡單的界面邏輯和少許的數據,涉及到文件、系統操做、進程線程、原生交互方面的知識比較少,能夠多瞭解一下計算機操做系統方面的知識、掌握代碼設計模式和一些基本的算法優化方面的知識能讓你更加勝任Electron桌面軟件開發任務!