為什么要做頁(yè)面可視化搭建系統(tǒng)
統(tǒng)一微前端架構(gòu)各個(gè)微應(yīng)用頁(yè)面的樣式和交互 我們公司的供應(yīng)鏈 saas 系統(tǒng)而多個(gè)獨(dú)立部署、技術(shù)棧不統(tǒng)一的系統(tǒng)組合而成晰甚,這些系統(tǒng)的樣式,交互存在差異椰于,通過頁(yè)面可視化搭建系統(tǒng)生成的頁(yè)面底層使用同一套組件庫(kù)梭姓,這可以滿足樣式遵班,交互一致屠升,并且面對(duì)之后的樣式和交互變更支持批量修改
縮短常規(guī)頁(yè)面開發(fā)時(shí)間 我們公司的供應(yīng)鏈 saas 系統(tǒng)是一個(gè) toB 系統(tǒng),這里面存在數(shù)量可觀的類似的頁(yè)面狭郑,開發(fā)重復(fù)頁(yè)面容易磨滅開發(fā)人員的積極性腹暖,整理各類頁(yè)面的共同之處,通過可視化搭建系統(tǒng)來減少頁(yè)面開發(fā)重復(fù)度翰萨,讓開發(fā)人員集中精力開發(fā)邏輯復(fù)雜的頁(yè)面
整個(gè)可視化搭建系統(tǒng)分為三部分脏答,分別是配置頁(yè)(setting),視圖頁(yè)(view) 和 json schema。配置頁(yè)生成 json schema,視圖頁(yè)消費(fèi) json schema
寫在前面
使用 codemirror 實(shí)現(xiàn)在可視化界面上編輯自定義行為的代碼
接口地址只填寫以/開頭的相對(duì)路徑殖告,視圖頁(yè)在運(yùn)行的時(shí)候決定接口所在的環(huán)境
使用 cool-path 實(shí)現(xiàn)按字段路徑取值阿蝶、按字段路徑修改值
使用 new Function 在視圖頁(yè)將 json schema 對(duì)應(yīng)的字符串轉(zhuǎn)化成對(duì)象或者函數(shù)
可創(chuàng)建的頁(yè)面類型有:列表、詳情黄绩、表單羡洁。詳情和表單頁(yè)的設(shè)計(jì)思路差別不大,列表頁(yè)與另外兩種頁(yè)面差別比較大
功能
列表頁(yè)
定義按鈕操作爽丹、定義搜索項(xiàng)(單行搜索框\事件選擇器\下拉框\級(jí)聯(lián)選擇器\批量輸入搜索)筑煮、動(dòng)態(tài)獲取下拉框和級(jí)聯(lián)選擇器的備選數(shù)據(jù)、列表排序粤蝎、table 行多選真仲、自定義 table 行的操作、自定義 table 列的顯示內(nèi)容
詳情頁(yè)\表單頁(yè)
表單聯(lián)動(dòng)初澎、表格數(shù)據(jù)格式校驗(yàn)秸应、一列布局、多列布局碑宴、表格分頁(yè)灸眼、自定義文本的顯示內(nèi)容
列表頁(yè)設(shè)計(jì)
經(jīng)過分析我們公司的列表頁(yè)布局有一個(gè)統(tǒng)一的模式。列表由右上角的操作按鈕墓懂、左上角的標(biāo)題\面包屑焰宣、正上面的篩選區(qū)域、中間的 table 以及正下方的分頁(yè)器組成捕仔,中間的 table 是必須存在的匕积,其他內(nèi)容可選。如下圖所示:
由于列表頁(yè)有一個(gè)統(tǒng)一的布局模式榜跌,在列表的配置頁(yè)闪唆,我將列表頁(yè)分成多個(gè)獨(dú)立的區(qū)域進(jìn)行分別配置,如下圖:
基本配置區(qū)域中填寫的數(shù)據(jù)不會(huì)顯示在列表視圖頁(yè)中钓葫,這個(gè)區(qū)域填寫的數(shù)據(jù)只是為了方便列表配置數(shù)據(jù)的查找
全局配置
由于列表頁(yè)是一個(gè)動(dòng)態(tài)的頁(yè)面悄蕾,頁(yè)面中大部分?jǐn)?shù)據(jù)都是從后端開發(fā)人員提供的接口中得到的,每一個(gè)接口都對(duì)應(yīng)了多個(gè)環(huán)境础浮,在我們公司每個(gè)接口至少有開發(fā)環(huán)境帆调、測(cè)試環(huán)境、生成環(huán)境這三個(gè)環(huán)境豆同,所以在列表配置頁(yè)中不能將接口的域名寫死番刊,在需要填寫接口地址的地方只能填寫接口的相對(duì)路徑,除此之外這個(gè)頁(yè)面可視化搭建系統(tǒng)需要為多個(gè)獨(dú)立部署的系統(tǒng)生成頁(yè)面影锈,所以在全局配置區(qū)域要選擇后端接口的所屬系統(tǒng)芹务,如下圖:
列表視圖頁(yè)中從 json schema 中得到接口所屬系統(tǒng)標(biāo)識(shí)符蝉绷,再根據(jù)視圖頁(yè)的運(yùn)行環(huán)境動(dòng)態(tài)生成接口的域名
并不是所有的列表頁(yè)都存在按鈕、filterStatus 和搜索框枣抱,在這三個(gè)區(qū)域可以根據(jù)實(shí)際情況進(jìn)行配置
按鈕配置
在配置按鈕的時(shí)候必須選擇按鈕的操作類型熔吗,目前可選的操作類型有:上傳、導(dǎo)出佳晶、自定義磁滚,不同操作類型的按鈕需要填寫的配置項(xiàng)有所不同。在這里以導(dǎo)出為例宵晚,不同的列表頁(yè)導(dǎo)出之后需要進(jìn)行的后續(xù)操作有所差異垂攘,所以配置人員可以自定義導(dǎo)出之后的回調(diào)函數(shù),為了減少配置人員對(duì)參數(shù)順序的記憶成本淤刃,在 codemirror 代碼編輯器中只能寫函數(shù)體中的內(nèi)容晒他,配置頁(yè)將 json schema 保存到服務(wù)器之前會(huì)將代碼編輯器中的內(nèi)容包裹在函數(shù)中,簡(jiǎn)化代碼如下:
if(button.type==='upload') {
button.callback ='function (vm,content) {'+ toSwitch(button.callback) +'}'
}else{
button.callback ='function (vm) {'+ toSwitch(button.callback) +'}'
}
當(dāng)再此編輯函數(shù)體的內(nèi)容時(shí)逸贾,需要將函數(shù)中的函數(shù)體取出陨仅,簡(jiǎn)化代碼如下:
consttoSwitch = (func) =>{
constmatchResult =func.toLocaleString().match(/(?:\/\*[\s\S]*?\*\/|\/\/.*?\r?\n|[^{])+\{([\s\S]*)\}$/)
constbody = (matchResult||[])[1] ||''
returnbody.trim();
}button.callback = toSwitch(button.callback)
由于不同的接口需要傳遞的參數(shù)形式有所不同,所以在所有填寫接口地址的地方铝侵,都可以自定義組裝接口的參數(shù)灼伤,視圖頁(yè)在渲染頁(yè)面時(shí)有生成接口參數(shù)的行為,在自定義組裝接口參數(shù)編輯器中可以修改這一默認(rèn)行為
filterStatus 配置較為簡(jiǎn)單咪鲜,在這兒略過
搜索區(qū)域配置
searchBox 區(qū)域可配置的搜索框有:?jiǎn)涡休斎肟蚝摹⑾吕颉⒓?jí)聯(lián)選擇器疟丙、時(shí)間選擇器颖侄、時(shí)間范圍選擇器
不同的搜索框需要填寫的配置項(xiàng)不同。對(duì)于時(shí)間范圍選擇器而言享郊,有的列表接口要求將開始時(shí)間和結(jié)束時(shí)間放在同一個(gè)數(shù)組中览祖,有的列表接口則要求將開始時(shí)間和結(jié)束時(shí)間分別放在不同的字段中,所以搜索框的字段名具有解構(gòu)的功能炊琉。在填寫字段名時(shí)可以填寫 [param1,param2] 這種格式展蒂。在視圖頁(yè)解析 json schema 時(shí)會(huì)將搜索框的參數(shù)賦給解構(gòu)之后的參數(shù),簡(jiǎn)化代碼如下:
functionseparateParam(originalArr,key){
constkeyArr = key.replace(/^\[/,'').replace(/\]$/,'').split(',');
constresult = {};
keyArr.forEach((key,index) =>{
result[key] = originalArr[index] });returnresult;
}
在某些列表中可能需要給搜索框設(shè)置默認(rèn)值苔咪,默認(rèn)值可能是固定的靜態(tài)數(shù)據(jù)也可能是視圖頁(yè)運(yùn)行時(shí)動(dòng)態(tài)生成的數(shù)據(jù)锰悼。如果默認(rèn)值輸入框中包含 return,則會(huì)認(rèn)為默認(rèn)值是從函數(shù)中動(dòng)態(tài)生成悼泌,配置頁(yè)在將 json schema 保存到服務(wù)器之前會(huì)將代碼編輯器中輸入的內(nèi)容包裹到函數(shù)中
視圖頁(yè)給搜索框賦默認(rèn)值的代碼如下:
functiongetDefaultValue(searchConfig){
returnisFunction(searchConfig.default) ? searchConfig.default(vm) : searchConfig.default;
}
下拉框和級(jí)聯(lián)選擇器需要有下拉備選項(xiàng)松捉,這些下拉備選項(xiàng)可以從接口中獲取也可以配置靜態(tài)的數(shù)據(jù)
table 區(qū)域
table 配置是列表頁(yè)配置中最為復(fù)雜的地方,table 也是列表視圖中主要的內(nèi)容馆里,它的復(fù)雜之處在于隘世,列數(shù)不固定,每列的顯示形式不固定鸠踪,配置區(qū)域如下:
由于 table 每一列要展示的數(shù)據(jù)的嵌套層級(jí)不固定丙者,所以表頭字段支持按路徑取值。例如:表頭字段可以是order.id营密,這使用cool-path來實(shí)現(xiàn)這個(gè)功能
table 支持的列的展示形式有:多選械媒、操作、文本评汰。如果某一列是操作列纷捞,就必須自定義操作列的展示形式。如果某一列是文本被去,默認(rèn)情況會(huì)根據(jù)表頭字段去取值主儡,然后將文本內(nèi)容顯示在界面上,考慮到實(shí)際的需求惨缆,配置人員也可以改變這一默認(rèn)行為糜值,去自定義顯示內(nèi)容。自定義顯示內(nèi)容使用的 Vue 的渲染函數(shù)來實(shí)現(xiàn),簡(jiǎn)化代碼如下:
:row="scope.row"
:index="scope.$index"
:col="col"
/>
</template>
// v-render 組件定義如下
components:{ vRender:{ render(createElement) {//這兒的this.renderFunc 是在列表配置界面寫的函數(shù)
returnthis.renderFunc(createElement,this.row,vm.$parent,this.col,this.index,this.oldRowData)
}, props:{ renderFunc:{ type:Function, required:true
}, row:{ type:Object,default(){return{}}
}, index:{ type:Number, default:0
}, col:{ type:Object,default() {return{} }
} }, data(){return{
oldRowData:deepClone(this.row)
} } } }
由于 table 中要展示的數(shù)據(jù)都是從后端提供的接口獲取坯墨,在我們公司內(nèi)部這個(gè)頁(yè)面搭建系統(tǒng)要服務(wù)于多個(gè)獨(dú)立的系統(tǒng)寂汇,這些系統(tǒng)的后端接口規(guī)范不盡相同,所以在配置頁(yè)可以根據(jù)接口返回的值組裝 table 要展示的數(shù)據(jù)捣染。組裝 table 數(shù)據(jù)與組裝接口參數(shù)類似骄瓣,都是在代碼編輯框中寫函數(shù),然后函數(shù)必須有一個(gè)返回值耍攘,視圖頁(yè)會(huì)將返回值當(dāng)作接口參數(shù)或者 table 數(shù)據(jù)
詳情頁(yè)/表單頁(yè)的設(shè)計(jì)
詳情頁(yè)和表單頁(yè)的設(shè)計(jì)思路相同累贤,不同的是在頁(yè)面上展示的組件不同,在下面的文字中統(tǒng)稱為詳情頁(yè)少漆。詳情頁(yè)中有兩種類型的組件臼膏,分別是布局組件和基礎(chǔ)組件,基礎(chǔ)組件只能放置在布局組件中示损,布局組件不能相互嵌套
在這里我以行為緯度來創(chuàng)建詳情頁(yè)渗磅,并且將行分成一至三列,每一列可以容納任意多個(gè)基礎(chǔ)組件检访,選中基礎(chǔ)組件或者布局組件對(duì)這個(gè)組件進(jìn)行配置始鱼,可以將配置詳情頁(yè)當(dāng)做搭積木
頁(yè)面數(shù)據(jù)的獲取
由于創(chuàng)建的是動(dòng)態(tài)頁(yè)面需要請(qǐng)求后端接口,所以在創(chuàng)建詳情頁(yè)時(shí)需要選擇接口所屬的后端系統(tǒng)并且在需要填寫接口地址的地方只能填寫接口的相對(duì)路徑脆贵,這一點(diǎn)與配置列表頁(yè)相同
對(duì)于所有的詳情頁(yè)而言医清,它們都需要將詳情數(shù)據(jù)展示在界面上,在這里暫且將這些數(shù)據(jù)統(tǒng)稱為詳情頁(yè)面數(shù)據(jù)卖氨。在我們公司的業(yè)務(wù)系統(tǒng)中通常通過詳情 ID 或者其他的參數(shù)從接口中獲取頁(yè)面數(shù)據(jù)
在頁(yè)面可視化搭建系統(tǒng)中有兩種方式獲取頁(yè)面數(shù)據(jù)会烙,分別是:
填寫獲取頁(yè)面數(shù)據(jù)的接口地址负懦,這種方式將大部分的工作都交給視圖頁(yè)自動(dòng)完成,最為簡(jiǎn)單柏腻。
在配置頁(yè)自定義函數(shù)得到頁(yè)面數(shù)據(jù)纸厉,在這里支持 promise 和 同步執(zhí)行的函數(shù),這種方式很靈活
先介紹第一種方式五嫂,界面如下:
在接口地址輸入框中颗品,可以填寫類似這樣的內(nèi)容/basic/someApi/detail?poId=202004130000121&type&code=333,視圖頁(yè)在拿到 json schema 去生成視圖的時(shí)候會(huì)將poId沃缘,type和code 作為接口的參數(shù)躯枢,并且視圖頁(yè)會(huì)優(yōu)先從瀏覽器地址欄中取這些參數(shù)的值,如果瀏覽器不存在某個(gè)參數(shù),程序就使用 json schema 中給定的值槐臀。例如:瀏覽器地址欄的查詢字符串為?po_id=99&type=2,視圖頁(yè)在請(qǐng)求/basic/someApi/detail這個(gè)接口時(shí)锄蹂,傳給接口的參數(shù)為:{po_id:99,type:2,code:333}。這種方式會(huì)將接口返回的content字段當(dāng)前頁(yè)面數(shù)據(jù)
根據(jù)接口地址輸入框中的值與瀏覽器地址欄中的 query 獲取接口參數(shù)的代碼如下:
/**
* 從 query 中得到接口的參數(shù)
*@param params
*@param query
*@returns{{[p: string]: *}}
*/
exportfunctiongetParams(params, query){
constresult = {
...params };Object.keys(result).forEach(key=>{
// 用瀏覽器 query 中的參數(shù)值替換 params 中的值
if(query[key]){
result[key] = query[key]
}
});
return result
}
第二種方式:在配置頁(yè)自定義函數(shù)得到頁(yè)面數(shù)據(jù)峰档,這種方式你只需要寫函數(shù)體败匹,并且必須有一個(gè)返回值,界面如下:
這種方式支持 promise 和同步執(zhí)行的函數(shù)讥巡。如果函數(shù)返回 promise掀亩,視圖頁(yè)會(huì)將 promise resolve 的值當(dāng)作頁(yè)面數(shù)據(jù)受楼,如果是同步執(zhí)行的函數(shù)鼎文,視圖頁(yè)會(huì)將同步函數(shù)的返回值當(dāng)作頁(yè)面數(shù)據(jù)
結(jié)合這兩種方式視圖頁(yè)獲取頁(yè)面數(shù)據(jù)的代碼如下:
/**
* 獲取頁(yè)面數(shù)據(jù)
*@param pageConfig 頁(yè)面配置
*@param vm 詳情頁(yè)的 Vue 實(shí)例
*@returns{Promise<any | never>}
*/
exportfunctionfetchPageData({pageConfig,vm}){
returnnewPromise((resolve, reject) =>{
// 從接口中獲取頁(yè)面數(shù)據(jù)
if(pageConfig.url) {
const paramsFromUrl = getParamsFromUrl(pageConfig.url)
// 得到完整的接口地址
const fullUrl = getFullUrl(pageConfig.belong,paramsFromUrl.origin)
request(fullUrl, getParams(paramsFromUrl.params,vm.$route.query)).then(res => {
resolve(res.content)
})
}
// 通過自定義函數(shù)獲取頁(yè)面數(shù)據(jù)
else if(pageConfig.getPageData ){
if(typeof pageConfig.getPageData === 'function') {
const result = pageConfig.getPageData.call(vm,vm)
resolve(result);
} else {
resolve(pageConfig.getPageData)
}
} else {
resolve({})
}
}).then((content) => {
return content
})
}
組件的配置參數(shù)
如下是一個(gè)輸入框組件的配置:
{
"title":"用戶名",
"path":"user.name",
"key":"userName",
"type":"string",
"visible":true,
"x-linkages":[],
"x-component":"dm-input",
"x-component-props":{
"type":"text",
"size":"small",
"placeholder":"請(qǐng)輸入用戶名"
},"x-props":{
"style":{
"margin":"7px 5px",
"color":"#333333"
} },"editable":true,
"triggerType":"submit",
"events":{},
"x-rules":{
"format":"",
"required":false,
"pattern":"",
"max":"5",
"min":"2"
}}
組件可配置的字段如下:
屬性名 描述 類型
**title **字段標(biāo)題 string
**path **取值路徑 string
key 接口字段名 string
description 字段描述 string
**defaultUI **組件字段默認(rèn)值 any
editable 是否可編輯 boolean
**type **字段值類型 string,object,array,number,boolean
**enum **枚舉數(shù)據(jù) array,object,function
url 獲取枚舉數(shù)據(jù)或者 UI 組件數(shù)據(jù)的接口地址 string
**items **組件的子組件的配置字段 array
**trigger **Type字段校驗(yàn)時(shí)機(jī) string
**visible **字段是否可見 boolean
eventsUI 組件的事件 Object
x-props 字段的擴(kuò)展屬性 object
**x-component **字段的 UI 組件名 string
**x-component-props **字段 UI 組件的屬性 object
**x-linkages **字段聯(lián)動(dòng) array
**x-rules **字段規(guī)則 object
x-props 數(shù)據(jù)屬性
屬性名 描述 類型
**style **字段的 UI 組件的 style 樣式 object
className 字段的 UI 組件的 className string
**label **字段的 UI 組件的枚舉 label 取值路徑 string
value 字段的 UI 組件的枚舉 value 取值路徑 string
**button **Type按鈕的操作類型 string
render 自定義組件的顯示內(nèi)容 function
**buttonSubmitUrl **提交按鈕的接口地址 string
**paging **列表是否分頁(yè) boolean
x-rules 數(shù)據(jù)屬性
屬性名描述類型
**format **字段值類型 string
**required **是否必填 boolean
**pattern **正則 RegExp,string
**max **最大長(zhǎng)度 number
**min **最小長(zhǎng)度 number
**len **長(zhǎng)度 number
**maximum **最大數(shù)值 number
minimum 最小數(shù)值 number
**message **錯(cuò)誤文案 string
format 的可選值:url,郵箱汽纠,手機(jī)號(hào)抬驴,金額炼七,數(shù)字
x-linkages 字段聯(lián)動(dòng)
屬性名描述類型可選值
**type **聯(lián)動(dòng)類型 String linkage:hidden,linkage:disabled,linkage:value
subscribe 聯(lián)動(dòng)訂閱器 Function-
下面以文本組件,下拉框組件布持,按鈕組件為例進(jìn)行說明
文本組件
文件組件用于在詳情頁(yè)中顯示某個(gè)字段對(duì)應(yīng)的值豌拙,他的配置界面如下:
先介紹非自定義文本組件顯示內(nèi)容的情況,這個(gè)時(shí)候文本組件的取值路徑是必填項(xiàng)的题暖,視圖頁(yè)會(huì)根據(jù)取值路徑從頁(yè)面數(shù)據(jù)中取文本組件的顯示內(nèi)容按傅。取值路徑還支持在路徑后面增加過濾器,這里的過濾器和Vue 中的過濾器功能一致胧卤。取值路徑例如為:
create_at|formatDate('datetime'): 從頁(yè)面數(shù)據(jù)的create_at字段中取值唯绍,然后使用formatDate格式化create_at字段對(duì)應(yīng)值
簡(jiǎn)化代碼如下:
... vue 組件
computed:{
//使用計(jì)算屬性得到文本組件要顯示的內(nèi)容
textContent(){ const p =this.fieldSchema.path.split('|')
//如果填寫了取值路徑
if(formatPathStr(p[0])) {
const filters = p.slice(1)
//這里的 Path 指 cool-path
const path =newPath(p[0]);
//從頁(yè)面數(shù)據(jù)中取值
let value = path.getIn(this.pageVm.pageData)
//過濾器
if(filters && filters.length) {
value = filters.reduce((a, b)=>{
returnthis.evalFilter(b, a,this)
}, value) }returnvalue ||'- -'
}else{
returnthis.fieldSchema.default||'- -'
} }},methods:{ evalFilter(filterStr,val){ const parms = filterStr.match(/^([_$0-9A-Za-z]+)\(([^()]+)\)$/) || ['', filterStr]
const fn = parms[1]
let args = [val]try{
args = args.concat(eval(`[${parms[2]}]`))
}catch(e) {
console.error(e)
this.$message.error(this.fieldSchema.title+'執(zhí)行過濾器時(shí)拼接參數(shù)出錯(cuò)了')
}//根據(jù)過濾器名得到過濾器對(duì)應(yīng)的方法
const filterFn =this.$options.filters &&this.$options.filters[fn]
if(typeoffilterFn =='function') {
returnfilterFn.apply(this, args)
}returnval
}}
從上面的配置文件組件的可視化界面中可以看到,我們還可以配置文本組件的枚舉數(shù)據(jù)枝誊,這個(gè)枚舉數(shù)據(jù)主要是考慮到接口返回的頁(yè)面數(shù)據(jù)中的某些字段是數(shù)字或者英語(yǔ)單詞况芒,但是在界面上我們需要顯示這些字段的中文含義,枚舉數(shù)據(jù)可以是從接口中獲取會(huì)可以在配置頁(yè)中寫死叶撒,枚舉數(shù)據(jù)的獲取方式與上面介紹的頁(yè)面數(shù)據(jù)獲取方式類似绝骚,在這里不再贅述
不自定義文本組件顯示內(nèi)容已經(jīng)可以滿足大部分使用場(chǎng)景耐版,這種方式有一個(gè)局限性:一個(gè)文本組件只能顯示一個(gè)字段的值,在某些時(shí)候可能需要將多個(gè)字段合并在一個(gè)文本組件中顯示在界面皮壁,在這種情況下我使用Vue的渲染函數(shù)來自定義文本組件的顯示內(nèi)容椭更。自定義的渲染函數(shù)類似于下面這樣:
returnh('div',[
h('span',pageData.user.name),
h('span',pageData.uesr.age)
])
在視圖頁(yè)在渲染視圖時(shí)會(huì)執(zhí)行文件組件的渲染函數(shù)哪审,簡(jiǎn)化代碼如下:
<!--do something-->
:renderFunc="fieldSchema['x-props'].render"
/>
// do something
components:{
vRender:{
render(createElement) {
const parentVm = this.$parent;
return this.renderFunc(createElement,parentVm,parentVm.pageVm,parentVm.pageVm.pageData)
},
props:{
renderFunc:{
type:Function,
required: true
},
}
}
}
</script>
在可視化創(chuàng)建詳情頁(yè)中蛾魄,除了文本組件支持寫渲染函數(shù)之外,表格組件中的列也支持寫渲染函數(shù)
下拉框組件
下拉框組件的配置界面如下:
下拉框組件有三個(gè)區(qū)域進(jìn)行配置湿滓,在這里著重介紹下拉框的顯示配置和聯(lián)動(dòng)配置滴须,先介紹顯示配置再介紹聯(lián)動(dòng)配置
下拉框是一個(gè)表單組件,它除了可以對(duì)數(shù)據(jù)進(jìn)行展示還可以對(duì)數(shù)據(jù)進(jìn)行修改叽奥,我將表單組件的值(即:組件 value 屬性對(duì)應(yīng)的值)存放在 vuex 中扔水。對(duì)于詳情頁(yè)而言,表單組件需要顯示它的初始值朝氓,表單的初始值位于頁(yè)面數(shù)據(jù)中魔市,為了讓表單組件在 vuex 中取到它要展示的值,在表單組件 created 鉤子函數(shù)中赵哲,我將這個(gè)表單組件在頁(yè)面數(shù)據(jù)中的值另存到 vuex 中待德,在此之后表單組件取值和修改值都是針對(duì) vuex 中的數(shù)據(jù)進(jìn)行操作,簡(jiǎn)化之后的代碼如下:
<template>
<dm-select
v-model="value"
v-bind="fieldSchema['x-component-props']"
:class="fieldSchema['x-props'].className"
>
<!--....some options-->
exportdefault{
computed:{
value:{
get() {
// 從 Vuex 的 formData 中取值
return new Path(this.fieldSchema.key).getIn(this.formData)
},
set(value){
// 將表單字段的保存到 Vuex 的 formData 中
this.saveFormData({name:this.fieldSchema.key,value:value})
}
},
},
created(){
this.setFieldInitValue()
},
methods:{
setFieldInitValue(){
// 從頁(yè)面數(shù)據(jù)中取表單組件的初始值
let initValue = new Path(this.fieldSchema.path).getIn(this.pageData)
// 將表單組件的初始值保存到 Vuex 的 formData 中
this.saveFormData({name:this.fieldSchema.key,value:initValue})
}
}
}
</script>
下拉組件除了要顯示選中的值枫夺,還需要備選數(shù)據(jù)将宪,它的備選數(shù)據(jù)可以通過從接口中獲取也可以在配置中寫死,支持返回一個(gè) promise橡庞,返回同步計(jì)算的值或者填寫 url较坛。下拉框的備選數(shù)據(jù)獲取方式與上面介紹的頁(yè)面數(shù)據(jù)的獲取方式類似,不再贅述
表單聯(lián)動(dòng)配置
表單聯(lián)動(dòng)是指:這個(gè)表單組件的狀態(tài)受其他表單組件的值的影響扒最,目前支持的聯(lián)動(dòng)類型有:隱藏丑勤、禁用、組件值聯(lián)動(dòng)吧趣。聯(lián)動(dòng)訂閱器用于觀察 formData 中值的變化法竞,針對(duì)表單組件的聯(lián)動(dòng)類型對(duì)組件的狀態(tài)作出影響。聯(lián)動(dòng)訂閱器是一個(gè)函數(shù)再菊,在視圖頁(yè)中使用聯(lián)動(dòng)訂閱器計(jì)算計(jì)算屬性的值爪喘,所以只要在聯(lián)動(dòng)訂閱器中訪問的值發(fā)生了變化,就會(huì)重新計(jì)算計(jì)算屬性纠拔,進(jìn)而影響組件的狀態(tài)秉剑。簡(jiǎn)化的代碼如下:
<template>
<dm-select
v-model="value"
:disabled="disabled"
:hidden="hidden"
>
<!--....some options-->
export default { computed:{ disabled(){ if(this.linkages['linkage:disabled']) {
return this.linkages['linkage:disabled'](this.pageVm,this.pageVm.pageData,this.formData)
} else { return false } }, hidden(){ if(this.linkages['linkage:hidden']) {
return this.linkages['linkage:hidden'](this.pageVm,this.pageVm.pageData,this.formData)
} else { return false } }, value:{ get() { // 從 Vuex 的 formData 中取值 return new Path(this.fieldSchema.key).getIn(this.formData) }, set(value){ // 將表單組件的值保存到 Vuex 的 formData 中 this.saveFormData({name:this.fieldSchema.key,value:value}) } }, valueOfLinkage(){ if(this.linkages['linkage:value']) {
return this.linkages['linkage:value'](this.pageVm,this.pageVm.pageData,this.formData)
} else { return '' } } }, watch:{ valueOfLinkage(val){ this.value = val } }}</script>
表單組件的值聯(lián)動(dòng)比隱藏聯(lián)動(dòng)和禁用聯(lián)動(dòng)要復(fù)雜一些,這是因?yàn)槁?lián)動(dòng)訂閱器可以改變表單組件的值稠诲,表單組件它自身也可以改變它的值侦鹏。表單組件的值由最后一次變化為準(zhǔn)
對(duì)于禁用聯(lián)動(dòng)诡曙,它的聯(lián)動(dòng)訂閱器中可填寫的內(nèi)容如下:
if(formData.status +''==='2') {
returntrue
}else{
returnfalse
}
上面的聯(lián)動(dòng)訂閱器表示:當(dāng) vuex 中的 formData.status 等于 2 時(shí),這個(gè)表單組件會(huì)被禁用
對(duì)于隱藏聯(lián)動(dòng)略水,它的聯(lián)動(dòng)訂閱器中可填寫的內(nèi)容如下:
if(formData.username.length >3) {
returntrue
}else{
returnfalse
}
上面的聯(lián)動(dòng)訂閱器表示:當(dāng) vuex 中的 formData.username 的長(zhǎng)度 > 3 時(shí)价卤,這個(gè)表單組件會(huì)被隱藏
對(duì)于值聯(lián)動(dòng),它的聯(lián)動(dòng)訂閱器中可以填寫的內(nèi)容如下:
if(formData.id) {
return3
}else{
return''
}
上面的聯(lián)動(dòng)訂閱器表示:當(dāng) vuex 中的 formData.id 為 truly 時(shí)渊涝,這個(gè)表單組件的值會(huì)被置為 3
按鈕組件
按鈕組件的配置界面如下:
按鈕組件是一種比較特別的組件慎璧,與其他組件相比它的操作行為不固定而且影響范圍比較廣。根據(jù)業(yè)務(wù)需求分為三種操作類型跨释,分別是:提交(即:將表單數(shù)據(jù)提交到服務(wù)器)胸私,重置(即:將表單組件的值重置為初始狀態(tài)),自定義(即:自定義按鈕的點(diǎn)擊事件處理程序)鳖谈。在下面只介紹提交和自定義這兩種類型
提交操作
通常在將表單數(shù)據(jù)提交到服務(wù)器之前岁疼,我們需要對(duì)表單數(shù)據(jù)進(jìn)行校驗(yàn),只有所有的數(shù)據(jù)符合要求才能將表單數(shù)據(jù)提交到服務(wù)器缆娃,否則將錯(cuò)誤語(yǔ)顯示到界面上捷绒。為了滿足這個(gè)需求,我們需要在按鈕提交事件的處理程序中訪問到所有的表單數(shù)據(jù)以及表單組件的數(shù)據(jù)校驗(yàn)規(guī)則贯要,由于表單數(shù)據(jù)保存在 Vuex 中暖侨,并且存放數(shù)據(jù)校驗(yàn)規(guī)則的 json schema 在視圖頁(yè)中全局共享,所以在提交事件處理程序中能夠很容易拿到想要的數(shù)據(jù)郭毕。需要注意的是它碎,如果某個(gè)表單組件的數(shù)據(jù)沒有通過校驗(yàn),它錯(cuò)誤信息要顯示在表單組件所在的位置显押,這就意味著消費(fèi)錯(cuò)誤信息的位置和生成錯(cuò)誤信息的位置不相同
我將對(duì)錯(cuò)誤信息進(jìn)行操作的方法收集到單獨(dú)的模塊中扳肛。簡(jiǎn)化代碼如下:
/**
*表單錯(cuò)誤收集器
**/
importVue from'vue'
exportconsterrorCollector = new Vue({
data(){
return{
errorObj:{} } }, methods:{ clearError(){this.errorObj = {}
}, delError(name){consterrorObj = {
...this.errorObj
} delete errorObj[name]this.errorObj = errorObj
}, setError(name,value){this.errorObj = {
...this.errorObj,
[name]: value } }, initFieldError(name){this.errorObj = {
...this.errorObj,
[name]:''
} } }})
錯(cuò)誤信息收集器是一個(gè) Vue 實(shí)例,在每個(gè)表單組件中引入錯(cuò)誤信息收集器乘碑,并且將它作為組件的一個(gè) data 屬性挖息,錯(cuò)誤信息作為組件的計(jì)算屬性,這樣一來只要錯(cuò)誤信息收集器中的數(shù)據(jù)發(fā)生變化界面就會(huì)更新兽肤,簡(jiǎn)化代碼如下:
<template>
<!-- do something-->
{{ errorMsg }}
exportdefault{
data(){
return{
errorCollector:errorCollector
}
},
computed:{
errorMsg(){
returnthis.errorCollector.errorObj[this.fieldSchema.key]
}
}
}
自定義操作
自定義操作實(shí)際上 json schema 中定義按鈕的點(diǎn)擊事件處理程序套腹,在視圖頁(yè)中的實(shí)現(xiàn)比較簡(jiǎn)單
如何使用
在開發(fā)環(huán)境 json schema 保存在數(shù)據(jù)庫(kù),要在測(cè)試環(huán)境和生產(chǎn)環(huán)境使用 json schema 生成頁(yè)面资铡,需要將 json schema 下載到項(xiàng)目中的一個(gè)特定文件夾中电禀,當(dāng)在瀏覽器中訪問這個(gè)視圖頁(yè)時(shí),會(huì)根據(jù)頁(yè)面 ID 到下載好的靜態(tài)文件中讀取頁(yè)面的 json schema笤休,然后視圖頁(yè)將頁(yè)面渲染出來
從靜態(tài)文件中讀取配置代碼如下:
import("@static/jsons/tables/table_string_"+id+".json").then(fileContent=>{
console.log('配置數(shù)據(jù):',fileContent)
})
json 文件中保存的 json schema 是一個(gè)字符串尖飞,但是在視圖頁(yè)渲染界面的時(shí)候需要的是一個(gè)對(duì)象,并且對(duì)象的某些字段必須是函數(shù)。為了將字符串轉(zhuǎn)成需要的格式政基,我使用 new Function('return ' + strConfig)() 來完成這一需求,簡(jiǎn)化代碼如下:
functionparseStrConfig(jsonSchema){
returnnewFunction('return '+ jsonSchema)();
}
存在的不足
1.生產(chǎn)出的頁(yè)面不能獨(dú)立于頁(yè)面搭建系統(tǒng)運(yùn)行贞铣。要想在其他系統(tǒng)中使用生成的頁(yè)面,必須在對(duì)應(yīng)系統(tǒng)中使用 iframe 或者 single-spa 微前端技術(shù)引入頁(yè)面搭建系統(tǒng)
2.頁(yè)面的 json schema 沒有與頁(yè)面搭建系統(tǒng)獨(dú)立沮明。由于每創(chuàng)建一個(gè)頁(yè)面就要該頁(yè)面的 json schema 下載到頁(yè)面可視化搭建系統(tǒng)中辕坝,這導(dǎo)致頁(yè)面可視化搭建系統(tǒng)需要被頻繁的發(fā)布,但是頁(yè)面可視化搭建系統(tǒng)的業(yè)務(wù)功能相對(duì)穩(wěn)定