XCEL 是由凹凸實驗室推出的一個 Excel 數據清洗工具,其通過可視化的方式讓用戶輕松地對 Excel 數據進行篩選。
XCEL 基於 Electron 和 Vue 2.x,它不僅跨平台(windows 7+、Mac 和 Linux),而且充分利用 Electron 多進程任務處理等功能,使其性能優異。
落地頁:https://xcel.aotu.io/ ✨✨✨
項目地址:https://github.com/o2team/xcel ✨✨✨
項目背景
用戶研究的定量研究和輕量級數據處理中,均需對數據進行清洗處理,以剔除異常數據,保證數據結果的信度和效度。目前因調研數據和輕量級數據的多變性,對輕量級數據清洗往往采取人工清洗,缺少統一、標准的清洗流程,但對於調研和輕量級的數據往往是需要保證數據穩定性的,因此,在對數據進行清洗時最好有標准化的清洗方式。
特性一覽
- 基於 Electron 研發並打包成為原生應用,用戶體驗良好;
- 可視化操作 Excel 數據,支持文件的導入導出;
- 擁有單列運算邏輯、多列運算邏輯和雙列范圍邏輯三種篩選方式,並且可通過“且”、“或”和“編組”的方式任意組合。
思路與實現
基於用研組的需求,利用 Electron 和 Vue 的特性對該工具進行開發。
技術選型
- Electron:桌面端跨平台框架,為 Web 提供了原生接口的權限。打包后的程序兼容 Windows 7 及以上、Mac、Linux 的 32 / 64 位系統。詳情>>
- Vue 全家桶:Vue 擁有數據驅動視圖的特性,適合重數據交互的應用。詳情>>
- js-xlsx:兼容各種電子表格格式的解析器和生成器。純 JavaScript 實現,適用於 Node.js 和 Web 前端。詳情>>
實現思路
- 通過 js-xlsx 將 Excel 文件解析為 JSON 數據
- 根據篩選條件對 JSON 數據進行篩選過濾
- 將過濾后的 JSON 數據轉換成 js-xlsx 指定的數據結構
- 利用 js-xlsx 對轉換后的數據生成 Excel 文件
紙上得來終覺淺,絕知此事要躬行
相關技術
如果對某項技術比較熟悉,則可略讀/跳過。
Electron
Electron 是什么?
Electron 是一個可以用 JavaScript、HTML 和 CSS 構建桌面應用程序的庫。這些應用程序能打包到 Mac、Windows 和 Linux 系統上運行,也能上架到 Mac 和 Windows 的 App Store。
- JavaScript、HTML 和 CSS 都是 Web 語言,它們是組成網站的一部分,瀏覽器(如 Chrome)懂得如何將這些代碼轉為可視化圖像。
- Electron 是一個庫:Electron 對底層代碼進行抽象和封裝,讓開發者能在此之上構建項目。
為什么它如此重要?
通常來說,每個操作系統的桌面應用都由各自的原生語言進行編寫,這意味着需要 3 個團隊分別為該應用編寫相應版本。而 Electron 則允許你用 Web 語言編寫一次即可。
- 原生(操作系統)語言:用於開發主流操作系統應用的原生語言的對應關系(大多數情況下):Mac 對應 Objective C、Linux 對應 C、Windows 對應 C++。
它由什么組成?
Electron 結合了 Chromium、Node.js 和用於調用操作系統本地功能的 API(如打開文件窗口、通知、圖標等)。
- Chromium:Google 創造的一個開源庫,並用於 Google 的瀏覽器 Chrome。
- Node.js(Node):一個在服務器運行 JavaScript 的運行時(runtime),它擁有訪問文件系統和網絡權限(你的電腦也可以是一台服務器!)。
開發體驗如何?
基於 Electron 的開發就像在開發網頁,而且能夠無縫地 使用 Node。或者說:在構建一個 Node 應用的同時,通過 HTML 和 CSS 構建界面。另外,你只需為一個瀏覽器(最新的 Chrome)進行設計(即無需考慮兼容性等)。
- 使用 Node:這還不是全部!除了完整的 Node API,你還可以使用托管在 npm 上超過 350,000 個的模塊。
- 一個瀏覽器:並非所有瀏覽器都提供一致的樣式,Web 設計師和開發者經常因此而不得不花費更多的精力,讓網站在不同瀏覽器上表現一致。
- 最新的 Chrome:可使用超過 90% 的 ES2015 特性和其它很酷的特性(如 CSS 變量)。
兩個進程(重點)
Electron 有兩種進程:『主進程』和『渲染進程』。部分模塊只能在兩者之一上運行,而有些則無限制。主進程更多地充當幕后角色,而渲染進程則是應用程序的各個窗口。
注:可通過任務管理器(PC)/活動監視器(Mac)查看進程的相關信息。
- 模塊:Electron 的 API 是根據它們的用途進行分組。例如:
dialog
模塊擁有所有原生 dialog 的 API,如打開文件、保存文件和警告等彈窗。
主進程
主進程,通常是一個命名為 main.js
的文件,該文件是每個 Electron 應用的入口。它控制了應用的生命周期(從打開到關閉)。它既能調用原生元素,也能創建新的(多個)渲染進程。另外,Node API 是內置其中的。
- 調用原生元素:打開 diglog 和其它操作系統的交互均是資源密集型操作(注:出於安全考慮,渲染進程是不能直接訪問本地資源的),因此都需要在主進程完成。
渲染進程
渲染進程是應用的一個瀏覽器窗口。與主進程不同,它能存在多個(注:一個 Electron 應用只能存在一個主進程)並且相互獨立(它也能是隱藏的)。主窗口通常被命名為 index.html
。它們就像典型的 HTML 文件,但 Electron 賦予了它們完整的 Node API。因此,這也是它與瀏覽器的區別。
- 相互獨立:每個渲染進程都是獨立的,這意味着某個渲染進程的崩潰,也不會影響其余渲染進程。
- 隱藏:可隱藏窗口,然后讓其在背后運行代碼(👍)。
把它們想象成這樣
Chrome(或其他瀏覽器)的每個標簽頁(tab)及其頁面,就好比 Electron 中的一個單獨渲染進程。即使關閉所有標簽頁,Chrome 依然存在。這好比 Electron 的主進程,能打開新的窗口或關閉這個應用。
注:在 Chrome 瀏覽器中,一個標簽頁(tab)中的頁面(即除了瀏覽器本身部分,如搜索框、工具欄等)就是一個渲染進程。
相互通訊
由於主進程和渲染進程各自負責不同的任務,而對於需要協同完成的任務,它們需要相互通訊。IPC就為此而生,它提供了進程間的通訊。但它只能在主進程與渲染進程之間傳遞信息(即渲染進程之間不能進行直接通訊)。
- IPC:主進程和渲染進程各自擁有一個 IPC 模塊。
匯成一句話
Electron 應用就像 Node 應用,它也依賴一個 package.json
文件。該文件定義了哪個文件作為主進程,並因此讓 Electron 知道從何啟動應用。然后主進程能創建渲染進程,並能使用 IPC 讓兩者間進行消息傳遞。
至此,Electron 的基礎部分介紹完畢。該部分是基於筆者之前翻譯的一篇文章《Essential Electron》,譯文可點擊 這里。
Vue 全家桶
該工具使用了 Vue、Vuex、Vuex-router。在工具基本定型階段,由 1.x 升級到了 2.x。
為什么選擇 Vue
對於筆者來說:
- 簡單易用,一般使用只需看官方文檔。
- 數據驅動視圖,所以基本不用操作 DOM 了。
- 框架的存在是為了幫助我們應對復雜度。
- 全家桶的好處是:對於一般場景,我們就不需要考慮用哪些個庫(插件)。
Vue 1.x -> Vue 2.0 的版本遷移用 vue-migration-helper 即可分析出大部分需要更改的地方。
網上已有很多關於 Vue 的教程,故在此不再贅述。至此,Vue 部分介紹完畢。
js-xlsx
該庫支持各種電子表格格式的解析與生成。它由 JavaScript 實現,適用於前端和 Node。詳情>>
目前支持讀入的格式有(不斷更新):
- Excel 2007+ XML Formats (XLSX/XLSM)
- Excel 2007+ Binary Format (XLSB)
- Excel 2003-2004 XML Format (XML "SpreadsheetML")
- Excel 97-2004 (XLS BIFF8)
- Excel 5.0/95 (XLS BIFF5)
- OpenDocument Spreadsheet (ODS)
支持寫出的格式有:
- XLSX
- CSV (and general DSV)
- JSON and JS objects (various styles)
目前該庫提供的 sheet_to_json
方法能將讀入的 Excel 數據轉為 JSON 格式。而對於導出操作,我們需要為 js-xlsx 提供指定的 JSON 格式。
更多關於 Excel 在 JavaScript 中處理的知識可查看凹凸實驗室的《Node讀寫Excel文件探究實踐》。但該文章存在兩處問題(均在 js-xlsx 實戰的導出表格部分):
- 生成頭部時,Excel 的列信息簡單地通過
String.fromCharCode(65+j)
生成。當列大於 26 時會出現問題。這個問題會在后面章節中給出解決方案; - 轉換成 worksheet 需要的結構處,出現邏輯性錯誤,並且會導致嚴重的性能問題。邏輯問題在此不講述,我們看看性能問題:
隨着 ECMAScript 的不斷更新,JavaScript 變得更加強大和易用。盡管如此,我們還是要做到『物盡所用』,而不要『大材小用』,否則可能會得到“反效果”。這里導致性能問題的正是 Object.assign() 方法,該方法可以把任意多個源對象的可枚舉屬性拷貝至目標對象,並返回目標對象。由於該方法自身的實現機制,會在此案例中產生大量的冗余操作。在該案例中,單元格信息是唯一的,所以直接通過 forEach 為一個空對象賦值即可。提升 N 倍性能的同時,也把邏輯性錯誤解決了。
原來的:
var result = 某數組.reduce((prev, next) => Object.assign({}, prev, {[next.position]: {v: next.v}}), {});
改為:
var result = 某數組.forEach((v, i) => data[v.position]= {v: v.v})
實踐是檢驗真理的唯一標准
在理解上述知識后,下面就談談在該項目實踐中總結出來的技巧、難點和重點。
CSS、JavaScript 和 Electron 相關的知識和技巧
高亮 table 的列
Excel 單元格采用 table
標簽展示。在 Excel 中,被選中的單元格會高亮相應的『行』和『列』,以提醒用戶。在該應用中也有做相應的處理,橫向高亮采用 tr:hover
實現,而縱向呢?這里所采用的一個技巧是:
假設 HTML 結構如下:
div.container
table
tr
td
CSS 代碼如下:
.container { overflow:hidden; }
td { position: relative; }
td:hover::after {
position: absolute;
left: 0;
right: 0;
top: -1個億px; // 小目標達成,不過是負的😭
bottom: -1個億px;
z-index: -1; // 避免遮住自身和同列 td 的內容、border 等
}
斜分割線
如圖:
分割線可以通過 ::after/::before
偽類元素實現一條直線,然后通過 transform:rotate();
旋轉特定角度實現。但這種實現的一個問題是:由於寬度是不定的,因此需要通過 JavaScript 運算才能得到准確的對角分割線。
因此,這里可以通過 CSS 線性漸變 linear-gradient(to top right, transparent, transparent calc(50% - .5px), #d3d6db calc(50% - .5px), #d3d6db calc(50% + .5px), transparent calc(50% + .5px))
實現。無論寬高如何變,依然妥妥地自適應。
Excel 的列轉換
- Excel 的列需要用『字母』表示,但不能簡單地通過 String.fromCharCode() 實現,因為當超出
26 列
時就會產生問題(如:第27
列,String.fromCharCode(65+26)
得到的是[
,而不是AA
)。因此,這需要通過『十進制和 26 進制轉換』算法來實現。
// 將傳入的自然數轉換為26進制表示。映射關系:[0-25] -> [A-Z]。
function getCharCol(n) {
let temCol = '',
s = '',
m = 0
while (n >= 0) {
m = n % 26 + 1
s = String.fromCharCode(m + 64) + s
n = (n - m) / 26
}
return s
}
// 將傳入的26進制轉換為自然數。映射關系:[A-Z] ->[0-25]。
function getNumCol(s) {
if (!s) return 0
let n = 0
for (let i = s.length - 1, j = 1; i >= 0; i--, j *= 26) {
let c = s[i].toUpperCase()
if (c < 'A' || c > 'Z') return 0
n += (c.charCodeAt() - 64) * j
}
return n - 1
}
為 DOM 的 File 對象增加了 path 屬性
Electron 為 File 對象額外增了 path 屬性,該屬性可得到文件在文件系統上的真實路徑。因此,你可以利用 Node 為所欲為😈。應用場景有:拖拽文件后,通過 Node 提供的 File API 讀取文件等。
支持常見的編輯功能,如粘貼和復制
Electron 應用在 MacOS 中默認不支持『復制』『粘貼』等常見編輯功能,因此需要為 MacOS 顯式地設置復制粘貼等編輯功能的菜單欄,並為此設置相應的快捷鍵。
// darwin 就是 MacOS
if (process.platform === 'darwin') {
var template = [{
label: 'FromScratch',
submenu: [{
label: 'Quit',
accelerator: 'CmdOrCtrl+Q',
click: function() { app.quit(); }
}]
}, {
label: 'Edit',
submenu: [{
label: 'Undo',
accelerator: 'CmdOrCtrl+Z',
selector: 'undo:'
}, {
label: 'Redo',
accelerator: 'Shift+CmdOrCtrl+Z',
selector: 'redo:'
}, {
type: 'separator'
}, {
label: 'Cut',
accelerator: 'CmdOrCtrl+X',
selector: 'cut:'
}, {
label: 'Copy',
accelerator: 'CmdOrCtrl+C',
selector: 'copy:'
}, {
label: 'Paste',
accelerator: 'CmdOrCtrl+V',
selector: 'paste:'
}, {
label: 'Select All',
accelerator: 'CmdOrCtrl+A',
selector: 'selectAll:'
}]
}];
var osxMenu = menu.buildFromTemplate(template);
menu.setApplicationMenu(osxMenu);
}
更貼近原生應用
Electron 的一個缺點是:即使你的應用是一個簡單的時鍾,但它也不得不包含完整的基礎設施(如 Chromium、Node 等)。因此,一般情況下,打包后的程序至少會達到幾十兆(根據系統類型進行浮動)。當你的應用越復雜,就越可以忽略文件體積問題。
眾所周知,頁面的渲染難免會導致『白屏』,而且這里采用了 Vue 這類框架,情況就更加糟糕了。另外,Electron 應用也避免不了『先打開瀏覽器,再渲染頁面』的步驟。下面提供幾種方法來減輕這種情況,以讓程序更貼近原生應用。
- 指定 BrowserWindow 的背景顏色;
- 先隱藏窗口,直到頁面加載后再顯示;
- 保存窗口的尺寸和位置,以讓程序下次被打開時,依然保留的同樣大小和出現在同樣的位置上。
對於第一點,若應用的背景不是純白(#fff
)的,那么可指定窗口的背景顏色與其一致,以避免渲染后的突變。
mainWindow = new BrowserWindow({
title: 'XCel',
backgroundColor: '#f5f5f5',
};
對於第二點,由於 Electron 本質是一個瀏覽器,需要加載非網頁部分的資源。因此,我們可以先隱藏窗口。
var mainWindow = new BrowserWindow({
title: 'ElectronApp',
show: false,
};
等到渲染進程開始渲染頁面的那一刻,在 ready-to-show
的回調函數中顯示窗口。
mainWindow.on('ready-to-show', function() {
mainWindow.show();
mainWindow.focus();
});
對於第三點,筆者並沒有實現,原因如下:
- 用戶一般是根據當時的情況對程序的尺寸和位置進行調整,即視情況而定。
- 以上是我個人臆測,主要是我懶🐶。
其實現方式,可參考《4 must-know tips for building cross platform Electron apps》。
如何在渲染進程調用原生彈框?
在渲染進程中調用原本專屬於主進程中的 API (如彈框)的方式有兩種:
- IPC 通訊模塊:先在主進程通過
ipcMain
進行監聽,然后在渲染進程通過ipcRenderer
進行觸發; - remote 模塊:該模塊為渲染進程和主進程之間提供了快捷的通訊方式。
對於第二種方式,在渲染進程中,運行以下代碼即可:
const remote = require('electron').remote
remote.dialog.showMessageBox({
type: 'question',
buttons: ['不告訴你', '沒有夢想'],
defaultId: 0,
title: 'XCel',
message: '你的夢想是什么?'
}
自動更新
如果 Electron 應用沒有提供自動更新功能,那么就意味着用戶想體驗新開發的功能或用上修復 Bug 后的新版本,只能靠用戶自己主動地去官網下載,這無疑是糟糕的體驗。Electron 提供的 autoUpdater 模塊可實現自動更新功能,該模塊提供了第三方框架 Squirrel 的接口,但 Electron 目前只內置了 Squirrel.Mac,且它與 Squirrel.Windows(需要額外引入)的處理方式也不一致(在客戶端與服務器端兩方面)。因此如果對該模塊不熟悉,處理起來會相對比較繁瑣。具體可以參考筆者的另一篇譯文《Electron 自動更新的完整教程(Windows 和 OSX)》。
目前 Electron 的 autoUpdater 模塊不支持 Linux 系統。
另外,XCel 目前並沒有采用 autoUpdater 模塊實現自動更新功能,而是利用 Electron 的 DownloadItem 模塊實現,而服務器端則采用了 Nuts。
為 Electron 應用生成 Windows 安裝包
通過 electron-builder 可直接生成常見的 MacOS 安裝包,但它生成的 Windows 的安裝包卻略顯簡潔(默認選項時)。
Mac 常見的安裝模式,將“左側的應用圖標”拖拽到“右側的 Applications”即可
通過 electron-builder 生成的 Windows 安裝包與我們在 Windows 上常見的軟件安裝界面不太一樣,它沒有安裝向導和點擊“下一步”的按鈕,只有一個安裝時的 gif 動畫(默認的 gif 動畫如下圖,當然你也可以指定特定的 gif 動畫),因此也就關閉了用戶選擇安裝路徑等權利。
Windows 安裝時 默認顯示的 gif 動畫
如果你想為打包后的 Electron 應用(即通過 electron-packager/electron-builder 生成的,可直接運行的程序目錄)生成擁有點擊“下一步”按鈕和可讓用戶指定安裝路徑的常見安裝包,可以嘗試 NSIS 程序,具體可看這篇教程 《[教學]只要10分鐘學會使用 NSIS 包裝您的桌面軟體–安裝程式打包。完全免費。》。
注:electron-builder 也提供了生成安裝包的配置項,具體查看>>。
NSIS(Nullsoft Scriptable Install System)是一個開源的 Windows 系統下安裝程序制作程序。它提供了安裝、卸載、系統設置、文件解壓縮等功能。正如其名字所描述的那樣,NSIS 是通過它的腳本語言來描述安裝程序的行為和邏輯的。NSIS 的腳本語言和常見的編程語言有類似的結構和語法,但它是為安裝程序這類應用所設計的。
至此,CSS、JavaScript 和 Electron 相關的知識和技巧部分闡述完畢。
性能優化
下面談談『性能優化』,這部分涉及到運行效率和內存占用量。
注:以下內容均基於 Excel 樣例文件(數據量為:1913 行 x 180 列)得出的結論。
執行效率和渲染的優化
Vue 性能真的好?
Vue 一直標榜着自己性能優異,但當數據量上升到一定量級時(如 1913 x 180 ≈ 34 萬個數據單元),會出現嚴重的性能問題(未做相應優化的前提下)。
如直接通過列表渲染 v-for
渲染數據時,會導致程序卡死。
答:通過查閱相關資料可得, v-for
在初次渲染時,需要對每個子項進行初始化(如數據綁定等操作,以便擁有更快的更新速度),這對於數據量較大時,無疑會造成嚴重的性能問題。
當時,我想到了兩種解決思路:
- Vue 是數據驅動視圖的,對數據分段 push,即將一個龐大的任務分割為 N 份。
- 自己拼接 HTML 字符串,再通過 innerHTML 一次性插入。
最終,我選擇了第二條,理由是:
- 性能最佳,因為每次執行數據過濾時,Vue 都要進行 diff,性能不佳。
- 更符合當前應用的需求:純展示且無需動畫過渡等。
- 實現更簡單
將原本繁重的 DOM 操作(Vue)轉換為 JavaScript 的拼接字符串后,性能得到了很大提升(不會導致程序卡死而渲染不出視圖)。這種優化方式難道不就是 Vue、React 等框架解決的問題之一嗎?只不過框架考慮的場景更廣,有些地方需要我們自己根據實際情況進行優化而已。
在瀏覽器當中,JavaScript 的運算在現代的引擎中非常快,但 DOM 本身是非常緩慢的東西。當你調用原生 DOM API 的時候,瀏覽器需要在 JavaScript 引擎的語境下去接觸原生的 DOM 的實現,這個過程有相當的性能損耗。所以,本質的考量是,要把耗費時間的操作盡量放在純粹的計算中去做,保證最后計算出來的需要實際接觸真實 DOM 的操作是最少的。 —— 《Vue 2.0——漸進式前端解決方案》
當然,由於 JavaScript 天生單線程,即使執行數速度再快,也難免會導致頁面有短暫的時間拒絕用戶的輸入。此時可通過 Web Worker 或其它方式解決,這也將是我們后續講到的問題。
也有網友提供了優化大量列表的方法:https://clusterize.js.org/。但在此案例中筆者並沒有采用此方式。
強大的 GPU 加速
將拼接的字符串插入 DOM 后,出現了另外一個問題:滾動會很卡。猜想這是渲染問題,畢竟 34 萬個單元格同時存在於界面中。
添加 transform: translate3d(0, 0, 0) / translateZ(0)
屬性啟動 GPU 渲染,即可解決這個渲染性能問題。再次感嘆該屬性的強大。🐂
后來,考慮到用戶並不需要查看全部數據,只需展示部分數據讓用戶進行參考即可。我們對此只渲染前 30/50 行數據。這樣即可提升用戶體驗,也能進一步優化性能。
記得關閉 Vuex 的嚴格模式
另外,由於自己學藝不精和粗心大意,忘記在生產環境關閉 Vuex 的『嚴格模式』。
Vuex 的嚴格模式要在生產環境中關閉,否則會對 state 樹進行一個深觀察 (deep watch),產生不必要的性能損耗。也許在數據量少時,不會注意到這個問題。
還原當時的場景:導入 Excel 數據后,再進行交互(涉及 Vuex 的讀寫操作),需要等幾秒才會響應,而直接通過純 DOM 監聽的事件則無此問題。由此,判斷出是 Vuex 問題。
const store = new Vuex.Store({
// ...
strict: process.env.NODE_ENV !== 'production'
})
多進程!!!
前面說道,JavaScript 天生單線程,即使再快,對於數據量較大時,也會出現拒絕響應的問題。因此需要 Web Worker 或類似的方案去解決。
在這里我不選擇 Web worker 的原因有如下幾點:
- 有其它更好的替代方案:一個主進程能創建多個渲染進程,通過 IPC 即可進行數據交互;
- Electron 不支持 Web Worker!(當然,可能會在新版本支持,最新信息請關注官方)
Electron 作者在 2014.11.7 在《state of web worker support?》 issue 中回復了以下這一段:
Node integration doesn't work in web workers, and there is no plan to do. Workers in Chromium are implemented by starting a new thread, and Node is not thread safe. Back in past we had tried to add node integration to web workers in Atom, but it crashed too easily so we gave up on it.
因此,我們最終采用了創建一個新的渲染進程 background process
進行處理數據。由 Electron 章節可知,每個 Electron 渲染進程是獨立的,因此它們不會互相影響。但這也帶來了一個問題:它們不能相互通訊?
錯!下面有 3 種方式進行通訊:
- Storage API:對某個標簽頁的 localStorage/sessionStorage 對象進行增刪改時,其他標簽頁能通過 window.storage 事件監聽到。
- IndexedDB:IndexedDB 是一個為了能夠在客戶端存儲可觀數量的結構化數據,並且在這些數據上使用索引進行高性能檢索的 API。
- 通過主進程作為中轉站:設主界面的渲染進程是 A,
background process
是 B,那么 A 先將 Excel 數據傳遞到主進程,然后主進程再轉發到 B。B 處理完后再原路返回,具體如下圖。當然,也可以將數據存儲在主進程中,然后在多個渲染進程中使用 remote 模塊來訪問它。
該工具采用了第三種方式的第一種情況:
1、主頁面渲染進程 A 的代碼如下:
//①
ipcRenderer.send('filter-start', {
filterTagList: this.filterTagList,
filterWay: this.filterWay,
curActiveSheetName: this.activeSheet.name
})
// ⑥ 在某處接收 filter-response 事件
ipcRenderer.on("filter-response", (arg) => {
// 得到處理數據
})
2、作為中轉站的主進程的代碼如下:
//②
ipcMain.on("filter-start", (event, arg) => {
// webContents 用於渲染和控制 web page
backgroundWindow.webContents.send("filter-start", arg)
})
// ⑤ 用於接收返回事件
ipcMain.on("filter-response", (event, arg) => {
mainWindow.webContents.send("filter-response", arg)
})
3、處理繁重數據的 background process
渲染進程 B 的代碼如下:
// ③
ipcRenderer.on('filter-start', (event, arg) => {
// 進行運算
...
// ④ 運算完畢后,再通過 IPC 原路返回。主進程和渲染進程 A 也要建立相應的監聽事件
ipcRenderer.send('filter-response', {
filRow: tempFilRow
})
})
至此,我們將『讀取文件』、『過濾數據』和『導出文件』三大耗時的數據操作均轉移到了 background process
中處理。
這里,我們只創建了一個 background process
,如果想要做得更極致,我們可以新建『CPU 線程數- 1 』 個的 background process
同時對數據進行處理,然后在主進程對處理后數據進行拼接,最后再將拼接后的數據返回到主頁面的渲染進程。這樣就可以充分榨干 CPU 了。當然,在此筆者不會進行這個優化。
不要為了優化而優化,否則得不償失。 —— 某網友
內存占有量過大
解決了執行效率和渲染問題后,發現也存在內存占用量過大的問題。當時猜測是以下幾個原因:
- 三大耗時操作均放置在
background process
處理。在通訊傳遞數據的過程中,由於不是共享內存(因為 IPC 是基於 Socket 的),導致出現多份數據副本(在寫這篇文章時才有了這相對確切的答案)。 - Vuex 是以一個全局單例的模式進行管理,但它會是不是對數據做了某些封裝,而導致性能的損耗呢?
- 由於 JavaScript 目前不具有主動回收資源的能力,所以只能主動對閑置對象設置為
null
,然后等待 GC 回收。
由於 Chromium 采用多進程架構,因此會涉及到進程間通信問題。Browser 進程在啟動 Render 進程的過程中會建立一個以 UNIX Socket 為基礎的 IPC 通道。有了 IPC 通道之后,接下來 Browser 進程與 Render 進程就以消息的形式進行通信。我們將這種消息稱為 IPC 消息,以區別於線程消息循環中的消息。
——《Chromium的IPC消息發送、接收和分發機制分析》
定義:為了易於理解,以下『Excel 數據』均指 Excel 的全部有效單元格轉為 JSON 格式后的數據。
最容易處理的無疑是第三點,手動將不再需要的變量及時設置為 null
,但效果並不明顯。
后來,通過操作系統的『活動監視器』(Windows 上是任務管理器)對該工具的每階段(打開時、導入文件時、篩選時和導出時)進行粗略的內存分析,得到以下報告:
---------------- S:報告分割線 ----------------
經觀察,主要耗內存的是頁面渲染進程。下面通過截圖說明:
PID 15243
是主進程
PID 15246
是頁面渲染進程
PID 15248
是 background 渲染進程
a、首次啟動程序時(第 4 行是主進程;第 1 行是頁面渲染進程;第 3 行是 background 渲染進程 )
b、導入文件(第 5 行是主進程;第 2 行是頁面渲染進程;第 4 行是 background 渲染進程 )
c、篩選數據(第 4 行是主進程;第 1 行是頁面渲染進程;第 3 行是 background 渲染進程 )
由於 JavaScript 目前不具有主動回收資源的功能,所以只能主動將對象設置為 null
,然后等待 GC 回收。
因此,經過一段時間等待后,內存占用如下:
d、一段時間后(第 4 行是主進程;第 1 行是頁面渲染進程;第 3 行是 background 渲染進程 )
由上述可得,頁面渲染進程由於頁面元素和 Vue 等 UI 相關資源是固定的,占用內存較大且不能回收。主進程占用資源也不能得到很好釋放,暫時不知道原因,而 background 渲染進程則較好地釋放資源。
---------------- E:報告分割線 ----------------
根據報告,初步得出的結論是 Vue 和通訊時占用資源較大。
根據該工具的實際應用場景:Excel 數據只在『導入』和『過濾后』兩個階段需要展示,而且展示的是通過 JavaScript 拼接的 HTML 字符串所構成的 DOM 而已。因此將表格數據放置在 Vuex 中,有點濫用資源的嫌疑。
另外,在 background process
中也有存有一份 Excel 數據副本。因此,索性只在 background process
存儲一份 Excel 數據,然后每當數據變化時,通過 IPC 讓 background process
返回拼接好的 HTML 字符串即可。這樣一來,內存占有量立刻下降許多。另外,這也是一個一舉多得的優化:
- 字符串拼接操作也轉移到了
background process
,頁面渲染進程進一步減少耗時的操作; - 內存占有量大大減小,響應速度也得到了提升。
其實,這也有點像 Vuex 的『全局單例模式管理』,一份數據就好。
當然,對於 Excel 的基本信息,如行列數、SheetName、標題組等均依然保存在 Vuex。
優化后的內存占有量如下圖。與上述報告的第三張圖相比(同一階段),內存占有量下降了 44.419%:
另外,對於不需要響應的數據,可通過 Object.freeze()
凍結起來。這也是一種優化手段。但該工具目前並沒有應用到。
至此,優化部分也闡述完畢了!
該工具目前是開源的,歡迎大家使用或推薦給用研組等有需要的人。
你們的反饋(可提交 issues / pull request)能讓這個工具在使用和功能上不斷完善。
最后,感謝 LV 在產品規划、界面設計和優化上的強力支持。全文完!