如何做一個看板搭建系統(tǒng)
如何做一個看板搭建系統(tǒng)
http://zoo.zhengcaiyun.cn/blog/article/buildingsystem
一、什么是數(shù)據(jù)看板,數(shù)據(jù)看板有什么用
在解釋數(shù)據(jù)看板概念之前,我們要先知道,什么是數(shù)據(jù)可視化。
“數(shù)據(jù)可視化被許多學(xué)科視為與視覺傳達(dá)含義相同的現(xiàn)代概念。它涉及到數(shù)據(jù)的可視化表示的創(chuàng)建和研究。為了清晰有效地傳遞信息,數(shù)據(jù)可視化使用統(tǒng)計圖形、圖表、信息圖表和其他工具。可以使用點(diǎn)、線或條對數(shù)字?jǐn)?shù)據(jù)進(jìn)行編碼,以便在視覺上傳達(dá)定量信息。有效的可視化可以幫助用戶分析和推理數(shù)據(jù)和證據(jù)。它使復(fù)雜的數(shù)據(jù)更容易理解和使用。- 維基百科
數(shù)據(jù)看板即是數(shù)據(jù)可視化的載體,通過合理的頁面布局、效果設(shè)計來將可視化數(shù)據(jù)更好的展現(xiàn)。
個人認(rèn)為,數(shù)據(jù)看板的作用大致為以下兩種:
1、掌握情況
通過數(shù)據(jù)呈現(xiàn),決策者們能較為清晰地掌握自己產(chǎn)品的運(yùn)營情況。
2、問題解決
通過數(shù)據(jù)分析,能夠通過數(shù)據(jù)可視化,從動態(tài)數(shù)據(jù)中提煉出規(guī)律,發(fā)現(xiàn)不符合預(yù)期的部分并給出修改意見。
二、配置文件數(shù)據(jù)結(jié)構(gòu)設(shè)計
假設(shè)有如下這樣的一個看板頁面,讓你用一份 json 文件來記錄組件的信息,類似,寬高、位置以及標(biāo)題等等,試想一下你會怎么設(shè)計 json 的數(shù)據(jù)結(jié)構(gòu)(可以不用關(guān)心具體的值是什么)。
這個問題并不是很難,相信大部分人都能設(shè)計一份自己的數(shù)據(jù)結(jié)構(gòu),比如我設(shè)計的結(jié)構(gòu)就是下面這樣:
[
{
"type": 'line', // 類型
"id": 1, // 唯一標(biāo)識
"data": [], // 數(shù)據(jù)存放
"title": "貨物銷售情況", // 組件標(biāo)題
"layout": { x: 0, y: 0, w: 3, h: 2 }, // 頁面布局信息(坐標(biāo)、寬高)
},
{
"type": 'bar',
"id": 2,
"data": [],
"title": "貨物留存情況",
"layout": { i: 'c', x: 3, y: 0, w: 3, h: 2 },
}
]
有了這樣一份數(shù)據(jù)結(jié)構(gòu)之后,接下來,我們接下來就要開發(fā)組件,比如{type:line}的配置項(xiàng)就去用 line 組件渲染,并且我們需要把數(shù)據(jù)還有一些其他配置項(xiàng)傳給組件。關(guān)于每個組件的開發(fā),這里就不深入探討了,需要考慮的就是你的內(nèi)部組件要具有接受數(shù)據(jù)并處理配置的能力。
三、組件容器
現(xiàn)在組件有了,配置也有了,接下來就很簡單了,根組件直接循環(huán)遍歷下,就可以了,代碼如下:
<div>
{widgets.map(widget => {
const WidgetComp = getWidgetComp(widget.type);
return <WidgetComp config={widget} key={widget.id} />
})}
</div>
好了,大功告成,頁面渲染完成!
先別著急,我們想想看,是不是每個組件,我們都需要去做同樣的工作,比如數(shù)據(jù)請求、布局處理等等。
這時候,我們就需要在一個統(tǒng)一的地方去做這些同樣且重復(fù)的工作。這里我想到了兩種方式:1、公共的 util 函數(shù)。2、給所有組件增加一層包裹容器。
第一種方法雖然可以滿足我們的要求,但是還是存在局限性,那就是當(dāng)我們想要在 dom 上做一些處理的時候,還是不可避免的會有重復(fù)代碼量,比如給組件添加點(diǎn)擊事件、設(shè)置 dom 的 style 屬性等。
我個人比較推薦第二種方法,也就是組件容器。一個組件容器大概長這樣:
const Widget = ({config}) => {
// 組件點(diǎn)擊事件
const handleClick=() => {/**/};
// 布局處理
const handleLayout=() => {/**/};
// 獲取組件數(shù)據(jù)
const getWidgetData=() => {/**/};
// 其他的一些公共邏輯
.......
const WidgetComp = getWidgetComp(widget.type);
return (
return <WidgetComp config={widget} />
)
}
對應(yīng)的根組件代碼可以改成這樣:
<div>
{widgets.map(widget => <Widget config={widget} key={widget.id} />)}
</div>
四、配置文件的編輯
上面的工作做完之后,我們可以做一個簡單的渲染了,但是既然是搭建系統(tǒng),怎么可能僅僅滿足于渲染呢?接下來,我們要考慮下,給搭建的用戶提供一個可以修改配置的地方。為了統(tǒng)一口徑,我們把使用配置的地方,稱用戶側(cè),修改配置的地方,稱編輯側(cè)。
一個編輯側(cè)可能長這樣:
大致分為三個區(qū)域,組件商店、展示區(qū)域和配置區(qū)域,這里我們先說右側(cè)的組件配置區(qū)域。
我們這里拿 line 組件 舉例。假設(shè),現(xiàn)在產(chǎn)品小姐姐給你提了第一個需求,需要這個 line 組件支持修改名稱。so easy!直接寫個 form 表單:
<Form form={form}>
<Item name="name" label="名稱" rules={[{ required: true, message: '請輸入' }]}>
<Input placeholder="請輸入" />
</Item>
</Form>
修改完之后,直接把新的名稱發(fā)給后端存起來就完事了。
一天后,產(chǎn)品小姐姐又提了另一個需求,bar 組件需要支持修改寬度。沒辦法,接著改,不能讓產(chǎn)品小姐姐看不起!于是你的代碼可能變成了這樣:
const renderForm = ({type}) => {
if(type === 'line') {
return (
<Item name="name" label="名稱" rules={[{ required: true, message: '請輸入' }]}>
<Input placeholder="請輸入" />
</Item>
)
} else if(type === 'bar') {
return (
<Item name="width" label="寬度" rules={[{ required: true, message: '請輸入' }]}>
<Input placeholder="請輸入" />
</Item>
)
}
}
return (<Form form={form}>
{renderForm()}
</Form>)
緊接著,產(chǎn)品小姐姐的需求與日增多,組件數(shù)量也越來越龐大,你的 if else 越寫越多....。所以我們需要一個統(tǒng)一的配置器,和一個統(tǒng)一的描述組件配置的 schema 文件。
先說組件的 schema 文件,每一個組件都配帶一個 schema 文件,schema 里主要記錄當(dāng)前組件支持修改的配置項(xiàng)(fields),和當(dāng)前組件配置項(xiàng)的默認(rèn)值(models)。類似這樣:
{
"fields": [{
"label": "名稱",
"type": "input",
"name": "name"
}],
"models": {
"name": "默認(rèn)名稱"
}
}
當(dāng)點(diǎn)擊一個組件的編輯按鈕時,根據(jù)組件類型獲取到對應(yīng)的 schema 文件,將組件配置項(xiàng)默認(rèn)值 models 和從后端拿到的數(shù)據(jù),做一個 merge,將 merge 后的數(shù)據(jù)和 fieds 傳給配置器。
配置器要做的工作就是,根據(jù) fileds 動態(tài)渲染表單,關(guān)于表單動態(tài)渲染,我們團(tuán)隊(duì)有一篇不錯的文章《表單數(shù)據(jù)形式配置化設(shè)計》(https://juejin.cn/post/7119639489567260686),大家有興趣可以參考下。
總結(jié)下編輯側(cè)的工作,第一步,拿到對應(yīng)組件的 schema 文件,傳給配置器。第二步,配置器根據(jù) fileds 動態(tài)渲染表單,并根據(jù)后端返回數(shù)據(jù)和 schema 中的默認(rèn)數(shù)據(jù),用作表單回顯。最后就是,搭建用戶修改配置項(xiàng),再把修改后的數(shù)據(jù)發(fā)送給后端保存。
五、從遠(yuǎn)程組件商店加載組件
以上已經(jīng)完成了配置的產(chǎn)出、使用和修改。接下來我們再思考思考組件。我們現(xiàn)在的組件都是存在本地的,后期隨著組件數(shù)量的增多,頁面 js 文件肯定會越來越大,即使你使用了代碼分割,也不可避免會導(dǎo)致 build 后的包體積越來越大。
所以我們需要一個遠(yuǎn)端存放組件的地方,也就是組件商店。
那么商店既然是遠(yuǎn)端的,那把這個商店建在哪里呢?我們團(tuán)隊(duì)的解決方案就是,把每個組件打上版本號上傳到靜態(tài)服務(wù)器上這樣,版本號的作用這里先不管,這樣不管在編輯側(cè)還是用戶側(cè),我們可以根據(jù)項(xiàng)目的配置數(shù)據(jù)遠(yuǎn)程加載組件,關(guān)于遠(yuǎn)程組件的加載方案,可以參考下我們團(tuán)隊(duì)另一篇寫的不錯的文章《淺談低代碼平臺遠(yuǎn)程組件加載方案》(https://juejin.cn/post/7127440050937151525)。
當(dāng)然,一個組件商店不僅于此,我們目前只實(shí)現(xiàn)了一些基本功能,一個完整的組件商店功能包括:
組件線上編輯(上傳)模塊。
組件審核模塊。
組件更新/發(fā)布模塊。
組件管理(上架/下架/刪除/下載/版本)。
六、靜態(tài)化
現(xiàn)在我們在編輯側(cè)修改提交,用戶側(cè)就能實(shí)時地得到修改后的配置了,對應(yīng)的頁面刷新就會變化。
可是這樣會導(dǎo)致一個問題,假設(shè)以前的老版本 v1.0 需要修改到新版本 v2.0,并且工作量很大,需要兩天才能完成,那么你第一天只能改一半,第二天早上你準(zhǔn)備改另一半的時候,可能你們公司的投訴電話已經(jīng)被打爆了,因?yàn)?,你的客戶早上打開頁面的時候,是你第一天只改了一半配置的頁面。
怎么解決這個問題呢?答案是增加一個發(fā)布操作,只有發(fā)布過后,你修改的配置才會在用戶側(cè)生效。
但是這樣還是會有問題,假設(shè)你在項(xiàng)目 B 修改了一個組件,該組件在項(xiàng)目 A 也用到了,那一旦該組件引發(fā)了 bug,項(xiàng)目 A,項(xiàng)目 B都發(fā)出問題,如果是十幾個項(xiàng)目,那就會引起十幾個項(xiàng)目的問題。
基于此,我們需要思考一種方法,針對發(fā)布過后的項(xiàng)目,即使對應(yīng)組件有修改,也不會影響到它們,我們團(tuán)隊(duì)使用的解決方案就是組件靜態(tài)化。
大致思路就是,發(fā)布時,首先獲取對應(yīng)的項(xiàng)目配置,根據(jù)項(xiàng)目配置獲取組件列表。然后根據(jù)列表,獲取遠(yuǎn)程組件商店的 js 文件,將獲取到的 js 文件插入到對應(yīng)的 html 模版中。最后,將拼裝好的 html 放到靜態(tài)服務(wù)器上。
這樣,后續(xù)用戶側(cè)只需要訪問靜態(tài)服務(wù)器上的 html 就可以了,即使組件也修改,已發(fā)布的項(xiàng)目只要不重新發(fā)布,就不會影響已經(jīng)發(fā)布的項(xiàng)目。
“Tips: 進(jìn)一步的話,我們可以把項(xiàng)目配置也做成靜態(tài)化,這樣,第一可以省去后端同學(xué)同步兩份不同環(huán)境數(shù)據(jù)的工作量,但對于前端來說,就是順手的事。第二,方便前端自己管理已發(fā)布的配置數(shù)據(jù)。
但是如果組件修改,后續(xù)已發(fā)布的項(xiàng)目也需要重新發(fā)布呢?怎么盡量減少發(fā)布風(fēng)險呢?這個就需要做組件的版本管理和容器的版本管理了。
七、組件和容器的版本管理
前面我們在介紹組件商店的時候說到,上傳組件的時候,我們給對應(yīng)組件打上了版本號,后續(xù)組件有修改的時候,修改過的組件,會被打上新的版本號。這樣,針對已發(fā)布的項(xiàng)目,只要不更新對應(yīng)組件的版本號,便不會影響對應(yīng)的項(xiàng)目組件了。
那為什么要做容器的版本管理呢?前面我們介紹了組件容器的作用就是處理組件的通用邏輯,如果說組件的修改有可能會影響到其他項(xiàng)目,那么組件容器的修改就是一定會影響到其他項(xiàng)目。所以容器的版本管理比組件的版本管理更加重要。思路其實(shí)跟組件差不多,我們可以把容器理解成一個特殊的組件,跟組件不同的是,這個組件不在配置文件解析出來的組件列表中。
八、組件之間的通信
在真實(shí)的搭建場景中,避免不了的就是,組件之間需要通信,比如,點(diǎn)擊組件 A,組件 B 需要做出對應(yīng)的響應(yīng)。我們在這里借鑒的是【發(fā)布-訂閱】。
我們設(shè)計一個事件調(diào)度中心,可以處理所有的事件注冊與事件響應(yīng)。訂閱者發(fā)布對應(yīng)的事件,事件調(diào)度中心負(fù)責(zé)將事件放入事件池中。發(fā)布者負(fù)責(zé)在對應(yīng)的時機(jī)觸發(fā)對應(yīng)事件,上面例子中,組件 A 就是發(fā)布者,當(dāng)組件 A 被點(diǎn)擊的時候,就是觸發(fā)事件的對應(yīng)時機(jī)。事件在調(diào)度中心出發(fā)后,再將結(jié)果反饋給訂閱者。訂閱者拿到反饋結(jié)果做出行為。
關(guān)于事件的配置描述的數(shù)據(jù)結(jié)構(gòu),形式有很多種,這里貼一份我們目前項(xiàng)目中使用的數(shù)據(jù)結(jié)構(gòu):
export default {
publish:[ // 事件發(fā)布
{
name: "電子賣場預(yù)警發(fā)布/流程/onChange", // 發(fā)布事件名
reflectName: "onChange" // 什么行為觸發(fā)該事件
}
],
subscribe: [ // 事件訂閱
{
list: ["電子賣場預(yù)警發(fā)布/onChange"], // 訂閱的事件集合
action: "setClass", // 事件行為,就是你訂閱的事件被觸發(fā)后,你要干什么
handler: "function parse(params) {? return {};? }" // 結(jié)合事件行為與該事件的返回值,組件做出自身行為
}
]
}
九、參考
1、《如何設(shè)計可視化搭建平臺的組件商店》(https://juejin.cn/post/6986824393653485605)
作者:立航
歡迎關(guān)注微信公眾號 :前端民工