從零設(shè)計(jì)可視化大屏搭建引擎
相關(guān)文章: 從零開(kāi)發(fā)一款可視化大屏制作平臺(tái)
演示地址: V6可視化大屏編輯器
作者: 徐小夕
幾個(gè)月前我寫(xiě)了一篇關(guān)于從零開(kāi)發(fā)一款可視化大屏制作平臺(tái) 的文章, 簡(jiǎn)單概述了一下可視化大屏搭建平臺(tái)的一些設(shè)計(jì)思路和效果演示, 這篇文章我會(huì)就 如何設(shè)計(jì)可視化大屏搭建引擎 這一主題, 詳細(xì)介紹一下實(shí)現(xiàn)原理。
按照我一向的寫(xiě)作風(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)我們開(kāi)始進(jìn)入正文。
快速了解數(shù)據(jù)可視化
說(shuō)到數(shù)據(jù)可視化, 想必大家多多少少稍接觸過(guò), 從技術(shù)層面談, 最直觀(guān)的就是前端可視化框架, 比如:
echart antv Chart.js D3.js Vega
這些庫(kù)都能幫我們輕松制作可視化圖表。

從實(shí)用性的角度來(lái)談, 其最主要的意義就在于幫助用戶(hù)更好的分析和表達(dá)數(shù)據(jù)。所以說(shuō)談到數(shù)據(jù)可視化, 更多的是和各種圖表打交道, 通過(guò) 數(shù)據(jù) -> 圖表組合 -> 可視化頁(yè)面 這一業(yè)務(wù)流程, 就構(gòu)成了我們今天要研究的話(huà)題——設(shè)計(jì)可視化大屏搭建引擎。
如何設(shè)計(jì)通用的大屏搭建引擎
說(shuō)到 “引擎” 這個(gè)詞也許有種莫名的高大上, 其實(shí)在互聯(lián)網(wǎng)技術(shù)中, 我們經(jīng)常會(huì)聽(tīng)到各種相關(guān)的名詞,比如 “瀏覽器渲染引擎” , “規(guī)則引擎” , “圖像識(shí)別引擎” 等, 我覺(jué)得 “引擎” 的本質(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è)直觀(guān)特征:
可視化組件集 空間坐標(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)
俗話(huà)說(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)系”這一問(wèn)題。我們先來(lái)看一下實(shí)現(xiàn)效果:

有關(guān)拖拽的技術(shù)實(shí)現(xiàn), 我們可以利用原生 js 實(shí)現(xiàn), 也可以使用第三方成熟的拖拽庫(kù), 比如:
DnD React-Dragable react-moveable
我之前也開(kāi)源了一個(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);
// 自定義的拖拽開(kāi)始邏輯
}}
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è)置參考線(xiàn), 對(duì)齊線(xiàn), 吸附等, 并且可以在拖拽的不同時(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é)議。并且為了方便物料管理和查詢(xún), 我們還需要對(duì)物料進(jìn)行分類(lèi), 我的分類(lèi)如下:
可視化組件 (柱狀圖, 餅圖, 條形圖, 地圖可視化等) 修飾型組件 (圖片, 輪播圖, 修飾素材等) 文字類(lèi)組件 (文本, 文本跑馬燈, 文字看板)
具體的物料庫(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ù)過(guò)濾, 接入
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 , 也有專(zhuān)有的 props, 取決于我們?nèi)绾味x物料的Schema。
在設(shè)計(jì) Schema 前我們需要明確組件的屬性劃分, 為了滿(mǎn)足組件配置的靈活性和通用性, 我做了如下劃分:
外觀(guān)屬性 (組件寬高, 顏色, 標(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), 通過(guò) 解析引擎 動(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ì)類(lèi)似的通用schema。
我們通過(guò)以上設(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ī)制, 讓組件異步加載到畫(huà)布上, 而不是一次性加載幾十個(gè)幾百個(gè)組件(這樣的話(huà)頁(yè)面會(huì)有大量的白屏?xí)r間, 用戶(hù)體驗(yàn)極度下降)。
動(dòng)態(tài)加載器就是提供了這樣一種機(jī)制, 保證組件的加載都是異步的, 一方面可以減少頁(yè)面體積, 另一方面用戶(hù)可以更早的看到頁(yè)面元素。目前我們熟的動(dòng)態(tài)加載機(jī)制也有很多, Vue 和 React 生態(tài)都提供了開(kāi)箱即用的解決方案(雖然我們可以用 webpack 自行設(shè)計(jì)這樣的動(dòng)態(tài)模型, 此處為了提高行文效率, 我們直接基于現(xiàn)成方案封裝)。我們先看一下動(dòng)態(tài)渲染組件的過(guò)程:

上面的演示可以細(xì)微的看出從左側(cè)組件菜單拖動(dòng)某個(gè)組件圖標(biāo)到畫(huà)布上后, 真正的組件才開(kāi)始加載渲染。
這里我們以 umi3.0 提供的 dynamic 函數(shù)來(lái)最小化實(shí)現(xiàn)一個(gè)動(dòng)態(tài)渲染器. 如果不熟悉 umi 生態(tài)的朋友, 也不用著急, 看完我的實(shí)現(xiàn)過(guò)程和原理之后, 就可以利用任何熟悉的動(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)。我之前寫(xiě)了一篇文章詳細(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>
)}
// 其他配置類(lèi)型
</React.Fragment>
);
})}
</Form>
);
};
如果大家想看更完整的配置面板實(shí)現(xiàn), 可以參考開(kāi)源項(xiàng)目 H5-Dooring | H5可視化編輯器
我們可以看看最終的配置面板實(shí)現(xiàn)效果:

控制中心概述 & 功能輔助設(shè)計(jì)
控制中心的實(shí)現(xiàn)主要是業(yè)務(wù)層的, 沒(méi)有涉及太多復(fù)雜的技術(shù), 所以這里我簡(jiǎn)單介紹一下。因?yàn)榭梢暬笃另?yè)面展示的信息有些可能是私密數(shù)據(jù), 只希望一部分人看到, 所以我們需要對(duì)頁(yè)面的訪(fǎng)問(wèn)進(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)管理。最基本的就是訪(fǎng)問(wèn)控制, 如下:

功能輔助設(shè)計(jì) 主要是一些用戶(hù)操作上的優(yōu)化, 比如快捷鍵, 畫(huà)布縮放, 大屏快捷導(dǎo)航, 撤銷(xiāo)重做等操作, 這塊可以根據(jù)具體的產(chǎn)品需求來(lái)完善。大家后期設(shè)計(jì)搭建產(chǎn)品時(shí)也可以參考實(shí)現(xiàn)。
可視化大屏后期規(guī)劃和未來(lái)展望
為了實(shí)現(xiàn)更富有展現(xiàn)力, 滿(mǎ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) 支持用戶(hù)自定義組件
如果大家對(duì)可視化搭建或者低代碼/零代碼感興趣, 也可以參考我往期的文章或者在評(píng)論區(qū)交流你的想法和心得。
往期文章
從零使用electron搭建桌面端可視化編輯器Dooring (低代碼)可視化搭建平臺(tái)數(shù)據(jù)源設(shè)計(jì)剖析 從零搭建一款PC頁(yè)面編輯器PC-Dooring 如何搭積木式的快速開(kāi)發(fā)H5頁(yè)面?
點(diǎn)個(gè)在看你最好看

