基于React/Vue搭建一個(gè)通用的表單管理配置平臺(tái)
前言
熟悉我的朋友可能會(huì)知道,我一向是不寫(xiě)熱點(diǎn)的。為什么不寫(xiě)呢?是因?yàn)槲也魂P(guān)注熱點(diǎn)嗎?其實(shí)也不是。有些事件我還是很關(guān)注的,也確實(shí)有不少想法和觀(guān)點(diǎn)。但我一直奉行一個(gè)原則,就是:要做有生命力的內(nèi)容。
這篇文章是一篇應(yīng)用性極強(qiáng)的文章,我們通過(guò)一個(gè)實(shí)際的應(yīng)用場(chǎng)景,去解決某一類(lèi)的問(wèn)題,提供一種或者幾種解決方案,來(lái)探索技術(shù)的魅力。接下來(lái)筆者主要分析表單定制平臺(tái)的實(shí)現(xiàn)思路和技術(shù)方案,來(lái)實(shí)現(xiàn)一個(gè)類(lèi)似于金數(shù)據(jù)或者問(wèn)卷星一樣的表單配置平臺(tái),大家也可以基于此方案,擴(kuò)展出功能更加強(qiáng)大的可視化平臺(tái)。
為什么要做一個(gè)這樣的平臺(tái)呢?一方面是因?yàn)楣P者多年來(lái)一直服務(wù)于B端產(chǎn)品,對(duì)于動(dòng)態(tài)表單以及配置化表單有一定的項(xiàng)目積累,并且深知配置化表單的價(jià)值所在。舉一個(gè)很傳統(tǒng)的B端表單配置化的例子:傳統(tǒng)2B企業(yè)在提供saas服務(wù)時(shí),為了滿(mǎn)足不同企業(yè)的定制化需求,往往會(huì)給企業(yè)客戶(hù)提供定制化或者自由配置的功能,如下圖:
圖片
對(duì)于saas系統(tǒng)而言,軟件即服務(wù),在提供基礎(chǔ)服務(wù)的同時(shí),同樣要滿(mǎn)足用戶(hù)個(gè)性化需求,所以傳統(tǒng)的saas軟件提供商往往會(huì)提供給客戶(hù)自由配置的空間,這種自由配置的橋梁就是通過(guò)表單,舉一個(gè)簡(jiǎn)單的例子:
圖片
通過(guò)這種方法就可以定制不同風(fēng)格的企業(yè)產(chǎn)品,這里只是舉了個(gè)比較簡(jiǎn)單的例子,往往實(shí)際項(xiàng)目中會(huì)更加復(fù)雜,可能會(huì)有幾十個(gè)配置項(xiàng),當(dāng)然這種模式是比較傳統(tǒng)的配置化方案,也僅僅是saas軟件提供的很小的一個(gè)服務(wù)模塊。目前主流的做法是采用可視化方案,而且國(guó)內(nèi)也有非常成熟的方案,但基本的思想是一致的,只不過(guò)后者的體驗(yàn)更好,操作難度更低。
筆者簡(jiǎn)單介紹一下saas,方便大家更容易理解其模式:
saas(軟件即服務(wù))是一種云計(jì)算產(chǎn)品,為用戶(hù)提供對(duì)供應(yīng)商云端軟件的訪(fǎng)問(wèn)。用戶(hù)無(wú)需在其本地設(shè)備上安裝應(yīng)用。相反,應(yīng)用駐留在遠(yuǎn)程云網(wǎng)絡(luò)中,通過(guò) Web 或 API 進(jìn)行訪(fǎng)問(wèn)。通過(guò)應(yīng)用,用戶(hù)可以存儲(chǔ)和分析數(shù)據(jù),并可進(jìn)行項(xiàng)目協(xié)作。
類(lèi)似的云計(jì)算產(chǎn)品也有很多,比如Paas(平臺(tái)即服務(wù)),Iaas(基礎(chǔ)架構(gòu)即服務(wù))等,感興趣的朋友可以學(xué)習(xí)了解一下。
以上介紹更多的是為了讓大家理解筆者設(shè)計(jì)這套平臺(tái)的基本背景,我們還可以舉個(gè)更實(shí)際的例子就是金數(shù)據(jù)或者問(wèn)卷星的表單配置模式,用戶(hù)可以在管理后臺(tái)定制自己的表單,并生成一個(gè)可訪(fǎng)問(wèn)的鏈接來(lái)向目標(biāo)用戶(hù)發(fā)放問(wèn)卷,填寫(xiě)信息,收集信息,最后實(shí)現(xiàn)數(shù)據(jù)分析的目的。
本文介紹的表單定制平臺(tái),也同樣支持表單管理,表單數(shù)據(jù)分析, 表單數(shù)據(jù)收集, 表單定制等功能, 筆者將采用比較熟悉的技術(shù)棧react以及第三方ui庫(kù)antd4.0來(lái)開(kāi)發(fā), 后端采用node + koa來(lái)設(shè)計(jì)路由接口.
設(shè)計(jì)思路
圖片
實(shí)現(xiàn)效果與分析
1. 表單定制管理列表
圖片
管理列表主要用來(lái)查看我們配置的表單模板,分析不同表單模板收集的數(shù)據(jù),對(duì)表單模板進(jìn)行編輯刪除等操作.
2. 表單定制頁(yè)面
圖片
圖片
由上圖可知表單定制頁(yè)面主要用來(lái)編輯自定義表單模板,我們可以添加表單標(biāo)題,表單字段等,目前提供了幾種自定義表單控件如下:
- 文本框
- 多行文本框
- 下拉框
- 單選框
- 復(fù)選框
- 文件上傳控件
基本涵蓋了我們所需要的所有表單業(yè)務(wù)場(chǎng)景.由上圖可知我們可以在任意位置插入自定義字段,同時(shí)可以編輯修改刪除表單字段.如果想象力再大一點(diǎn),我們可以基于它來(lái)實(shí)現(xiàn)不僅僅是表單問(wèn)卷型應(yīng)用,還可以實(shí)現(xiàn)答題,發(fā)布內(nèi)容等場(chǎng)景.(后期可支持富文本控件)
3. 草稿管理
圖片
草稿箱設(shè)計(jì)的目的是方便使用者在配置表單的過(guò)程中不確定是否符合需求或者由于某種臨時(shí)性舉動(dòng)而無(wú)法繼續(xù)配置,這個(gè)時(shí)候可以將以配置好的內(nèi)容存入草稿箱,下次繼續(xù)編輯,所以筆者專(zhuān)門(mén)設(shè)計(jì)了草稿箱管理列表,一旦用戶(hù)存在草稿,會(huì)在管理頁(yè)面通知用戶(hù)并顯示草稿的數(shù)量.作為一個(gè)追求體驗(yàn)的技術(shù)人,這一塊的設(shè)計(jì)還是相當(dāng)有必要的.
4. 生成前臺(tái)表單訪(fǎng)問(wèn)鏈接
圖片
當(dāng)我們配置好表單之后,我們點(diǎn)擊保存, 會(huì)生成一個(gè)前臺(tái)訪(fǎng)問(wèn)地址,實(shí)時(shí)訪(fǎng)問(wèn)表單信息,如下圖為點(diǎn)擊鏈接之后的頁(yè)面:
圖片
我們也可以根據(jù)自己的風(fēng)格,設(shè)計(jì)自己的表單錄入頁(yè)面, 具體如何實(shí)現(xiàn)這樣的過(guò)程, 后面我會(huì)詳細(xì)介紹.
5. 查看用戶(hù)已有數(shù)據(jù)錄入
圖片
圖片
我們可以通過(guò)點(diǎn)擊"查看數(shù)據(jù)"來(lái)訪(fǎng)問(wèn)收集到的表單數(shù)據(jù),并通過(guò)可視化的工具對(duì)數(shù)據(jù)做分析比較,同時(shí)我們也可以在數(shù)據(jù)列表中刪除數(shù)據(jù),來(lái)控制我們數(shù)據(jù)展示的純凈.
6. 表單數(shù)據(jù)分析
圖片
圖片
圖片
收集到數(shù)據(jù)只有,我們會(huì)自動(dòng)集成幾個(gè)可視化組件來(lái)分析表單數(shù)據(jù),以上是筆者列出的幾個(gè)可視化組件,基于antv G2來(lái)封裝.
應(yīng)用場(chǎng)景
以上主要介紹了自定義表單定制平臺(tái)的一些功能和交互效果, 我們可以利用該平臺(tái)做很多有意思的事情.因?yàn)楸韱蔚某橄笫菙?shù)據(jù),我們拿到定制化的表單json數(shù)據(jù)之后,我們可以有不同的展現(xiàn)形式,比如用戶(hù)的問(wèn)卷調(diào)查, 網(wǎng)站平臺(tái)的投票, 答題頁(yè)面, 發(fā)布動(dòng)態(tài)等功能,如下圖配置:
圖片
圖片
以上配置可以實(shí)現(xiàn)類(lèi)似于微信的發(fā)布朋友圈的功能, 然后我們可以通過(guò)前端的手段根據(jù)用戶(hù)發(fā)表的數(shù)據(jù)渲染成一個(gè)朋友圈列表.
如果我們?cè)俅蜷_(kāi)自己的腦洞,我們可以這樣配置,配置一個(gè)這樣的表單,表單包括一個(gè)文件上傳控件和n個(gè)文本輸入控件,如下圖:
圖片
將這樣的表單配置到H5管理模塊,我們只需要上傳三張圖,然后填寫(xiě)好對(duì)應(yīng)的配文,然后利用市面上成熟的H5全屏滾動(dòng)插件,就能輕松的定制各種H5活動(dòng)頁(yè)面了。該方案已被筆者的很多子系統(tǒng)使用,效果還是非常好的。
當(dāng)然基于該平臺(tái)甚至能直接配置小型的宣傳網(wǎng)站,還有更多想象空間,期待大家去挖掘。
代碼實(shí)現(xiàn)
要想開(kāi)發(fā)這樣一個(gè)表單定制平臺(tái), 核心在于如何實(shí)現(xiàn)表單動(dòng)態(tài)配置的機(jī)制.這里筆者將其劃分為兩部分:基礎(chǔ)表單物料和表單編輯生成器, 如下圖所示拆分圖:
圖片
接下來(lái)我們一步步實(shí)現(xiàn)以上兩個(gè)核心模塊。
1. 基礎(chǔ)表單物料
基礎(chǔ)表單物料主要是為了用戶(hù)選擇自定義表單控件使用,我們常用的表單動(dòng)態(tài)渲染有map循環(huán)+條件判斷和單層map+對(duì)象法,前者如果要渲染一個(gè)動(dòng)態(tài)表單,可能實(shí)現(xiàn)如下:
{
list.map((item, i) => {
return <React.Fragment key={i}>
{
item.type === 'input' && <Input />
}
{
item.type === 'radio' && <Radio />
}
// ...
</React.Fragment>
})
}
但是這樣做有個(gè)明顯的缺點(diǎn)就是會(huì)產(chǎn)生很多沒(méi)必要的判斷,如果對(duì)于復(fù)雜表單,性能往往很低,所以筆者采用后者來(lái)實(shí)現(xiàn),復(fù)雜度可以降到O(n).我們先來(lái)做配置模版:
// 基礎(chǔ)模版數(shù)據(jù)
const tpl = [
{
label: '文本框',
placeholder: '請(qǐng)輸入內(nèi)容',
type: 'text',
value: '',
index: uuid(5)
},
{
label: '單選框',
type: 'radio',
option: [{label: '男', value: 0}, {label: '女', value: 1}],
index: uuid(5)
},
{
label: '復(fù)選框',
type: 'checkbox',
option: [{label: '男', value: 0}, {label: '女', value: 1}],
index: uuid(5)
},
{
label: '多行文本',
placeholder: '請(qǐng)輸入內(nèi)容',
type: 'textarea',
index: uuid(5)
},
{
label: '選擇框',
placeholder: '請(qǐng)選擇',
type: 'select',
option: [{label: '中國(guó)', value: 0}, {label: '俄羅斯', value: 1}],
index: uuid(5)
},
{
label: '文件上傳',
type: 'upload',
index: uuid(5)
}
]
// 模版渲染組件
const tplMap = {
text: {
component: (props) => {
const { placeholder, label } = props
return <div className={styles.fieldOption}><span className={styles.fieldLabel}>{label}:</span><Input placeholder={placeholder} /></div>
}
},
textarea: {
component: (props) => {
const { placeholder, label } = props
return <div className={styles.fieldOption}><span className={styles.fieldLabel}>{label}:</span><TextArea placeholder={placeholder} /></div>
}
},
radio: {
component: (props) => {
const { option, label } = props
return <div className={styles.fieldOption}>
<span className={styles.fieldLabel}>{label}:</span>
<Radio.Group>
{
option && option.map((item, i) => {
return <Radio style={radioStyle} value={item.value} key={item.label}>
{ item.label }
</Radio>
})
}
</Radio.Group>
</div>
}
},
checkbox: {
component: (props) => {
const { option, label } = props
return <div className={styles.fieldOption}>
<span className={styles.fieldLabel}>{label}:</span>
<Checkbox.Group>
<Row>
{
option && option.map(item => {
return <Col span={16} key={item.label}>
<Checkbox value={item.value} style={{ lineHeight: '32px' }}>
{ item.label }
</Checkbox>
</Col>
})
}
</Row>
</Checkbox.Group>
</div>
}
},
select: {
component: (props) => {
const { placeholder, option, label } = props
return <div className={styles.fieldOption}>
<span className={styles.fieldLabel}>{label}:</span>
<Select placeholder={placeholder} style={{width: '100%'}}>
{
option && option.map(item => {
return <Option value={item.value} key={item.label}>{item.label}</Option>
})
}
</Select>
</div>
}
},
upload: {
component: (props) => {
return <div className={styles.fieldOption}>
<span className={styles.fieldLabel}>{props.label}:</span>
<Upload
listType="picture-card"
className="avatar-uploader"
showUploadList={false}
actinotallow="https://www.mocky.io/v2/5cc8019d300000980a055e76"
>
<div>+</div>
</Upload>
</div>
}
}
}
export {
tpl,
tplMap
}
基礎(chǔ)物料在下圖所示中使用:
圖片
當(dāng)我們要添加一個(gè)表單項(xiàng)時(shí),我們就可以在左邊預(yù)覽操作區(qū)看到添加的項(xiàng),并可以基于表單編輯生成器來(lái)編輯表單字段。
2. 表單編輯生成器
表單編輯生成器分為2部分, 第一部分是用來(lái)生成表單項(xiàng)的容器組件,封裝了添加,刪除,編輯操作功能,代碼如下:
// 表單容器組件
const BaseFormEl = (props) => {
const {isEdit, onEdit, onDel, onAdd} = props
const handleEdit = (v) => {
onEdit && onEdit(v)
}
return <div className={styles.formControl}>
<div className={styles.formItem}>{ props.children }</div>
<div className={styles.actionBar}>
<span className={styles.actionItem} onClick={onDel}><MinusCircleOutlined /></span>
<span className={styles.actionItem} onClick={onAdd}><PlusCircleOutlined /></span>
<span className={styles.actionItem} onClick={handleEdit}><EditOutlined /></span>
</div>
</div>
}
第二部分主要用來(lái)渲染操作區(qū)模版,基于BaseFormEl包裝不同類(lèi)型的表單組件, 這里舉一個(gè)比較復(fù)雜的select來(lái)說(shuō)明,其他表單控件類(lèi)似:
const formMap = {
title: {},
text: {},
textarea: {},
radio: {},
checkbox: {},
select: {
component: (props) => {
const { onDel, onAdd, onEdit, curIndex, index, type, label, placeholder, required, message, option } = props
return <BaseFormEl
onDel={onDel.bind(this, index)}
onAdd={onAdd.bind(this, index)}
onEdit={onEdit.bind(this, {index, type, placeholder, label, option, required})}
isEdit={curIndex === index}
>
<Form.Item name={label} label={label} rules={[{ message, required }]}>
<Select placeholder={placeholder}>
{
option && option.map(item => {
return <Option value={item.value} key={item.label}>{item.label}</Option>
})
}
</Select>
</Form.Item>
</BaseFormEl>
},
editAttrs: [
{
title: '字段名稱(chēng)',
key: 'label'
},
{
title: '選項(xiàng)',
key: 'option'
},
{
title: '提示文本',
key: 'placeholder'
},
{
title: '是否必填',
key: 'required'
},
]
},
upload: {}
}
editAttrs主要用來(lái)渲染編輯列表,說(shuō)明哪些表單項(xiàng)可以編輯,這部分代碼比較簡(jiǎn)單,這里直接用圖舉例:
圖片
最后我們來(lái)渲染表單生成器組件:
export default (props) => {
const {
formData,
handleDelete,
handleAdd,
handleEdit,
curEditRowIdx
} = props
return <Form name="customForm">
{
formData && formData.map(item => {
let CP = formMap[item.type].component
return <CP {...item} key={item.index}
onDel={handleDelete}
onAdd={handleAdd}
onEdit={handleEdit}
curIndex={curEditRowIdx}
/>
})
}
</Form>
}
至此,基本功能模塊已經(jīng)開(kāi)發(fā)完成,我們只需要將這些物料和組件導(dǎo)入到編輯頁(yè)面,基于業(yè)務(wù)來(lái)操作和請(qǐng)求即可。由于實(shí)現(xiàn)該案例還是有一定復(fù)雜度的,筆者沒(méi)有將所有組件都一一寫(xiě)出來(lái),希望為大家提供一個(gè)思考空間,后續(xù)筆者將會(huì)把該平臺(tái)整合到筆者的開(kāi)源CMS系統(tǒng)中,供大家學(xué)習(xí)使用。有關(guān)nodejs部分的內(nèi)容,由于筆者后期會(huì)陸續(xù)整理,如果有其他疑問(wèn),可以和筆者多交流。