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 倍效能的同時,也把邏輯性錯誤解決了。
原來的:
1 2 |
var result = 某陣列.reduce((prev, next) => Object.assign({}, prev, {[next.position]: {v: next.v}}), {}); |
改為:
1 2 |
var result = 某陣列.forEach((v, i) => data[v.position]= {v: v.v}) |
實踐是檢驗真理的唯一標準
在理解上述知識後,下面就談談在該專案實踐中總結出來的技巧、難點和重點。
CSS、JavaScript 和 Electron 相關的知識和技巧
高亮 table 的列
Excel 單元格採用 table
標籤展示。在 Excel 中,被選中的單元格會高亮相應的『行』和『列』,以提醒使用者。在該應用中也有做相應的處理,橫向高亮採用 tr:hover
實現,而縱向呢?這裡所採用的一個技巧是:
假設 HTML 結構如下:
1 2 3 4 5 |
div.container table tr td |
CSS 程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 |
.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 進位制轉換』演算法來實現。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 將傳入的自然數轉換為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 } |
1 2 3 4 5 6 7 8 9 10 11 12 |
// 將傳入的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 顯式地設定複製貼上等編輯功能的選單欄,併為此設定相應的快捷鍵。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
// 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
)的,那麼可指定視窗的背景顏色與其一致,以避免渲染後的突變。
1 2 3 4 5 |
mainWindow = new BrowserWindow({ title: 'XCel', backgroundColor: '#f5f5f5', }; |
對於第二點,由於 Electron 本質是一個瀏覽器,需要載入非網頁部分的資源。因此,我們可以先隱藏視窗。
1 2 3 4 5 |
var mainWindow = new BrowserWindow({ title: 'ElectronApp', show: false, }; |
等到渲染程式開始渲染頁面的那一刻,在 ready-to-show
的回撥函式中顯示視窗。
1 2 3 4 5 |
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 模組:該模組為渲染程式和主程式之間提供了快捷的通訊方式。
對於第二種方式,在渲染程式中,執行以下程式碼即可:
1 2 3 4 5 6 7 8 9 10 |
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 問題。
1 2 3 4 5 |
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 的程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
//① ipcRenderer.send('filter-start', { filterTagList: this.filterTagList, filterWay: this.filterWay, curActiveSheetName: this.activeSheet.name }) // ⑥ 在某處接收 filter-response 事件 ipcRenderer.on("filter-response", (arg) => { // 得到處理資料 }) |
2、作為中轉站的主程式的程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 |
//② 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 的程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 |
// ③ 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 在產品規劃、介面設計和優化上的強力支援。全文完!
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式