作者:vivo 網際網路巨量資料團隊- Wang Lei
一直以來,許多產品平臺都在嘗試通過視覺化搭建的手段來降低 GUI 應用的研發門檻,提高生產效率。隨著我們業務的發展,資料建設的完善,使用者對於資料視覺化的訴求也日益增多,而資料大屏是資料視覺化的其中一種展示方式,它作為巨量資料展示媒介的一種,被廣泛運用於各種會展、公司展廳、釋出會等。
相比於傳統手工客製化的圖表與資料儀表盤,通用大屏搭建平臺的出現,可以解決客製化開發, 資料分散帶來的應用開發、資料維護成本高等問題,通過資料採集、清洗、分析到直觀實時的資料視覺化展現,能夠多方位、多角度、全景展現各項指標,實時監控,動態一目瞭然。
本文將通過敏捷BI平臺的通用大屏搭建能力的實現方案,來講解一下通用視覺化搭建平臺整體的設計思路。
從技術層面上來講,最直觀的就是前端視覺化框架:Echart、Antv、Chart.js、D3.js、Vega 等,這些庫都能幫我們快速把資料轉換成各種形式的視覺化圖表。
從業務層面來講, 其最主要的意義就在於通過資料 -> 圖表組合 -> 視覺化頁面這一業務流程,來幫助使用者更加直觀整體的分析不同行業和場景的趨勢和規律。
所以在資料領域裡,對於複雜難懂且體量龐大的資料而言,圖表的資訊量要大得多,這也是資料視覺化最根本的目的。
主要由 視覺化元件 + 事件互動 + 座標關係 組成,效果如下圖所示:
經常會有同學會問到,視覺化大屏和BI報表看板的區別是什麼?
這裡簡單的做一下介紹:
大屏和報表看板都只是BI的其中一種展現方式,大屏更多是通過不同尺寸的顯示器硬體上進行投屏,而報表看板更多是在電腦端進行展示使用。
大屏更加註重資料動態變化 ,會有極強的視覺體驗和衝擊力,提供豐富的輪播動畫、表格捲動等動畫特效。而報表看板更注重互動式資料探索分析,例如上卷下鑽、排序、過濾、圖表切換、條件預警等。
前端框架:React 全家桶(個人習慣)
視覺化框架:Echarts\DataV-React (封裝度高,json結構的設定項易拓展) D3.js(視覺化元素粒度小、客製化能力強)
拖拽外掛:dnd-kit (滿足樹狀結構檢視的跨元件拖拽)
佈局外掛:React-Grid-Layout(網格自由佈局,修改原始碼,支援多個方向的拖拽,自由佈局、鎖定縮放比等)
下圖是我們搭建平臺的整體架構設計:
整個大屏搭建平臺包含四個非常重要的子系統和模組:
視覺化物料中心:是整個平臺最基礎的模組,我們在開源的圖表庫和自主開發的視覺化元件上面定義了一層標準的 DSL 協定,這個協定和接入 畫布編輯器 的協定是對應的,目前已經有 40+ 相關元件,元件數量還在不斷增長。
畫布編輯器:是搭建平臺的核心與難點,支援頁面佈局設定、頁面互動設定和元件資料設定等功能,另外還支援程式碼片段的設定,也可以稱得上是一個低程式碼平臺。
資料中心:是提供專門用於連線不同資料來源的服務,例如直連 MySQL、ClickHouse、Elasticsearch、Presto 等,提供了大屏搭建所需要的原始資料。
管理中心:是大屏的後臺運營管理模組,包含了大屏模版管理、大屏釋出下線、存取許可權等管理功能。
通過上面提到的大屏組成元素,我們可以分析總結出大屏搭建主流程如下圖所示:
接下來我們會逐一對平臺幾個核心功能實現進行解析:
1、大屏自適應佈局
背景:解決頁面錯亂問題,實現多種解析度的大屏適配:
思考:首先我們想到的是行動端適配主流的 vh、vw、rem組合的方式以及 font.js+rem 等兩種方案。第一種方案主要是通過媒體查詢來定義父級大小,然後對元件的height、margin、padding等多種css屬性採用rem作為單位,繼承父級設定等單位(1vw),實現自適應適配,第二種方案是參照第三方指令碼,通過在main.js中寫程式碼計算,使用rem進行繼承,實現適配。
① vh、vw、rem組合
//vw vh單位 w3c的官方解釋 vw:1% of viewport’s width vh:1% of viewport’s height //例如,設計稿的寬度為1920px,則1vw=19.2px,為了方便計算,我們將html元素的font-size大小設定為100px,也就是5.208vw=100px。 body,html { font-size:5.208vw }
② font.js + rem
//監聽視窗的oversize事件,來動態計算根節點字型大小,再配合rem做適配 (function(doc, win) { const docEl = doc.documentElement const resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize' const recalc = function() { let clientWidth = docEl.clientWidth if (!clientWidth) return docEl.style.fontSize = 100 * (clientWidth / 1920) + 'px' } if (!doc.addEventListener) return win.addEventListener(resizeEvt, recalc, false) doc.addEventListener('DOMContentLoaded', recalc, false) })(document, window)
缺陷:當我們大屏裡面使用到的第三方外掛,它的樣式使用的是px為單位時,例如 line-height 的設定為20px,此時就不能適應行高,就會出現重疊等錯亂問題。或者我們利用 postcss-px2rem 外掛進行全域性替換,但是在使用過程中,需要注意對已經處理過適配的外掛,例如 Ant Design,否則引入的antd 控制元件使用會出現樣式錯亂的問題
解決思路:採用了css3 的縮放 transform: scale(X,Y) 屬性,主要是通過監聽瀏覽器視窗的 onresize 事件,當視窗大小發生變化時,我們只需要根據大屏容器的實際寬高,去計算對應的放大縮小的比例,就可以實現佈局的自適應了,我們也提供了不同的佈局適應效果,例如等高縮放、等寬縮放、全螢幕鋪滿等,不同的縮放方式,決定了我們在計算寬高比例的優先順序。因此我們後面在做畫布的縮小功能,也可以直接使用這種方案來實現。
// 基於設定的設計稿尺寸 換算對應的寬高比 useEffect(() => { const wR = boxSize.width / viewWidth; const hR = boxSize.height / viewHeight; setBgScaleRatio(wR); setBgHeightScaleRatio(hR); }, [boxSize, viewWidth, viewHeight]); //根據等寬、等高、全螢幕等不同的縮放比例 計算scale值 const getScale = (proportion, x, y) => { if (proportion === 'radioWidth') { return `scaleX(${x})` } if (proportion === 'radioHeight') { return `scaleY(${y})` } return `scale(${x}, ${y})` }
2、大屏元件通用開發流程設計
背景:隨著視覺化元件的增多、新增元件流程繁瑣冗長,為了避免重複的造輪子以及後續引入第三方元件,需要制定一套通用的元件開發流程:
設計思路:元件 = component 元件主體 + schema 元件設定協定層 + 元件定義層(型別、從屬關係、初始化寬高等)
① component 元件主體:
視覺化框架選型:行業主流視覺化庫有 Echart、Antv、Chart.js、D3.js、Vega、DataV-React 基於視覺化的通用性和客製化性的需求,我們選擇了 Echart、DataV-React 作為基礎元件的開發框架,面對客製化性要求更高的自定義元件,我們選擇了視覺化粒度更小的 D3.js。
封裝通用 Echarts 元件(初始化、事件註冊、範例登出等):
// initialization echarts const renderNewEcharts = () => { // 1. new echarts instance const echartObj = updateEChartsOption(); // 2. bind events bindEvents(echartObj, onEvents || {}); // 3. on chart ready if (typeof onChartReady === 'function') onChartReady(echartObj); // 4. on resize echartObj.resize(); }; // bind the events const bindEvents = (instance, events) => { const _bindEvent = (eventName, func) => { instance.on(eventName, (param) => { func(param, instance); }); }; // loop and bind for (const eventName in events) { if (Object.prototype.hasOwnProperty.call(events, eventName)) { _bindEvent(eventName, events[eventName]); } } }; // dispose echarts and clear size-sensor const dispose = () => { if ($chartEl.current) { clear($chartEl.current); // dispose echarts instance (echartsLib || echarts).dispose($chartEl.current); } };
封裝通用 DataV 元件(DataV-React、自定義等元件入口,統一負責設定、資料收集、監聽resize)
const DataV: React.FC<DataVProps> = (props) => { const { config } = props; const [renderCounter, setRenderCounter] = useState(0); const $dataVWarpEl = useRef(null); const $componentEl = useRef(null); useEffect(() => { // 繫結容器size監聽 const resizefunc = debounce(() => { $componentEl.resize(); }, 500) // fixme addResizeListener($dataVWarpEl.current, resizefunc); return () => { // 清除訂閱 removeResizeListener($dataVWarpEl.current, resizefunc); }; }, []); return ( <DataVWarp ref={$dataVWarpEl}> <CompRender config={config} ref={$componentEl} /> </DataVWarp> ); };
② schema 元件設定協定層 + 元件定義層(型別、從屬關係、初始化寬高等)
我們自定義了一套 schema 元件的DSL,結構協定層。通過DSL約定了元件的設定協定,包括元件的可編輯屬性、編輯型別、初始值等,之所以定義一致的協定層,主要是方便後期的元件擴充套件,設定後移。
JSON Schema設計:
{ headerGroupName: '公共設定', //設定所屬型別 headerGroupKey: 'widget', //設定所屬型別key值 相同的key值都歸屬一類 name: '標題名稱', //屬性名稱 valueType: ['string'], //屬性值型別 optionLabels: [], //服務下拉選單、多選框等控制元件的標籤名 optionValues: [], //服務下拉選單、多選框等控制元件的標籤值 tip: false, //設定項的 Tooltip 註解 ui: ['input'], //使用的控制元件型別 class: false, //控制元件類名,客製化控制元件樣式 css: { width: '50%'}, //修改控制元件樣式 dependencies: ['widget,title.show,true'], //屬性之間的聯動,規則['設定所屬型別, 屬性key, 屬性值'] depContext: DepCommonShowState, //屬性之間的校驗回撥方法 compShow: ['line'], //哪些元件可設定 dataV: { key: 'title.text', value: '' }, //設定的key值和預設value值 },
表單DSL設計:
收益:以上是我們客製化的DSL結構協定層,使用者只需要填寫Excel表格,就可以實現動態表單的建立,實現元件設定項分類、設定複用、設定項之間聯動、屬性註釋等功能。目前屬性設定器已經支援了常用的15種的設定UI控制元件,通過客製化的DSL結構協定層,可以快速完成元件的設定介面初始化,為後續規劃的元件物料中心做準備。
3、拖拽器實現
背景:React-Grid-Layout 拖拽外掛不支援自由佈局和元件不同緯度拖拽:
解決方案:通過分析原始碼,對不同緯度的拖拽事件以及拖拽目標碰撞事件進行了重寫,並且也拓展了鎖定寬高比、旋轉透明度等功能。
原始碼分析:
resize伸縮特性增強(優化),拖拽的同時,除了修改容器寬高外,也動態調整了元件的座標位置。
// CSS Transforms support (default) if (useCSSTransforms) { if (activeResize) { const { width, height, handle } = activeResize; const clonePos = { ...pos }; if (["w", "nw", "sw"].includes(handle)) { clonePos.left -= clonePos.width - width; } if (["n", "nw", "ne"].includes(handle)) { clonePos.top -= clonePos.height - height; } style = setTransform(clonePos, this.props.angle); } else { style = setTransform(pos, this.props.angle); } }
堆疊顯示,自由佈局(優化),通過控制佈局是否壓縮,動態調整拖拽目標的層級zIndex來實現多圖層元件操作互動和自由定位。
// 每次拖拽時zIndex要在當前最大zIndex基礎上 + 1,並返回給元件使用 const getAfterMaxZIndex = useCallback(i => { if (i === curDragItemI) { return; } setCurDragItemI(i); setMaxZIndex(maxZIndex => maxZIndex + 1); return maxZIndex; }, []);
改造後效果展示
4、大屏狀態推播
背景:大屏的後期維護需要有版本釋出自更新以及大屏下線等需求,這個時候就需要有一套訊息通知機制,通過命令來控制大屏的執行狀態。
解決方案:基於websocket通訊機制,建立長連結,實現了心跳及重連機制,實時對上線釋出後的大屏進行更新或下線管理。
本文通過視覺化頁面搭建、no/low code 平臺、Schema 動態表單等技術思想來分析講解了如何去設計開發一個通用的資料大屏搭建平臺。
當前的設計方案基本滿足了資料大屏的核心能力搭建需求。如果想實現更富有展現力, 滿足更多場景的大屏搭建平臺, 我們還需要進一步提高平臺的擴充套件性和完善整體的物料生態, 具體規劃如下:
豐富和拓展大屏元件&設定能力,覆蓋不同行業的視覺化場景。
視覺化物料平臺的搭建,沉澱優秀的視覺化元件、大屏模版素材。
3D以及動效渲染引擎開發實現。