從零設(shè)計(jì)可視化大屏搭建引擎
幾個(gè)月前我寫了一篇關(guān)于從零開發(fā)一款可視化大屏制作平臺(tái) 的文章, 簡(jiǎn)單概述了一下可視化大屏搭建平臺(tái)的一些設(shè)計(jì)思路和效果演示, 這篇文章我會(huì)就 如何設(shè)計(jì)可視化大屏搭建引擎 這一主題, 詳細(xì)介紹一下實(shí)現(xiàn)原理。
按照我一向的寫作風(fēng)格, 我會(huì)在下面列出文章的大綱,以便大家有選擇且高效率的閱讀和學(xué)習(xí):
- 快速了解數(shù)據(jù)可視化
- 如何設(shè)計(jì)通用的大屏搭建引擎
- 大屏搭建引擎核心功能實(shí)現(xiàn)
- 拖拽器實(shí)現(xiàn)
- 物料中心設(shè)計(jì)
- 動(dòng)態(tài)渲染器實(shí)現(xiàn)
- 配置面板設(shè)計(jì)
- 控制中心概述
- 功能輔助設(shè)計(jì)
- 可視化大屏后期規(guī)劃和未來(lái)展望
大家可以輕松根據(jù)右側(cè)的文章導(dǎo)航, 快速定位到自己想看的位置, 接下來(lái)我們開始進(jìn)入正文。
快速了解數(shù)據(jù)可視化
說(shuō)到數(shù)據(jù)可視化, 想必大家多多少少稍接觸過, 從技術(shù)層面談, 最直觀的就是前端可視化框架, 比如:
- echart
- antv
- Chart.js
- D3.js
- Vega
這些庫(kù)都能幫我們輕松制作可視化圖表。
從實(shí)用性的角度來(lái)談, 其最主要的意義就在于幫助用戶更好的分析和表達(dá)數(shù)據(jù)。所以說(shuō)談到數(shù)據(jù)可視化, 更多的是和各種圖表打交道, 通過 數(shù)據(jù) -> 圖表組合 -> 可視化頁(yè)面 這一業(yè)務(wù)流程, 就構(gòu)成了我們今天要研究的話題——設(shè)計(jì)可視化大屏搭建引擎。
如何設(shè)計(jì)通用的大屏搭建引擎
說(shuō)到 “引擎” 這個(gè)詞也許有種莫名的高大上, 其實(shí)在互聯(lián)網(wǎng)技術(shù)中, 我們經(jīng)常會(huì)聽到各種相關(guān)的名詞,比如 “瀏覽器渲染引擎” , “規(guī)則引擎” , “圖像識(shí)別引擎” 等, 我覺得 “引擎” 的本質(zhì)就是提供一套可靠的機(jī)制, 為系統(tǒng)提供源源不斷的生產(chǎn)力。所以我們今天談的“可視化大屏搭建引擎”, 本質(zhì)上也是提供一套搭建機(jī)制, 支撐我們?cè)O(shè)計(jì)各種復(fù)雜的可視化頁(yè)面。
為了方便大家理解可視化搭建, 我這里展示2張可視化大屏的頁(yè)面, 來(lái)和大家一起分析一下可視化大屏的組成要素:
當(dāng)然實(shí)際應(yīng)用中大屏展現(xiàn)的內(nèi)容和形式遠(yuǎn)比這復(fù)雜, 我們從上圖可以提煉出大屏頁(yè)面的2個(gè)直觀特征:
- 可視化組件集
- 空間坐標(biāo)關(guān)系
因?yàn)槲覀兛梢暬笃凛d體是頁(yè)面, 是html, 所以還有另外一個(gè)特征: 事件/交互。綜上我們總結(jié)出了可視化大屏的必備要素:
我們只要充分的理解了可視化大屏的組成和特征, 我們才能更好的設(shè)計(jì)可視化大屏搭建引擎, 基于以上分析, 我設(shè)計(jì)了一張基礎(chǔ)引擎的架構(gòu)圖:
接下來(lái)我就帶大家一起來(lái)拆解并實(shí)現(xiàn)上面的搭建引擎。
大屏搭建引擎核心功能實(shí)現(xiàn)
俗話說(shuō): “好的拆解是成功的一半”, 任何一個(gè)復(fù)雜任務(wù)或者系統(tǒng), 我們只要能將其拆解成很多細(xì)小的子模塊, 就能很好的解決并實(shí)現(xiàn)它. (學(xué)習(xí)也是一樣)
接下來(lái)我們就逐一解決上述基礎(chǔ)引擎的幾個(gè)核心子模塊:
- 拖拽器實(shí)現(xiàn)
- 物料中心設(shè)計(jì)
- 動(dòng)態(tài)渲染器實(shí)現(xiàn)
- 配置面板設(shè)計(jì)
- 控制中心概述
- 功能輔助設(shè)計(jì)
拖拽器實(shí)現(xiàn)
拖拽器是可視化搭建引擎的核心模塊, 也是用來(lái)解決上述提到的大屏頁(yè)面特征中的“空間坐標(biāo)關(guān)系”這一問題。我們先來(lái)看一下實(shí)現(xiàn)效果:
有關(guān)拖拽的技術(shù)實(shí)現(xiàn), 我們可以利用原生 js 實(shí)現(xiàn), 也可以使用第三方成熟的拖拽庫(kù), 比如:
- DnD
- React-Dragable
- react-moveable
我之前也開源了一個(gè)輕量級(jí)自由拖拽庫(kù) rc-drag , 效果如下:
有關(guān)它的技術(shù)實(shí)現(xiàn)可以參考我的另一篇文章: 輕松教你搞定組件的拖拽, 縮放, 多控制點(diǎn)伸縮和拖拽數(shù)據(jù)上報(bào)。大家也可以基于此做二次擴(kuò)展和封裝。
我們拖拽器的基本原型代碼如下:
- export default function DragBox(props) {
- const [x, y, config] = props;
- const [target, setTarget] = React.useState();
- const [elementGuidelines, setElementGuidelines] = React.useState([]);
- const [frame, setFrame] = React.useState({
- translate: [x, y],
- });
- React.useEffect(() => {
- setTarget(document.querySelector(".target")!);
- }, []);
- return <div className="container">
- <div className="target">拖拽內(nèi)部組件, 比如圖表/基礎(chǔ)組件等</div>
- <Moveable
- target={target}
- elementGuidelines={elementGuidelines}
- snappable={true}
- snapThreshold={5}
- isDisplaySnapDigit={true}
- snapGap={true}
- snapElement={true}
- snapVertical={true}
- snapHorizontal={true}
- snapCenter={false}
- snapDigit={0}
- draggable={true}
- throttleDrag={0}
- startDragRotate={0}
- throttleDragRotate={0}
- zoom={1}
- origin={true}
- padding={{"left":0,"top":0,"right":0,"bottom":0}}
- onDragStart={e => {
- e.set(frame.translate);
- // 自定義的拖拽開始邏輯
- }}
- onDrag={e => {
- frame.translate = e.beforeTranslate;
- e.target.style.transform = `translate(${e.beforeTranslate[0]}px, ${e.beforeTranslate[1]}px)`;
- // 自定義的拖拽結(jié)束邏輯
- }}
- />
- </div>;
- }
以上只是實(shí)現(xiàn)了基本的拖拽功能, 我們需要對(duì)拖拽位置信息做保存以便在預(yù)覽是實(shí)現(xiàn)“所搭即所得”的效果。位置信息會(huì)和其他屬性統(tǒng)一保存在組件的DSL數(shù)據(jù)中, 這塊在接下來(lái)內(nèi)容中會(huì)詳細(xì)介紹。
對(duì)于拖拽器的進(jìn)一步深入, 我們還可以設(shè)置參考線, 對(duì)齊線, 吸附等, 并且可以在拖拽的不同時(shí)期(比如onDragStart和onDragEnd)做不同的業(yè)務(wù)邏輯。這些 Moveable 都提供了對(duì)應(yīng)的api支持, 大家可以參考使用。
物料中心設(shè)計(jì)
物料中心主要為大屏頁(yè)面提供“原材料”。為了設(shè)計(jì)健壯且通用的物料, 我們需要設(shè)計(jì)一套標(biāo)準(zhǔn)組件結(jié)構(gòu)和屬性協(xié)議。并且為了方便物料管理和查詢, 我們還需要對(duì)物料進(jìn)行分類, 我的分類如下:
- 可視化組件 (柱狀圖, 餅圖, 條形圖, 地圖可視化等)
- 修飾型組件 (圖片, 輪播圖, 修飾素材等)
- 文字類組件 (文本, 文本跑馬燈, 文字看板)
具體的物料庫(kù)演示如下:
這里我拿一個(gè)可視化組件的實(shí)現(xiàn)來(lái)舉例說(shuō)明:
- import React, { memo, useEffect } from 'react'
- import { Chart } from '@antv/g2'
- import { colors } from '@/components/BasicShop/common'
- import { ChartConfigType } from './schema'
- interface ChartComponentProps extends ChartConfigType {
- id: string
- }
- const ChartComponent: React.FC<ChartComponentProps> = ({
- id, data, width, height,
- toggle, legendPosition, legendLayout, legendShape,
- labelColor, axisColor, multiColor, tipEvent, titleEvent,
- dataType, apiAddress, apiMethod, apiData, refreshTime,
- }) => {
- useEffect(() => {
- let timer:any = null;
- const chart = new Chart({
- container: `chart-${id}`,
- autoFit: true,
- width,
- height
- })
- // 數(shù)據(jù)過濾, 接入
- const dataX = data.map(item => ({ ...item, value: Number(item.value) }))
- chart.data(dataX)
- // 圖表屬性組裝
- chart.legend(
- toggle
- ? {
- position: legendPosition,
- layout: legendLayout,
- marker: {
- symbol: legendShape
- },
- }
- : false,
- )
- chart.tooltip({
- showTitle: false,
- showMarkers: false,
- })
- // 其他圖表信息源配置, 方法雷同, 此處省略
- // ...
- chart.render()
- }, [])
- return <div id={`chart-${id}`} />
- }
- export default memo(ChartComponent)
以上就是我們的基礎(chǔ)物料的實(shí)現(xiàn)模式, 可視化組件采用了g2, 當(dāng)然大家也可以使用熟悉的echart, D3.js等. 不同物料既有通用的 props , 也有專有的 props, 取決于我們?nèi)绾味x物料的Schema。
在設(shè)計(jì) Schema 前我們需要明確組件的屬性劃分, 為了滿足組件配置的靈活性和通用性, 我做了如下劃分:
- 外觀屬性 (組件寬高, 顏色, 標(biāo)簽, 展現(xiàn)模式等)
- 數(shù)據(jù)配置 (靜態(tài)數(shù)據(jù), 動(dòng)態(tài)數(shù)據(jù))
- 事件/交互 (如單擊, 跳轉(zhuǎn)等)
有了以上劃分, 我們就可以輕松設(shè)計(jì)想要的通用Schema了。我們先來(lái)看看實(shí)現(xiàn)后的配置面板:
這些屬性項(xiàng)都是基于我們定義的schema配置項(xiàng), 通過 解析引擎 動(dòng)態(tài)渲染出來(lái)的, 有關(guān) 解析引擎 和配置面板, 我會(huì)在下面的章節(jié)和大家介紹。我們先看看組件的 schema 結(jié)構(gòu):
- const Chart: ChartSchema = {
- editAttrs: [
- {
- key: 'layerName',
- type: 'Text',
- cate: 'base',
- },
- {
- key: 'y',
- type: 'Number',
- cate: 'base',
- },
- ...DataConfig, // 數(shù)據(jù)配置項(xiàng)
- ...eventConfig, // 事件配置項(xiàng)
- ],
- config: {
- width: 200,
- height: 200,
- zIndex: 1,
- layerName: '柱狀圖',
- labelColor: 'rgba(188,200,212,1)',
- // ... 其他配置初始值
- multiColor: ['rgba(91, 143, 249, 1)', 'rgba(91, 143, 249, 1)', 'rgba(91, 143, 249,,1)', 'rgba(91, 143, 249, 1)'],
- data: [
- {
- name: 'A',
- value: 25,
- },
- {
- name: 'B',
- value: 66,
- }
- ],
- },
- }
其中 editAttrs 表示可編輯的屬性列表, config 為屬性的初始值, 當(dāng)然大家也可以根據(jù)自己的喜好, 設(shè)計(jì)類似的通用schema。
我們通過以上設(shè)計(jì)的標(biāo)準(zhǔn)組件和標(biāo)準(zhǔn)schema, 就可以批量且高效的生產(chǎn)各種物料, 還可以輕松集成任何第三方可視化組件庫(kù)。
動(dòng)態(tài)渲染器實(shí)現(xiàn)
我們都知道, 一個(gè)頁(yè)面中元素很多時(shí)會(huì)影響頁(yè)面整體的加載速度, 因?yàn)闉g覽器渲染頁(yè)面需要消耗CPU / GPU。對(duì)于可視化頁(yè)面來(lái)說(shuō), 每一個(gè)可視化組件都需要渲染大量的信息元, 這無(wú)疑會(huì)對(duì)頁(yè)面性能造成不小的影響, 所以我們需要設(shè)計(jì)一種機(jī)制, 讓組件異步加載到畫布上, 而不是一次性加載幾十個(gè)幾百個(gè)組件(這樣的話頁(yè)面會(huì)有大量的白屏?xí)r間, 用戶體驗(yàn)極度下降)。
動(dòng)態(tài)加載器就是提供了這樣一種機(jī)制, 保證組件的加載都是異步的, 一方面可以減少頁(yè)面體積, 另一方面用戶可以更早的看到頁(yè)面元素。目前我們熟的動(dòng)態(tài)加載機(jī)制也有很多, Vue 和 React 生態(tài)都提供了開箱即用的解決方案(雖然我們可以用 webpack 自行設(shè)計(jì)這樣的動(dòng)態(tài)模型, 此處為了提高行文效率, 我們直接基于現(xiàn)成方案封裝)。我們先看一下動(dòng)態(tài)渲染組件的過程:
上面的演示可以細(xì)微的看出從左側(cè)組件菜單拖動(dòng)某個(gè)組件圖標(biāo)到畫布上后, 真正的組件才開始加載渲染。
這里我們以 umi3.0 提供的 dynamic 函數(shù)來(lái)最小化實(shí)現(xiàn)一個(gè)動(dòng)態(tài)渲染器. 如果不熟悉 umi 生態(tài)的朋友, 也不用著急, 看完我的實(shí)現(xiàn)過程和原理之后, 就可以利用任何熟悉的動(dòng)態(tài)加載機(jī)制實(shí)現(xiàn)它了。實(shí)現(xiàn)如下:
- import React, { useMemo, memo, FC } from 'react'
- import { dynamic } from 'umi'
- import LoadingComponent from '@/components/LoadingComponent'
- const DynamicFunc = (cpName: string, category: string) => {
- return dynamic({
- async loader() {
- // 動(dòng)態(tài)加載組件
- const { default: Graph } = await import(`@/components/materies/${cpName}`)
- return (props: DynamicType) => {
- const { config, id } = props
- return <Graph {...config} id={id} />
- }
- },
- loading: () => <LoadingComponent />
- })
- }
- const DynamicRenderEngine: FC<DynamicType> = memo((props) => {
- const {
- type,
- config,
- // 其他配置...
- } = props
- const Dynamic = useMemo(() => {
- return DynamicFunc(config)
- }, [config])
- return <Dynamic {...props} />
- })
- export default DynamicRenderEngine
是不是很簡(jiǎn)單? 當(dāng)然我們也可以根據(jù)自身業(yè)務(wù)需要, 設(shè)計(jì)更復(fù)雜強(qiáng)大的動(dòng)態(tài)渲染器。
配置面板設(shè)計(jì)
實(shí)現(xiàn)配置面板的前提是對(duì)組件 Schema 結(jié)構(gòu)有一個(gè)系統(tǒng)的設(shè)計(jì), 在介紹組件庫(kù)實(shí)現(xiàn)中我們介紹了通用組件 schema 的一個(gè)設(shè)計(jì)案例, 我們基于這樣的案例結(jié)構(gòu), 來(lái)實(shí)現(xiàn) 動(dòng)態(tài)配置面板。
由上圖可以知道, 動(dòng)態(tài)配置面板的一個(gè)核心要素就是 表單渲染器。表單渲染器的目的就是基于屬性配置列表 attrs 來(lái)動(dòng)態(tài)渲染出對(duì)應(yīng)的表單項(xiàng)。我之前寫了一篇文章詳細(xì)的介紹了表單設(shè)計(jì)器的技術(shù)實(shí)現(xiàn)的文章, 大家感興趣也可以參考一下: Dooring可視化之從零實(shí)現(xiàn)動(dòng)態(tài)表單設(shè)計(jì)器。
我這里來(lái)簡(jiǎn)單實(shí)現(xiàn)一個(gè)基礎(chǔ)的表單渲染器模型:
- const FormEditor = (props: FormEditorProps) => {
- const { attrs, defaultValue, onSave } = props;
- const onFinish = (values: Store) => {
- // 保存配置項(xiàng)數(shù)據(jù)
- onSave && onSave(values);
- };
- const handlechange = (value) => {
- // 更新邏輯
- }
- const [form] = Form.useForm();
- return (
- <Form
- form={form}
- {...formItemLayout}
- onFinish={onFinish}
- initialValues={defaultValue}
- onValuesChange={handlechange}
- >
- {
- attrs.map((item, i) => {
- return (
- <React.Fragment key={i}>
- {item.type === 'Number' && (
- <Form.Item label={item.name} name={item.key}>
- <InputNumber />
- </Form.Item>
- )}
- {item.type === 'Text' && (
- <Form.Item label={item.name} name={item.key}>
- <Input placeholder={item.placeholder} />
- </Form.Item>
- )}
- {item.type === 'TextArea' && (
- <Form.Item label={item.name} name={item.key}>
- <TextArea rows={4} />
- </Form.Item>
- )}
- // 其他配置類型
- </React.Fragment>
- );
- })}
- </Form>
- );
- };
如果大家想看更完整的配置面板實(shí)現(xiàn), 可以參考開源項(xiàng)目 H5-Dooring | H5可視化編輯器
我們可以看看最終的配置面板實(shí)現(xiàn)效果:
控制中心概述 & 功能輔助設(shè)計(jì)
控制中心的實(shí)現(xiàn)主要是業(yè)務(wù)層的, 沒有涉及太多復(fù)雜的技術(shù), 所以這里我簡(jiǎn)單介紹一下。因?yàn)榭梢暬笃另?yè)面展示的信息有些可能是私密數(shù)據(jù), 只希望一部分人看到, 所以我們需要對(duì)頁(yè)面的訪問進(jìn)行控制。其次由于企業(yè)內(nèi)部業(yè)務(wù)戰(zhàn)略需求, 可能會(huì)對(duì)頁(yè)面進(jìn)行各種驗(yàn)證, 狀態(tài)校驗(yàn), 數(shù)據(jù)更新頻率等, 所以我們需要設(shè)計(jì)一套控制中心來(lái)管理。最基本的就是訪問控制, 如下:
功能輔助設(shè)計(jì) 主要是一些用戶操作上的優(yōu)化, 比如快捷鍵, 畫布縮放, 大屏快捷導(dǎo)航, 撤銷重做等操作, 這塊可以根據(jù)具體的產(chǎn)品需求來(lái)完善。大家后期設(shè)計(jì)搭建產(chǎn)品時(shí)也可以參考實(shí)現(xiàn)。
可視化大屏后期規(guī)劃和未來(lái)展望
為了實(shí)現(xiàn)更富有展現(xiàn)力, 滿足更多場(chǎng)景的可視化大屏引擎, 我們一方面需要提高引擎擴(kuò)展性, 一方面需要完善物料生態(tài), 其次只要與時(shí)俱進(jìn), 提供更多智能化的場(chǎng)景功能, 比如搭建埋點(diǎn), 數(shù)據(jù)預(yù)警等, 具體規(guī)劃如下:
- 豐富組件物料, 支持3D組件, 地理空間組件等
- 搭建埋點(diǎn), 方便后期對(duì)組件進(jìn)行分析
- 實(shí)現(xiàn)數(shù)據(jù)源, 事件機(jī)制閉環(huán)
- 支持用戶自定義組件
本文轉(zhuǎn)載自微信公眾號(hào)「趣談前端」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系趣談前端公眾號(hào)。












































