編者按:本內容源自葡萄城客戶——政採雲前端技術團隊。政採雲公司以全球領先的雲計算、大數據、人工智能等數字技術爲基礎,搭建了全國首個政府採購雲服務平臺——政採雲平臺,目前該平臺已成爲行業內服務範圍最廣、用戶數量最多、交易最活躍的跨區域、跨層級、跨領域的一體化採購雲服務平臺。
前言
數據可視化包含三個分支:科學可視化、信息可視化、可視分析。
1、科學可視化主要關注的是三維現象的可視化,如建築學、氣象學、醫學或生物學方面的各種系統。重點在於對體、面以及光源等等的逼真渲染,或許甚至還包括某種動態成分。
2、信息可視化是一種將數據與設計結合起來的圖片,有利於個人或組織簡短有效地向受衆傳播信息的數據表現形式。
3、可視分析學被定義爲由可視交互界面爲基礎的分析推理科學,將圖形學、數據挖掘、人機交互等技術融合在一起,形成人腦智能和機器智能優勢互補和相互提升。
可視化分析中可視化報表是重中之重,把大量的數據快速的展示出來,並且靈活的進行數據操作,其中操作包括數據的篩選、關聯、聯動、鑽取,文案的查詢,替換、樣式設置,條件格式的注入實現多色階、圖標集、數據條、重複值,以及公式的插入,跨表聯動等。SpreadJS 在解決可視化分析報表中最爲突出,下面我們只針對可視化分析中 SpreadJS 所扮演色做探討。
報表可視化難點
互聯網電商服務業行業,平時會處理大量商業信息和用戶信息,客服和數據分析師,是報表主要用戶人員。
客服平時每天都會處理大量的工單填報、客訴登記、第三方平臺原始數據的導入、統計彙總、審覈審批、電籤、分發等工作。平時大部分工作信息的載體都是 Excel,每天服務器需要處理海量的文檔,由於 Excel 文檔本身數據難以提取入庫,模板更新時也不方便第一時間分發到操作員處,難以整合到 Web 頁面裏等問題。
數據分析師需要拿到數據進行彙總,算出各個商品品牌的銷售額,最大值、最小值、平均值等,標識出有價值的數據。抓取有效數據,製作成報表給 boss。
針對以上的場景,報表可視化可以總結出以下幾個難點:
1. 併發
公司客服人數衆多,幾千人同時在線重度操作,業務流轉週期短、數據量大,所以對服務端併發性能消耗是很大的。可以在後臺用 Apache POI 來提取和修改 Excel 數據、並執行其中的公式計算等。這樣會遇到兩個性能瓶頸:
1)需要頻繁地上傳、下載文檔,服務器帶寬承受了很大的壓力;
2)所有 Excel 解析、提取的操作都在服務器端,頻繁的 IO 操作讓服務器不堪重負。
以上兩個性能點,在目前的架構下很難突破,這也是重構項目時最具挑戰性的需求點之一。當然硬堆服務器配置也是一個解決方案,但無法解決其它的一些問題,並且也會帶來運維的壓力。
2. 對 Excel 操作和兼容性要求較高
新系統如果不能讓大家快速上手使用,以這個項目用戶的體量,培訓成本將無法承受。而且要能夠直接導入已有的 Excel 報表模板,否則再次開發或設計所有 Excel 報表也是難以接受的。
3. 報表格式靈活多變
針對不同的業務場景,報表的模版也是千變萬化。因此不需要研發的介入,操作員的設計和填報都可以在頁面上完成顯得尤爲重要。
4. 支持公式計算
由於涉及到商品、訂單、成本覈算、財務統計等模塊,對計算公式的種類和性能要求較高。
5. 工作流中的數據文檔
以前系統的工作流,涉及到 Excel 報表時,要麼數據會先在服務端和 Excel 模板進行拼裝,要麼系統根據路徑找到文件服務器的 Excel 文件,然後流轉到對應環節。一些新的業務模塊,甚至還只能用郵件進行文件傳輸。
這個過程會產生大量的文件,對文件服務器的帶來了很大壓力,後臺也不得不定期做批量的數據拆分和維護。這次升級系統需要解決這個問題。
思考如何選型
首先,選型的第一步就是搞清楚市面上具體有哪些產品供我們選擇,對於目前市面上能集成到系統中,支持這種在線表格文檔編輯的產品有不少,大體我把他們分了兩類。
1. 雲文檔類型產品
這種產品有很多,類似 WPS、石墨文檔、office online。它們本身具備較高的完成度,已經幫用戶實現了包括在線協同內的幾乎所有功能,甚至也支持一定程度的二次開發而且可以私有化部署。但問題在於通常這類產品封閉性比較強,二次定製開發還是相對比較困難,且不夠輕量。授權方式也多以按時間、按併發量、用戶數量等方式授權,價格昂貴,不是很適合我們的需要。
2. 控件類型產品
像 LuckySheet、Handsontable、SpreadJS 這種就是標準的控件了,它們都是純前端表格控件,都支持 Excel 的功能特性和 json 數據綁定。
LuckySheet 是國內的MIT開源軟件,可以拿來商用。但在我調研時它纔剛上線 1、2 個月,並且不像 React 這種有某個大廠來背書,所以不可能拿來用到我們的正式項目裏。截止目前已經過去了 1 年,陸續推出了 QQ 羣、論壇等交流平臺,但仍顯薄弱。
Handsontable 是國外的一個商業表格控件,據說二次開發坑較多,但對我們來說最大的問題是它沒有中文支持團隊。
SpreadJS 是葡萄城公司的商業Excel表格控件,有趣的是我發現在 V2EX 的 LuckySheet 下方評論區中,LuckySheet 的作者也說 SpreadJS 是行業標杆。它支持導入包括公式、圖表、樣式、條件格式在內的絕大部分 Excel 特性(不支持宏)。並且最驚喜的是,它的操作界面是一個完整的 Excel 界面,完全純 JS 開發的,用 json 進行模板和數據交互。同時 SpreadJS 也有對應的售後支持團隊,技術問題可以工作日期間隨時電話、論壇交流,相關的資料包括視頻、文檔、示例、API 手冊也都非常豐富,甚至還可以請他們的技術顧問來公司培訓。對於像我們這種工期短、開發任務比較繁重的項目組,確實能節約大量的精力,降低了風險。
圖片來源:SpreadJS在線Excel編輯器
那麼什麼是控件?爲什麼要用控件?
引用維基百科
在計算機編程當中,控件(或部件,widget或control)是一種圖形用戶界面元素,其顯示的信息排列可由用戶改變,例如視窗或文本框。控件定義的特點是爲給定數據的直接操作(direct manipulation)提供單獨的互動點。控件是一種基本的可視構件塊,包含在應用程序中,控制着該程序處理的所有數據以及關於這些數據的交互操作。
按照我自己理解,控件就是隻提供了基本功能,支持二次開發的功能模塊。控件相對依賴更輕,可塑性更好,並且也有對應的開發文檔和 API,是面向開發者的基礎功能包,便於按需求來定製功能。
SpreadJS 需求解決方案和優勢
1. 併發
由於 SpreadJS 是數據和模板分離的設計,填報人員只需要在頁面上完成填報。提交時可以只提交填報好的數據 json 即可,服務器再也不用集中解析所有Excel 文件了。帶寬消耗也直接節約了一半。
2. 對 Excel 操作和兼容性要求較高
在內部試用時,財務和客服的小姐姐們反饋,使用體驗跟 Excel 幾乎完全一樣,不需要再特意培訓。而且我們自己的大量 Excel 報表可以直接導入進去(二次開發後也可以實現批量和遠程導入),包括圖表、公式、表格樣式等等一系列元素都可以直接導入線上操作。
3. 報表格式靈活多變
設計人員可以直接在線設計,或者把 Excel 設計好的報表,拿到 Web 端,做好數據綁定,提交保存成 json 格式即可(Spread JS 的 ssjson 格式包括 Excel 文檔的所有信息)
4. 支持公式計算
支持了 450 多種( Excel 一共 480 多種)公式,還可以自己開發擴展自定義公式,對財務也夠用了。同時還支持所有 Excel 的引用操作,比如跨 sheet 引用、絕對引用、函數命名信息之類。
5. 工作流中的數據文檔
基本脫離了對文件的依賴,所有流程狀態和依賴的數據都可以在數據庫中記錄,文件服務器只需要保存少量的模板文檔即可(其實模板數量不大時可以直接放到數據庫裏,不過我們有現成的文件服務器)。這裏節約了我們 90% 文件服務器的空間開銷,運維的小夥伴半夜都要笑醒。
深入SpreadJS
重點來了,其實最讓我這個前端開發者感興趣的就是 SpreadJS 的一些底層設計、以及對內存、性能平衡性的優化。對此我做了很多調研和學習,好在這方面資料不難找,常常可以在葡萄城官方論壇的公開課版塊( https://gcdn.grapecity.com.cn/forum.php?mod=forumdisplay&fid=225&filter=typeid&typeid=274&fileGuid=QKgTJRrrCD96PXwh)
渲染性能
性能肯定是每個深度表格控件用戶最擔心的問題。我們的數據量常常達到好幾千條,而且 Excel 不方便分頁(涉及前端的公式計算彙總),所以選型期間很擔心。後來發現想多了,SpreadJS 可以輕鬆加載 50 萬條數據加載耗時 200 ms左右(官網性能演示示例只能加載 5 萬,我們自己扒下來測的 50 萬)。後來深入瞭解才知道,解決這個問題,他們的思路是這樣的:
- 實時渲染 + Double buffering (翻譯成雙層緩存?):
用 Canvas 渲染表格部分,並且只渲染用戶看到的部分內容,這就實現了加載 1000 行和加載 100000 行數據速度都很快,性能相差不大的現象。
而 Double buffering 是爲了解決連續渲染的連續性體驗問題,也可以進一步提升渲染速度。這個名詞估計聽過的人少,但應該人人都體驗過,Double buffering 在圖形學裏,一般稱作雙緩衝,實際上的繪圖指令是在一個緩衝區完成,這裏的繪圖非常的快,在繪圖指令完成之後,再通過交換指令把完成的圖形立即顯示在屏幕上,這就避免了出現繪圖的不完整,同時效率很高。在遊戲裏其實很常見,當我們主控的人物在地圖上奔跑時,遊戲引擎會按照人物移動方向實時加載和渲染地圖,這就避免了一次性加載超大地圖時那漫長的等待。
圖片來源:葡萄城公開課【SpreadJS性能優化】
SpreadJS 性能優化 - 葡萄城公開課 - 葡萄城產品技術社區 (grapecity.com.cn)
- 稀疏數組:
SpreadJS 對錶格數據的存儲優化採用了稀疏數組的數據結構。稀疏數組常用來優化二維數組(比如棋盤、地圖等場景)的內存佔用,但它有個天生的缺陷,就是訪問性能慢。
所以當時針對這個疑問,我給它做了壓力測試,百萬級別的遍歷耗時 200 多ms。性能可以滿足我們的需求。
計算引擎
據官方介紹來看,公式引擎其實是包含了兩大實現的部分,一個是計算邏輯系統、一個是引用系統。
- 引用系統
Excel中公式的計算都是依賴於某些原始數據的,比如 C1 引用 B1、B1 又引用 A1等等, SpreadJS 把這部分功能封裝的已經非常原生化了,根本不需要開發者操心(除非有引用回溯等特殊需求)。
Excel 中 有直接引用、跨 Sheet 表單引用、相對/絕對引用、命名信息的引用、 table 行列公式的引用、跨工作簿引用等等(沒列舉完,感興趣的同學可以自行搜索學習)。SpreadJS 的 runtime 是在網頁端,因此跨 Workbook 引用就別想了,至少目前肯定沒支持。
- 計算邏輯
SUM、IF、MATCH、VLOOKUP 這種能輸入到單元格里的計算公式,用起來就像是一個個的小“邏輯包”,目前 SpreadJS 有 460+ 種原生的公式函數,而 Excel 只有 490+ 種,並且 SpreadJS 能自定製公式,使用體驗與原生公式一樣。
對於底層實現,實際上經過多個版本的迭代,這些公式早已不是一個個獨立的“邏輯孤島”了。公式的實現在底層有大量的抽象和複用,據說新版本在性能提升的同時,代碼量比老版本有明顯精簡,這對前端工程打包也是比較友好的。
對於嵌套公式計算的實現,SpreadJS 在底層建立起了 AST 樹來解析用戶設置公式的計算邏輯,從官方示例的代碼來看,公式底層建立了一套 Expression,並且有對應的 public 接口可供調用,如圖:
圖片來源:【SpreadJS公式結構樹形展示】
https://gcdn.grapecity.com.cn/showtopic-79188-1-1.html?fileGuid=QKgTJRrrCD96PXwh
- 性能
首先,作爲一個前端技術,咱們可以先從公式計算的技術要求上來分析性能可能出現的瓶頸以及造成的影響。我們在開發時用到了大量的用戶事件、髒數據、聯動等功能,所有這些功能確保正確運行的一個重要前提,就是必須能確保隨時可以拿到正確的計算結果,那麼最直接的實現思路就是讓公式以高優先級、同步的方式來執行完計算。
大家都知道,多線程可以幫助分擔計算壓力,但是先拋開設計和實現難度不說,即便支持了 Web Worker,JavaScript 嚴格來說也只能算是一個單線程語言,因爲它的 Web Worker 子線程完全受主線程控制,並且主線程無法被阻塞掛起。所以即使引入了 Web Worker,也無法確保上邊提到的同步執行。
經過以上分析,可以看出公式計算性能的侷限性,取決於 JavaScript 的計算能力。我找了一張相關的圖片,可以直觀反映 Node.js 的計算能力(Node.js 是 V8 引擎,公認最快的 JS 引擎)
圖片引用自《深入淺出Node.js》
據我們測試,以上計算性能接近原生JS的計算性能,SpreadJS 在這方面的優化已經十分接近物理極限了。目前在我們的應用場景中,這個計算性能已經足夠使用,但不排除以後會出現海量的數據和公式的計算需求,而在這方面官方也給出了相關解決方案,參考這裏。
據說,官方還在進一步開發緩存技術,來實現公式計算的分塊緩存:即使引用鏈上有值發生變化,也不需要計算整個引用鏈的公式。聽起來很強大,思路也靠譜,但願早點推出。
樣式系統
Excel 的樣式系統非常複雜,邊框、字體、對齊、數據格式、條件格式等等每一個功能點都有非常靈活龐大的實現,剛開始瞭解 SpreadJS 時,我也被它的 Style 類驚呆了,除了我能想象到的邊框、背景、字體、對齊等這些能“看得到”的,竟然也有單元格類型、數據格式、表格按鈕、下拉、水印這類東西。不由得感嘆 Style 太重了,如果定製大量的單元格樣式,內存和性能肯定都不好。不過實際應用中倒也沒發現瓶頸,原來這裏採用了分層結構來設計,如圖:
圖片來源:葡萄城公開課【SpreadJS性能優化】
SpreadJS怎麼用?
1. 渲染表格
圖 6.1-1 綁定數據和公式
首先獲取全局 spread 對象,spread 是整個表格的主體,spread 又分成多個 sheet。SpreadJS 初始化結束都會返回一個 spread 對象。
- vue 版本 spread 對象
<gc-spread-sheets @workbookInitialized='spreadInitHandle(\$event)' />
methods:{
spreadInitHandle: function (spread) {
this.spread = sprea
},
}
-
綁定數據,綁定公式
tableDataBind() { // 數據源,可以從後臺請求拿到 var dataSource = { // 注意這裏加了一層bindPath,用於映射表格的綁定路徑 bindPath_table: [{ c1: 100, c2: 90, c3: 30, c4: 40 }, { c1: 88, c2: 66, c3: 55, c4:100 }, { c1: 30, c2: 89, c3: 100, c4: 40 },{ c1: 40, c2: 66, c3: 88, c4: 40 }] }; // 表格綁定和單元格綁定數據源,需要用SpreadJS的CellBindingSource包裝一下 var spreadNS = GC.Spread.Sheets; var dataSource1 = new spreadNS.Bindings.CellBindingSource(dataSource); var table2 = this.activeSheet.tables.add("tableName", 0, 0, 1, 5, spreadNS.Tables.TableThemes.light6); table2.showFooter(true); table2.autoGenerateColumns(false); var c1 = new spreadNS.Tables.TableColumn(1); c1.name("語文"); c1.dataField("c1"); var c2 = new spreadNS.Tables.TableColumn(2); c2.name("數學"); c2.dataField("c2"); var c3 = new spreadNS.Tables.TableColumn(3); c3.name("英語"); c3.dataField("c3"); var c4 = new spreadNS.Tables.TableColumn(4); c4.name("化學"); c4.dataField("c4"); var c5 = new spreadNS.Tables.TableColumn(5); c5.name("合計"); table2.bindColumns([c1, c2, c3, c4, c5]); table2.bindingPath("bindPath_table"); // 設置公式 table2.setColumnDataFormula(4, "=[@語文]+[@數學]+[@英語]+[@化學]"); table2.setColumnFormula(4, "=SUBTOTAL(109,[合計])"); // 設置允許單元格的內容超出單元格,與綁定無關 this.activeSheet.options.allowCellOverflow = true; // 綁定dataSource this.activeSheet.setDataSource(dataSource1); this.spread.resumePaint(); }
圖 6.1-2函數名和函數碼映射表
渲染條件格式
渲染條件格式:數據渲染完成只能保證數據能正常顯示出來,但是這還不能滿足數據分析師的需求,還要明顯展示有效數據譬如:最大值,最小值標紅,進度條展示一個變化狀態,圖標展示上升還是下降,雙色階,三色階,等,具體怎麼實現?
- 圖標集:效果如圖
-
實現代碼
iconset() { var activeSheet = this.activeSheet; var iconSetRule = new GC.Spread.Sheets.ConditionalFormatting.IconSetRule(); // 演示demo先寫死區域 iconSetRule.ranges([new GC.Spread.Sheets.Range(0,0, 5, 5)]); // IconSetType圖標誌的類型:箭頭,圓圈和execl 打通的,excel有哪些這這邊就支持哪些 iconSetRule.iconSetType(GC.Spread.Sheets.ConditionalFormatting.IconSetType.threeArrowsColored); var iconCriteria = iconSetRule.iconCriteria(); iconCriteria[0] = new GC.Spread.Sheets.ConditionalFormatting.IconCriterion( true, GC.Spread.Sheets.ConditionalFormatting.IconValueType.number, 60 );(<60) iconCriteria[1] = new GC.Spread.Sheets.ConditionalFormatting.IconCriterion( true, GC.Spread.Sheets.ConditionalFormatting.IconValueType.number, 90 );(60<= <90) iconCriteria[2] = new GC.Spread.Sheets.ConditionalFormatting.IconCriterion( true, GC.Spread.Sheets.ConditionalFormatting.IconValueType.number, 90 );(>=90) iconSetRule.reverseIconOrder(false); iconSetRule.showIconOnly(false); activeSheet.conditionalFormats.addRule(iconSetRule); }
-
進度條:效果如圖
-
實現代碼
dataBar(){ var activeSheet = this.activeSheet; activeSheet.conditionalFormats.addDataBarRule( GC.Spread.Sheets.ConditionalFormatting.ScaleValueType.number,0,//最小數 GC.Spread.Sheets.ConditionalFormatting.ScaleValueType.number, 100,//最大值 "orange",//顏色 [new GC.Spread.Sheets.Range(0,0, 5, 4)] ); },
-
重複值:效果如圖
-
實現代碼
duplicateValue() { var activeSheet = this.activeSheet; var style = new GC.Spread.Sheets.Style(); style.backColor = "yellow"; style.foreColor = "red"; var ranges = [new GC.Spread.Sheets.Range(0,0, 5, 4)]; activeSheet.conditionalFormats.addDuplicateRule(style, ranges); } -
包含文本 6 的單元格:效果如圖
-
實現代碼
includeText() { var activeSheet = this.activeSheet; var style = new GC.Spread.Sheets.Style(); style.backColor = "red"; var ranges = [new GC.Spread.Sheets.Range(0,0, 5, 5)]; activeSheet.conditionalFormats.addSpecificTextRule( GC.Spread.Sheets.ConditionalFormatting.TextComparisonOperators.contains, "6", style, ranges ); }
-
綜合以上實現結果如圖
寫在最後
本文主要介紹了自己在數據可視化方向的一些探索,針對一些準備做市場大盤以及郵件訂閱報表,線上協同協作,可視化分析等方向的同學有一定的幫助。
因篇幅較長,所涉及概念性的東西比較多,難免會出現錯誤,希望大家多多指正,謝謝大家!
============================
小編有話說:感謝政採雲前端技術團隊對葡萄城產品的認可並提供上述內容。如您也有關於葡萄城產品的使用心得,歡迎向我們投稿,在微信公衆號後臺聯繫我們即可。