前面水了一篇《vue动态表单简单设计template版本》,打算再水2篇不同的版本。
日常开发中经常会有重复性很高,或者动态变化很大的表单需求。开发过程中我们有时候又很讨厌这种重复性的工作,秉着能偷懒就偷懒的原则,大家一般都会想到动态渲染的方案。
有哪些方案可以选择
template
正常vue开发中我们常常使用的template模版的形式,这也是vue的一大特色,模版语法是vue内部定制的一套规则,通过对模版的解析生成真实的code。其中还有vnode,Diff等等优化处理。总的看来说vue的模版语法给了我们一个下限,对vue的开发进行了兜底,同时也有了各种限制和学习成本,好在不高。
但是template的写法毕竟存在很大的局限性质,无法给我们提供自由的开发过程。
JSX
JSX的写法非常接近原生的写法,可以用js来编写html结构,提供了高灵活性。JSX的语法使得开发者能够以更接近自然语言的方式描述用户界面,减少了编写和维护代码的复杂性,同时也使得UI的设计和实现更加直观。
在Vue3很好的接入JSX之后,在Vue中也可以很舒服的使用JSX语法,相比较react在写法上基本相同,只是会存在细微的差距,一次上手就爱上了JSX的舒适性。
低代码
既然都有了通用渲染模版,那为啥还需要通过数据去配置页面结构呢,在此基础上加上物料区,和操作平台,不就成了低代码平台了嘛。(第三篇文章水低代码平台实现的过程)
具体实现
动态渲染的核心就是用数据驱动页面,首先就得需要定制数据的协议。 常见的协议比如钉钉的宜搭通过对JSON数据的定义,去实现一套通用的渲染方法。
针对业务需要的,主要的渲染逻辑
function externalSkeleton(item, index) {
if (item instanceof Array) {
return item.map((e, i) => externalSkeleton(e, i));
}
if (item.type == 'title') {
let childrenDom = []
if (item.select && Array.isArray(item.children) && item.children.length > 0) {
childrenDom = externalSkeleton(item.children,index)
}
if(childrenDom.length > 0) {
return (
<ElRow>
<ElCol>
<div class="secondaryTitle-title">
<span class="title"> {item.label} </span>
</div>
</ElCol>
{childrenDom}
</ElRow>
);
}
return null // 没有标题不展示
} else {
let childrenDom = []
if (item.select) {
if(item.children && item.children.length > 0) {
childrenDom = externalSkeleton(item.children,index)
return [generateDom(item, index),...childrenDom]
}
return generateDom(item, index)
}
return null
}
}
const generateDom = (item, index) => {
return (
<ElCol span={item.span || 12} key={item.id || UUID}>
<ElFormItem
prop={ElFormItem.${item.id}.itemValue}
label='item.label'
rules={[{ required: true, message: '请输入' + item.label, trigger: 'blur' }]}
>
{item.tips && (
<>
<span style={ item.style || { minWidth: '50px' }} className='required'>{item.unit? `${item.label} (${unit[item.unit]})` : item.label}</span>
<ElTooltip
class="box-item"
effect="dark"
content={item.tips}
placement="top">
<span style={ item.style || { marginLeft: '10px' }} class="question-icon">?</span>
</ElTooltip>
</>
)}
{!item.tips && (<span style={{ minWidth: '50px' }} className='required'>{item.unit? `${item.label} (${unit[item.unit]})` : item.label}</span>)}
<div style={{ width: '360px' }} class="upInput">
{typeMapping(item, index)}
</div>
</ElFormItem>
</ElCol>
)
}
对于页面的渲染首先需要考虑的是深度优先遍历,得从最深处的子级开始渲染,子集的渲染结果来判断上层是否需要进行相应的处理。
对于渲染数据和提交数据是两套树形结构,内部根据协议定制相关的属性,比如说简单的。
id: val.id,
itemText : val.attrName,
itemValue: itemValueVal || a.itemValue,
translate: translate || a.itemDescript,
files: a.files || a.annexList,
itemDescript: a.itemDescript,
optionType: val.optionType,
isFile: val.isFile,
parentName: val.parentName,
projectTypeName: val.projectTypeName,
unit: val.unit
最好是根据成熟的低代码方案定制,虽然只是动态渲染,但是渲染器原理差不多,只是中间的环境多少和复杂程度
对于事件的绑定,可以在配置渲染JSON的时候使用字符串的形式传入,使用eval()方法来运行,需要考虑的是安全性的问题。
typeMapping中根据JSON提供的数据来动态的选择当前需要渲染的组件,比如
const typeMapping = (item, index) => {
let returnElement;
switch (item.type) {
case 'input':
if (inputValue.value[item.id]) {
returnElement = item.isFile == 1 ? (
<AssociatedUploadInput
singleInput={true}
vModel={inputValue.value[item.id].itemValue}
onInputChange={associatedInputChange}
isNumber={item.unit}
onFileList={(e)=>{fileListChang(e,item.id)}}
fileList={inputValue.value[item.id].files}
/>
) : inputDom(item,index);
}
break;
case 'radio':
if (inputValue.value[item.id]) {
returnElement = item.isFile == 1 ? (
<AssociatedUploadInput
inputType='radio'
singleInput={true}
vModel={inputValue.value[item.id].itemValue}
onChange={radioGroupChange}
onInputChange={associatedInputChange}
onFileList={(e) => { fileListChang(e, item.id) }}
fileList={inputValue.value[item.id].files}
onRadioValueTranslate={(e) => { radioValueTranslate(e, item.id) }}
/>
) : renderRadioGroup(item, index);
}
break;
// 其他类型的处理
default:
if (inputValue.value[item.id]) {
returnElement = item.isFile == 1 ? (
<AssociatedUploadInput
singleInput={true}
vModel={inputValue.value[item.id].itemValue}
onInputChange={associatedInputChange}
onFileList={(e) => { fileListChang(e, item.id) }}
fileList={inputValue.value[item.id].files}
/>
) : inputDom(item, index);
}
break;
}
return returnElement;