廢話少說(shuō)直接上需求背景和實(shí)現(xiàn)思路
1.需求背景
我們希望小程序中某個(gè)頁(yè)面中的用戶輸入數(shù)據(jù)表單上可以在后臺(tái)的cms系統(tǒng)或者是可以通過(guò)接口進(jìn)行動(dòng)態(tài)控制的 ;也就是說(shuō)我們只要通過(guò)在管理系統(tǒng)進(jìn)行勾選以后,編輯,點(diǎn)擊生成頁(yè)面所對(duì)應(yīng)的表單部分就會(huì)出現(xiàn)我們動(dòng)態(tài)配置以后的表單(包括表單的個(gè)數(shù),每個(gè)表單的label,類型,id,默認(rèn)值等)
2.起初的實(shí)現(xiàn)思路
最早我拿到這個(gè)需求的時(shí)候,如果希望實(shí)現(xiàn)這種后臺(tái)配置好,前端小程序動(dòng)態(tài)解析生成表單的過(guò)程,主要有這三種方式:
1.后臺(tái)配置的表單元素是html富文本,小程序前端通過(guò)三方控件wxParse進(jìn)行動(dòng)態(tài)解析
2.將前端的表單抽離成最小單元組件并以u(píng)md形式的組件,放到服務(wù)端,小程序拉取服務(wù)端umd的組件進(jìn)行動(dòng)態(tài)解析
3.將前端的表單抽離成最小單元組件不放在遠(yuǎn)程服務(wù)器,只放在前端,后臺(tái)通過(guò)一套虛擬語(yǔ)法結(jié)構(gòu)(AST)對(duì)表單類型,id,規(guī)則進(jìn)行描述,小程序前端動(dòng)態(tài)解析
以上三種方式各自的問(wèn)題
方式1問(wèn)題:
wxparse 確實(shí)可以進(jìn)行對(duì)富文本解析成表單,但是無(wú)法將富文本內(nèi)部的行為,數(shù)據(jù)進(jìn)行共享出來(lái)(方案pass掉)
方式2問(wèn)題:
將前端的表單抽離成最小單元組件并以u(píng)md形式的組件,放到服務(wù)端,目前小程序并不支持umd形式的打包,并且小程序無(wú)法加載遠(yuǎn)程組件
咳咳 那就只剩下第三種了!?。?/h4>
本來(lái)想用1或者2做的高大上點(diǎn),但是最終只能這種方式,容我傷心1秒鐘
切入主題 制作動(dòng)態(tài)表單的具體實(shí)現(xiàn)
1.方案須知
最終我們選擇方案:
將前端的表單抽離成最小單元組件不放在遠(yuǎn)程服務(wù)器,只放在前端,后臺(tái)通過(guò)一套虛擬語(yǔ)法結(jié)構(gòu)(AST)數(shù)據(jù)結(jié)構(gòu)對(duì)表單個(gè)數(shù),類型,id,規(guī)則進(jìn)行描述,小程序前端動(dòng)態(tài)解析
2. 設(shè)計(jì)動(dòng)態(tài)表單語(yǔ)法結(jié)構(gòu)(AST)
2.1首先我們來(lái)看 給定要求的設(shè)計(jì)圖
image.png
2.1 接上 如果我們希望是此類的表單結(jié)構(gòu),那我們?cè)谠O(shè)計(jì)表單虛擬語(yǔ)法結(jié)構(gòu),
將前端的表單抽離成最小單元組件不放在遠(yuǎn)程服務(wù)器,只放在前端,后臺(tái)通過(guò)一套虛擬語(yǔ)法結(jié)構(gòu)(AST)數(shù)據(jù)結(jié)構(gòu)對(duì)表單個(gè)數(shù),類型,id,規(guī)則進(jìn)行描述,小程序前端動(dòng)態(tài)解析

的時(shí)候應(yīng)該如下圖所示:
1.整體的虛擬語(yǔ)法結(jié)構(gòu)是一個(gè)數(shù)組
2.數(shù)組包含兩層,第一層表示 對(duì)一級(jí)表單的名稱描述,ID
3.第二層表示 對(duì)一級(jí)一級(jí)表單下面具體表單個(gè)數(shù),類型,規(guī)則進(jìn)行描述
image.png
所以我們可以抽象出這樣的結(jié)構(gòu)
const formConfig = [
{
fieldId: '101', //對(duì)應(yīng)一級(jí)表單層級(jí)
fieldName: '基本信息',
formInfo: [ //每個(gè)層級(jí)下面 具體表單元素
{
label: '名字',//標(biāo)題
type: 'text', //表單類型 text,upload,picker,time
id: 'input-name-form', //表單id
placeholder: '輸入您的姓名',//設(shè)置文本框默認(rèn)提示
data: [], //填充表單的數(shù)據(jù) 例如下拉框
role: {
//驗(yàn)證規(guī)則正則表單式
//1.reg正則表達(dá)式
//2.notnull 非空驗(yàn)證
//3.null 不驗(yàn)證
type: 'reg',
value: '',//正則表達(dá)式
},
force: true,//是否必輸入
},
{
label: '身份證',//標(biāo)題
type: 'text', //表單類型 text,upload,picker,time
id: 'input-id-form', //表單id
placeholder: '輸入您的姓名',//設(shè)置文本框默認(rèn)提示
data: [], //填充表單的數(shù)據(jù) 例如下拉框
role: {
//驗(yàn)證規(guī)則正則表單式
//1.reg正則表達(dá)式
//2.notnull 非空驗(yàn)證
//3.null 不驗(yàn)證
type: 'reg',
value: '',//正則表達(dá)式
},
force: true,//是否必輸入
},
{
label: '性別',//標(biāo)題
type: 'picker', //表單類型 text,upload,picker,time
id: 'input-sex-form', //表單id
placeholder: '輸入您的姓名',//設(shè)置文本框默認(rèn)提示
data: [
{ id: 1, name: '男' },
{ id: 2, name: '女' },
], //填充表單的數(shù)據(jù) 例如下拉框
role: {
type: 'reg',
value: '',//正則表達(dá)式
},
force: true,//是否必輸入
}
]
},
{
fieldId: '102',
fieldName: '參賽信息',
formInfo: [
{
label: '參賽編號(hào)',//標(biāo)題
type: 'text', //表單類型 text,upload,picker,time
id: 'match-id-form', //表單id
placeholder: '輸入您的姓名',//設(shè)置文本框默認(rèn)提示
data: [], //填充表單的數(shù)據(jù) 例如下拉框
role: {
type: 'reg',
value: '',//正則表達(dá)式
},
force: false,//是否必輸入
},
{
label: '組別',//標(biāo)題
type: 'picker', //表單類型 text,upload,picker,time
id: 'group-id-from', //表單id
placeholder: '輸入您的姓名',//設(shè)置文本框默認(rèn)提示
data: [
{ id: 1, name: '第一組' },
{ id: 2, name: '第二組' },
{ id: 3, name: '第三組' },
], //填充表單的數(shù)據(jù) 例如下拉框
role: {
type: 'reg',
value: '',//正則表達(dá)式
},
force: false,//是否必輸入
},
{
label: '參賽時(shí)間',//標(biāo)題
type: 'time', //表單類型 text,upload,picker,time
id: 'join-time-form', //表單id
placeholder: '選擇時(shí)間',//設(shè)置picker未選擇默認(rèn)提示
data: [], //填充表單的數(shù)據(jù) 例如下拉框
role: {
type: 'reg',
value: '',//正則表達(dá)式
},
force: false,//是否必輸入
timeType:'date',//當(dāng)類型為time的時(shí)候 指定具體的時(shí)間控件類型 date是年月日選擇器 還是周 time還是天的日期(時(shí)分) timeType
endTime: '',//設(shè)置 表示有效日期范圍的開始,字符串格式為"YYYY-MM-DD"
starTime: ''//設(shè)置 表示有效日期范圍的開始,字符串格式為"YYYY-MM-DD"
},
{
label: '上傳證書',//標(biāo)題
type: 'upload', //表單類型 text,upload,picker,time
id: 'upload-zhn-form', //表單id
placeholder: '輸入您的姓名',//設(shè)置文本框默認(rèn)提示
data: [], //填充表單的數(shù)據(jù) 例如下拉框
role: {
type: 'reg',
value: '',//正則表達(dá)式
},
force: false,//是否必輸入
},
{
label: '所在區(qū)域',//標(biāo)題
type: 'region', //表單類型 text,upload,picker,time
id: 'region-area-form', //表單id
placeholder: '當(dāng)前選擇',//設(shè)置文本框默認(rèn)提示
data: [], //填充表單的數(shù)據(jù) 例如下拉框
role: {
type: 'reg',
value: '',//正則表達(dá)式
},
force: false,//是否必輸入
}
]
},
]
export default formConfig
對(duì)抽象的ast結(jié)構(gòu)中字段的解釋
** fieldId 對(duì)應(yīng)一級(jí)表單層級(jí)**
** fieldName 基本信息 **
** formInfo 每個(gè)層級(jí)下面 具體表單元素**
** label 每個(gè)表單元素標(biāo)題**
** type 表單類型 text(文本類型組件),upload( 上傳組件),picker(選擇組件),time(時(shí)間組件),region(省市縣組件) **
** id 表單id 這里為什么需要id呢?**
** placeholder 設(shè)置文本框默認(rèn)提示 **
** data 填充表單的數(shù)據(jù) 例如下拉框**
** role 驗(yàn)證規(guī)則正則表單式 對(duì)象形式 **
** type 驗(yàn)證類型 1.reg正則表達(dá)式 2.notnull 非空驗(yàn)證 3.null 不驗(yàn)證 **
** value type為正則的時(shí)候 進(jìn)行執(zhí)行 value中攜帶的正則表達(dá)式**
** force 表單是否必輸入 **
3.表單抽離成最小單元組件
3.1所以根據(jù)上面的的抽象以后我們可以大體上將組件按照最小化的方式進(jìn)行抽出來(lái),叫做最小元組件
1.text-input文本框輸入組件
2.text-picker選框組件(性別,分組選框)
3.text-time 時(shí)間組件
4.image-upload 文件上傳組件
5.region-picker 省市縣

3.2 當(dāng)組件抽離出來(lái)以后,組件的默認(rèn)數(shù)據(jù)的獲取可以通過(guò)ast中獲得,我們可以在動(dòng)態(tài)渲染中通過(guò)type進(jìn)行條件控制,但是組件內(nèi)部用戶數(shù)據(jù)的如何如何在用戶點(diǎn)擊按鈕時(shí)候拿到? 每個(gè)具體表單元素如何動(dòng)態(tài)渲染 ?一起看第四,第五點(diǎn)
4.元組件中數(shù)據(jù)如何共享出來(lái)
微信官方可用的技術(shù)方案須知,ps:基于 Component 的 behaviors中的selectComponent,當(dāng)我們希望拿到自定義組件中的一些數(shù)據(jù)時(shí)候,可以基于this.selectComponent('#自定義組件的id'),所以這也是為什么在抽象AST結(jié)構(gòu)的時(shí)候,需要加一個(gè)對(duì)應(yīng)的id,并且每個(gè)form元素的id都不是唯一的
實(shí)現(xiàn)該過(guò)程
//在自定義組件中
Component({
data: {
input_text:'', //第一步:定義接受文本框數(shù)據(jù)
},
methods:{
//第二步:改變data的值 對(duì)應(yīng)文本框上面綁定的事件
enterValue:function(e){
console.log(e.detail.value);
this.setData({
input_text: e.detail.value
});
}
},
//第三步: 通過(guò)聲明組件間共享數(shù)據(jù)的behaviors屬性達(dá)到數(shù)據(jù)共享
behaviors: ['wx://component-export'],
//第四步:將你要暴露的數(shù)據(jù)導(dǎo)出
export() {
return { textvalue: this.data.input_text }
}
});
//獲取這個(gè)組件中值的時(shí)候,在外層組件的事件中
this.selectComponent('#id').textvalue
5.每個(gè)具體組件的封裝和動(dòng)態(tài)渲染
1.組件的封裝 舉例image-upload組件,因?yàn)槭状挝丛x擇圖片上傳,upload的中只有左側(cè)label和右側(cè)按鈕,通過(guò)組件內(nèi)部一個(gè)inputType是否上傳類型來(lái)判斷如果已經(jīng)上傳那就改用文本框顯示上傳成功的圖片絕對(duì)路徑,再次點(diǎn)擊,則重新打開圖片選擇器
<view class='reports-form-box form-inner'>
<view class='reports-form-input input-all-width'>
<view class='form-inputname'>{{forminfo.label}}<text wx:if="{{forminfo.force}}" class="force_text">*</text></view>
<block wx:if="{{inputType==='upload'}}"><button bindtap='uploadImage' class='form-inputtext form-upload'></button></block>
<block wx:if="{{inputType==='uploaded'}}">
<input bindtap='uploadImage' value='{{input_text}}' class='form-inputtext'></input>
</block>
</view>
</view>
import util from '../../utils/util.js';
Component({
/**
* 組件的屬性列表
*/
options: {
//開啟共享css模式
addGlobalClass: true,
},
properties: {
label: {
type: String,
value: '',
},
formid: {
type: String,
value: '',
},
forminfo: {
type: Object,
value: {}
}
},
/**
* 組件的初始數(shù)據(jù)
*/
data: {
wx_back_image:'',
inputType:'upload',
input_text:'', //獲取到上傳成功以后的輸入地址
},
behaviors: ['wx://component-export'],
export() {
return { input_text: this.data.input_text}
},
/**
* 組件的方法列表
*/
methods: {
uploadImage:function(){
wx.chooseImage({
count: 1,
sizeType: ['original','compressed'],
sourceType: ['album', 'camera'],
success: (res)=>{
console.log(res);
const { tempFilePaths} =res;
console.log('wx image path::::',tempFilePaths[0]);
this.setData({
wx_back_image: tempFilePaths[0]
});
//util.wx_upload_image 方法是一個(gè)模擬微信上傳成功的接口 只為測(cè)試
util.wx_upload_image(tempFilePaths[0]).then((resp)=>{
console.log(resp);
this.setData({
inputType: 'uploaded',
input_text: resp,
});
});
},
})
},
}
})
2.組件的動(dòng)態(tài)渲染 那我們可以在遍歷第一層表單之后,在遍歷每一個(gè)第一層表單內(nèi)部具體的form元素的時(shí)候,通過(guò)type來(lái)動(dòng)態(tài)加載我們封裝好的各個(gè)元組件
<view class='reports-container'>
<view class='reports-box'
wx:for="{{formList}}"
wx:for-index="idx"
wx:for-item="fieldItem"
wx:key="{{fieldItem.fieldId}}">
<view class='reports-header'>
<view class='reports-header-line'></view>
<view class='reports-header-name'>{{fieldItem.fieldName}}</view>
</view>
<view class='reports-form'>
<block wx:for="{{fieldItem.formInfo}}"
wx:for-index="idx"
wx:for-item="items"
wx:key="{{items.id}}">
<!--動(dòng)態(tài)渲染元組件的過(guò)程-->
<block wx:if="{{items.type==='text'}}">
<!--每個(gè)元組件都需要傳遞id和當(dāng)前組件全部的信息 便于元組件內(nèi)部解析-->
<text-input id="{{items.id}}" forminfo="{{items}}"></text-input>
</block>
<block wx:if="{{items.type==='picker'}}">
<text-picker id="{{items.id}}" forminfo="{{items}}"></text-picker>
</block>
<block wx:if="{{items.type==='time'}}">
<text-time id="{{items.id}}" forminfo="{{items}}"></text-time>
</block>
<block wx:if="{{items.type==='region'}}">
<region-picker id="{{items.id}}" forminfo="{{items}}"></region-picker>
</block>
<block wx:if="{{items.type==='upload'}}">
<image-upload id="{{items.id}}" forminfo="{{items}}"></image-upload>
</block>
</block>
</view>
</view>
<view class='reports-form form-center'>
<!-- 并提交訂單 -->
<view bindtap='getInputValue' class='reports-form-btn'>提交</view>
</view>
</view>
6.測(cè)試并使用
//導(dǎo)入組件ast數(shù)據(jù)結(jié)構(gòu) 開發(fā)過(guò)程中有后端返回過(guò)來(lái)
import formConfig from '../../utils/formconfig.js';
Page({
/**
* 頁(yè)面的初始數(shù)據(jù)
*/
data: {
formList: formConfig
},
getInputValue:function(){
// const v = this.selectComponent('#my-input');
// console.log(v);
// const ids=this.data.formIds;
// for(var key in ids){
// console.log(ids[key], key, `#${key}`);
// console.log(this.selectComponent(`#${key}`));
// }
let result =[];
formConfig.forEach((item,i)=>{
result.push(item.formInfo)
})
//console.log(result);
var forms=result.reduce((a,b)=>{
return a.concat(b)
})
//console.log(forms);
forms.forEach((items,i)=>{
//console.log(items.id);
const v = this.selectComponent('#' + items.id);
console.log(items.id,':::',v);
});
}
})
測(cè)試結(jié)果


