在设计低代码表单生成器之前,需要了解组件库相关内容的基础内容
ElementUI中Layout布局与Form表单详解
核心流程
表单生成器从 JSON 配置到动态渲染表单的核心流程
如下:
解析 JSON 配置
:构建表单的结构和规则组件映射与渲染
:将配置转化为可视化表单:表单根组件
和表单项(递归,考虑布局列、横)
数据绑定与响应式
:实现数据的双向流动校验机制
:确保数据的合法性表单操作与事件处理
:实现交互逻辑
流程总览分析
主要执行流程说明:
初始化阶段 :
- 接收表单配置对象formConf
深拷贝配置
,初始化表单数据和验证规则
- 处理每个表单组件的默认值和
特殊配置(如文件上传)
渲染阶段 :
- 通过
render函数
创建el-form根组件 递归渲染表单项
,根据layout类型
选择渲染方式
- 使用
render组件
渲染具体的表单元素
- 通过
render组件
处理 :- 创建Vue渲染所需的数据对象
处理插槽内容
绑定事件处理器
- 构建最终的渲染数据对象
- 渲染具体的表单元素
事件处理
:统一的表单提交和重置处理
各个表单元素的值变更处理
- 特殊组件
(如文件上传)的自定义事件处理
文件结构分析
解析配置
/**
* 表单解析器核心模块
* 负责将JSON配置转换为可渲染的Vue表单组件
* 主要功能:
* - 表单布局生成
* - 数据初始化
* - 验证规则构建
* - 表单事件处理
*/
export
default {
// 组件配置...
methods: {
/**
* 初始化表单数据
* @param {Array} componentList - 表单组件配置列表
* @param {Object} formData - 表单数据对象
*/
initFormData(componentList, formData
) {
// 实现细节...
}
,
/**
* 构建验证规则
* @param {Array} componentList - 表单组件列表
* @param {Object} rules - 验证规则容器
*/
buildRules(componentList, rules
) {
// 实现细节...
}
}
}
配置模板
类图说明:
FormConfig :表单全局配置
- 定义表单的基本属性如大小、标签位置等
- 包含一个fields数组存储所有表单组件
ComponentConfig :组件配置
- 包含组件的核心配置( config )和插槽配置( slot )
- 定义组件的模型绑定( vModel )和其他属性
ConfigObject :组件核心配置
- 定义组件的标签、图标、默认值等基本属性
- 包含布局相关的配置如span和layout
- 包含验证规则配置
SlotObject :插槽配置
- 定义组件的插槽内容
- 包含选项数组(用于select、radio等)
- 包含输入框的前后置内容
ValidationRule :验证规则
- 定义必填、正则等验证规则
- 包含验证消息和触发方式
验证规则
- 定义不同表单组件的验证规则触发方式(blur/change)
- 为表单验证系统提供统一的触发机制配置
- 确保表单组件的验证行为一致性
生成代码
js
.js
根据表单配置(conf)动态生成 Vue 组件中的 JavaScript 脚本代码,包括 data、rules、methods 等
- 深拷贝配置对象 conf
- 初始化多个代码片段列表:
dataList、ruleList、optionsList、propsList、methodList、uploadVarList
- 遍历所有字段,调用 buildAttributes 构造各部分代码
- 汇总成完整脚本并返回
1. 核心执行流程:
- 入口函数 makeUpJs 接收表单配置和类型参数
- 递归处理每个表单字段,构建各种必要的代码片段
- 最终组装生成完整的Vue组件代码
2. 主要函数功能:
- buildAttributes : 递归处理字段属性
- buildData : 处理数据属性和默认值
- buildRules : 构建表单验证规则
- buildOptions : 处理选择器选项配置
- buildProps : 处理组件props
- buildBeforeUpload : 处理文件上传验证
- buildexport : 组装最终组件代码
特点
- 模块化设计,每个函数职责明确
- 支持递归处理复杂表单结构
- 完善的文件上传处理机制
- 灵活的验证规则配置
- 支持动态选项加载
/**
* 表单生成器的JavaScript代码生成模块
* 负责将JSON配置转换为Vue组件代码
*/
import {
isArray
}
from 'util'
import {
exportDefault, titleCase
}
from '@/utils/index'
import trigger from './ruleTrigger' // 导入验证规则触发器配置
// 文件大小单位换算常量
const units = {
KB: '1024'
,
MB: '1024 / 1024'
,
GB: '1024 / 1024 / 1024'
}
// 全局配置对象
let confGlobal
// 继承属性配置
const inheritAttrs = {
file: ''
,
dialog: 'inheritAttrs: false,'
}
/**
* 生成Vue组件代码的主入口函数
* @param {Object} conf - 表单配置对象
* @param {String} type - 生成类型(file/dialog)
* @returns {String} 生成的Vue组件代码
*/
export
function makeUpJs(conf, type
) {
confGlobal = conf = JSON.parse(JSON.stringify(conf)
)
const dataList = []
const ruleList = []
const optionsList = []
const propsList = []
const methodList = mixinMethod(type)
const uploadVarList = []
conf.fields.forEach(el =>
{
buildAttributes(el, dataList, ruleList, optionsList, methodList, propsList, uploadVarList)
}
)
const script = buildexport(
conf,
type,
dataList.join('\n'
)
,
ruleList.join('\n'
)
,
optionsList.join('\n'
)
,
uploadVarList.join('\n'
)
,
propsList.join('\n'
)
,
methodList.join('\n'
)
)
confGlobal =
null
return script
}
/**
* 构建组件属性
* 递归处理表单字段,生成数据、规则、选项等配置
* @param {Object} el - 字段配置对象
* @param {Array} dataList - 数据属性列表
* @param {Array} ruleList - 验证规则列表
* @param {Array} optionsList - 选项配置列表
* @param {Array} methodList - 方法列表
* @param {Array} propsList - 属性列表
* @param {Array} uploadVarList - 上传组件变量列表
*/
function buildAttributes(el, dataList, ruleList, optionsList, methodList, propsList, uploadVarList
) {
buildData(el, dataList)
buildRules(el, ruleList)
if (el.__slot__) {
if (el.__slot__.options && el.__slot__.options.length) {
buildOptions(el, optionsList)
}
}
else {
if (el.options && el.options.length) {
buildOptions(el, optionsList)
if (el.__config__.dataType === 'dynamic'
) {
const model = `${el.__vModel__
}Options`
const options = titleCase(model)
buildOptionMethod(`get${options
}`
, model, methodList)
}
}
}
if (el.props && el.props.props) {
buildProps(el, propsList)
}
if (el.action && el.__config__.tag === 'el-upload'
) {
uploadVarList.push(
`${el.__vModel__
}Action: '${el.action
}',
${el.__vModel__
}fileList: [],`
)
methodList.push(buildBeforeUpload(el)
)
if (!el['auto-upload']
) {
methodList.push(buildSubmitUpload(el)
)
}
}
if (el.__config__.children) {
el.__config__.children.forEach(el2 =>
{
buildAttributes(el2, dataList, ruleList, optionsList, methodList, propsList, uploadVarList)
}
)
}
}
/**
* 混入默认方法
* 根据类型添加默认的表单操作方法
* @param {String} type - 组件类型(file/dialog)
* @returns {Array} 方法列表
*/
function mixinMethod(type
) {
const list = []
;
const minxins = {
file: confGlobal.formBtns ? {
submitForm: `submitForm() {
this.$refs['${confGlobal.formRef
}'].validate(valid => {
if(!valid) return
})
},`
,
resetForm: `resetForm() {
this.$refs['${confGlobal.formRef
}'].resetFields()
},`
} :
null
,
dialog: {
onOpen: 'onOpen() {},'
,
onClose: `onClose() {
this.$refs['${confGlobal.formRef
}'].resetFields()
},`
,
close: `close() {
this.$emit('update:visible', false)
},`
,
handleConfirm: `handleConfirm() {
this.$refs['${confGlobal.formRef
}'].validate(valid => {
if(!valid) return
this.close()
})
},`
}
}
const methods = minxins[type]
if (methods) {
Object.keys(methods).forEach(key =>
{
list.push(methods[key]
)
}
)
}
return list
}
/**
* 构建数据属性
* 处理字段的默认值配置
* @param {Object} conf - 字段配置
* @param {Array} dataList - 数据列表
*/
function buildData(conf, dataList
) {
if (conf.__vModel__ ===
undefined
)
return
let defaultValue
if (
typeof (conf.__config__.defaultValue) === 'string' &&
!conf.multiple) {
defaultValue = `'${conf.__config__.defaultValue
}'`
}
else {
defaultValue = `${JSON.stringify(conf.__config__.defaultValue)
}`
}
dataList.push(`${conf.__vModel__
}: ${defaultValue
},`
)
}
/**
* 构建验证规则
* 处理必填和正则验证规则
* @param {Object} conf - 字段配置
* @param {Array} ruleList - 规则列表
*/
function buildRules(conf, ruleList
) {
if (conf.__vModel__ ===
undefined
)
return
const rules = []
if (trigger[conf.__config__.tag]
) {
if (conf.__config__.required) {
const type = isArray(conf.__config__.defaultValue) ? 'type: \'array\',' : ''
let message = isArray(conf.__config__.defaultValue) ? `请至少选择一个${conf.__vModel__
}` : conf.placeholder
if (message ===
undefined
) message = `${conf.__config__.label
}不能为空`
rules.push(`{ required: true, ${type
} message: '${message
}', trigger: '${trigger[conf.__config__.tag]
}' }`
)
}
if (conf.__config__.regList &&
isArray(conf.__config__.regList)
) {
conf.__config__.regList.forEach(item =>
{
if (item.pattern) {
rules.push(`{ pattern: ${eval(item.pattern)
}, message: '${item.message
}', trigger: '${trigger[conf.__config__.tag]
}' }`
)
}
}
)
}
ruleList.push(`${conf.__vModel__
}: [${rules.join(','
)
}],`
)
}
}
/**
* 构建选项配置
* 处理下拉框、级联选择器等的选项数据
* @param {Object} conf - 字段配置
* @param {Array} optionsList - 选项列表
*/
function buildOptions(conf, optionsList
) {
if (conf.__vModel__ ===
undefined
)
return
if (conf.__config__.dataType === 'dynamic'
) {
conf.options = []
}
const options = conf.__config__.tag ==='el-cascader'?conf.options:conf.__slot__.options;
const str = `${conf.__vModel__
}Options: ${JSON.stringify(options)
},`
optionsList.push(str)
}
/**
* 构建组件属性
* 处理组件的props配置
* @param {Object} conf - 字段配置
* @param {Array} propsList - 属性列表
*/
function buildProps(conf, propsList
) {
if (conf.__config__.dataType === 'dynamic'
) {
conf.valueKey !== 'value' &&
(conf.props.props.value = conf.valueKey)
conf.labelKey !== 'label' &&
(conf.props.props.label = conf.labelKey)
conf.childrenKey !== 'children' &&
(conf.props.props.children = conf.childrenKey)
}
const str = `${conf.__vModel__
}Props: ${JSON.stringify(conf.props.props)
},`
propsList.push(str)
}
/**
* 构建上传前验证方法
* 处理文件大小和类型验证
* @param {Object} conf - 上传组件配置
* @returns {String} 验证方法代码
*/
function buildBeforeUpload(conf
) {
const unitNum = units[conf.__config__.sizeUnit]
;
let rightSizeCode = ''
;
let acceptCode = ''
;
const
returnList = []
if (conf.__config__.fileSize) {
rightSizeCode = `let isRightSize = file.size / ${unitNum
} <
${conf.__config__.fileSize
}
if(!isRightSize){
this.$message.error('文件大小超过 ${conf.__config__.fileSize
}${conf.__config__.sizeUnit
}')
}`
returnList.push('isRightSize'
)
}
if (conf.accept) {
acceptCode = `let isAccept = new RegExp('${conf.accept
}').test(file.type)
if(!isAccept){
this.$message.error('应该选择${conf.accept
}类型的文件')
}`
returnList.push('isAccept'
)
}
const str = `${conf.__vModel__
}BeforeUpload(file) {
${rightSizeCode
}
${acceptCode
}
return ${returnList.join('&&'
)
}
},`
return returnList.length ? str : ''
}
/**
* 构建上传提交方法
* @param {Object} conf - 上传组件配置
* @returns {String} 提交方法代码
*/
function buildSubmitUpload(conf
) {
const str = `submitUpload() {
this.$refs['${conf.__vModel__
}'].submit()
},`
return str
}
/**
* 构建选项加载方法
* 用于动态加载选项数据
* @param {String} methodName - 方法名
* @param {String} model - 数据模型名
* @param {Array} methodList - 方法列表
*/
function buildOptionMethod(methodName, model, methodList
) {
const str = `${methodName
}() {
// TODO 发起请求获取数据
this.${model
}
},`
methodList.push(str)
}
/**
* 构建Vue组件导出代码
* 组装最终的组件代码结构
* @param {Object} conf - 配置对象
* @param {String} type - 组件类型
* @param {String} data - 数据定义代码
* @param {String} rules - 验证规则代码
* @param {String} selectOptions - 选项配置代码
* @param {String} uploadVar - 上传变量代码
* @param {String} props - 属性定义代码
* @param {String} methods - 方法定义代码
* @returns {String} 完整的Vue组件代码
*/
function buildexport(conf, type, data, rules, selectOptions, uploadVar, props, methods
) {
const str = `${exportDefault
}{
${inheritAttrs[type]
}
components: {},
props: [],
data () {
return {
${conf.formModel
}: {
${data
}
},
${conf.formRules
}: {
${rules
}
},
${uploadVar
}
${selectOptions
}
${props
}
}
},
computed: {},
watch: {},
created () {},
mounted () {},
methods: {
${methods
}
}
}`
return str
}
css
.js
根据表单字段配置 conf,生成样式(CSS)代码字符串:根据每个字段的 config.tag 类型,从 styles 中匹配对应的样式字符串并加入 CSS 列表,最后拼接成一个完整的样式字符串返回。
const styles = {
'el-rate': '.el-rate{display: inline-block; vertical-align: text-top;}'
,
'el-upload': '.el-upload__tip{line-height: 1.2;}'
}
function addCss(cssList, el
) {
const css = styles[el.__config__.tag]
css && cssList.indexOf(css) === -1 && cssList.push(css)
if (el.__config__.children) {
el.__config__.children.forEach(el2 =>
addCss(cssList, el2)
)
}
}
export
function makeUpCss(conf
) {
const cssList = []
conf.fields.forEach(el =>
addCss(cssList, el)
)
return cssList.join('\n'
)
}
html
.js
根据配置对象 (conf) 自动构建可复用的
Vue 表单模板
此模块的核心作用是:
- 根据配置生成动态的 Vue 表单代码,包括
样式、控件、布局
。 - 通过
组件映射和插槽机制
提升扩展性和可维护性。 - 支持 Element UI 的
各种表单控件与弹窗包装
,适用于低代码平台表单生成器。
主函数
export
function makeUpHtml(conf, type
) {
const htmlList = []
confGlobal = conf
someSpanIsNot24 = conf.fields.some(item => item.span !== 24
)
conf.fields.forEach(el =>
{
htmlList.push(layouts[el.__config__.layout](el)
)
}
)
//表单子元素
const htmlStr = htmlList.join('\n'
)
//表单
let temp = buildFormTemplate(conf, htmlStr, type)
//是否弹出框形式显示
if (type === 'dialog'
) {
temp = dialogWrapper(temp)
}
confGlobal =
null
return temp
}
构建表单子元素
布局
layouts
: 包含两种布局处理器:
- colFormItem : 处理单列表单项
- rowFormItem : 处理行布局,可包含多个子元素
const layouts = {
colFormItem(element
) {
let labelWidth = ''
if (element.__config__.labelWidth && element.__config__.labelWidth !== confGlobal.labelWidth) {
labelWidth = `label-width="${element.__config__.labelWidth
}px"`
}
const required = !trigger[element.__config__.tag] && element.__config__.required ? 'required' : ''
const tagDom = tags[element.__config__.tag] ? tags[element.__config__.tag](element) :
null
let str = `<el-form-item ${labelWidth
} label="${element.__config__.label
}" prop="${element.__vModel__
}" ${required
}>
${tagDom
}
</el-form-item>
`
str = colWrapper(element, str)
return str
}
,
rowFormItem(element
) {
const type = element.type === 'default' ? '' : `type="${element.type
}"`
const justify = element.type === 'default' ? '' : `justify="${element.justify
}"`
const align = element.type === 'default' ? '' : `align="${element.align
}"`
const gutter = element.gutter ? `gutter="${element.gutter
}"` : ''
const children = element.__config__.children.map(el => layouts[el.__config__.layout](el)
)
let str = `<el-row ${type
} ${justify
} ${align
} ${gutter
}>
${children.join('\n'
)
}
</el-row>
`
str = colWrapper(element, str)
return str
}
}
子元素映射
const tags = {
'el-button': el =>
{
const {
tag, disabled
} = attrBuilder(el)
const type = el.type ? `type="${el.type
}"` : ''
const icon = el.icon ? `icon="${el.icon
}"` : ''
const size = el.size ? `size="${el.size
}"` : ''
let child = buildElButtonChild(el)
if (child) child = `\n${child
}\n` // 换行
return `<
${el.__config__.tag
} ${type
} ${icon
} ${size
} ${disabled
}>
${child
}</${el.__config__.tag
}>
`
}
,
'el-input': el =>
{
const {
disabled, vModel, clearable, placeholder, width
} = attrBuilder(el)
const maxlength = el.maxlength ? `:maxlength="${el.maxlength
}"` : ''
const showWordLimit = el['show-word-limit'] ? 'show-word-limit' : ''
const readonly = el.readonly ? 'readonly' : ''
const prefixIcon = el['prefix-icon'] ? `prefix-icon='${el['prefix-icon']
}'` : ''
const suffixIcon = el['suffix-icon'] ? `suffix-icon='${el['suffix-icon']
}'` : ''
const showPassword = el['show-password'] ? 'show-password' : ''
const type = el.type ? `type="${el.type
}"` : ''
const autosize = el.autosize && el.autosize.minRows
? `:autosize="{minRows: ${el.autosize.minRows
}, maxRows: ${el.autosize.maxRows
}}"`
: ''
let child = buildElInputChild(el)
if (child) child = `\n${child
}\n` // 换行
return `<
${el.__config__.tag
} ${vModel
} ${type
} ${placeholder
} ${maxlength
} ${showWordLimit
} ${readonly
} ${disabled
} ${clearable
} ${prefixIcon
} ${suffixIcon
} ${showPassword
} ${autosize
} ${width
}>
${child
}</${el.__config__.tag
}>
`
}
,
'el-input-number': el =>
{
const {
disabled, vModel, placeholder
} = attrBuilder(el)
const controlsPosition = el['controls-position'] ? `controls-position=${el['controls-position']
}` : ''
const min = el.min ? `:min='${el.min
}'` : ''
const max = el.max ? `:max='${el.max
}'` : ''
const step = el.step ? `:step='${el.step
}'` : ''
const stepStrictly = el['step-strictly'] ? 'step-strictly' : ''
const precision = el.precision ? `:precision='${el.precision
}'` : ''
return `<
${el.__config__.tag
} ${vModel
} ${placeholder
} ${step
} ${stepStrictly
} ${precision
} ${controlsPosition
} ${min
} ${max
} ${disabled
}></${el.__config__.tag
}>
`
}
,
'el-select': el =>
{
const {
disabled, vModel, clearable, placeholder, width
} = attrBuilder(el)
const filterable = el.filterable ? 'filterable' : ''
const multiple = el.multiple ? 'multiple' : ''
let child = buildElSelectChild(el)
if (child) child = `\n${child
}\n` // 换行
return `<
${el.__config__.tag
} ${vModel
} ${placeholder
} ${disabled
} ${multiple
} ${filterable
} ${clearable
} ${width
}>
${child
}</${el.__config__.tag
}>
`
}
,
'el-radio-group': el =>
{
const {
disabled, vModel
} = attrBuilder(el)
const size = `size="${el.size
}"`
let child = buildElRadioGroupChild(el)
if (child) child = `\n${child
}\n` // 换行
return `<
${el.__config__.tag
} ${vModel
} ${size
} ${disabled
}>
${child
}</${el.__config__.tag
}>
`
}
,
'el-checkbox-group': el =>
{
const {
disabled, vModel
} = attrBuilder(el)
const size = `size="${el.size
}"`
const min = el.min ? `:min="${el.min
}"` : ''
const max = el.max ? `:max="${el.max
}"` : ''
let child = buildElCheckboxGroupChild(el)
if (child) child = `\n${child
}\n` // 换行
return `<
${el.__config__.tag
} ${vModel
} ${min
} ${max
} ${size
} ${disabled
}>
${child
}</${el.__config__.tag
}>
`
}
,
'el-switch': el =>
{
const {
disabled, vModel
} = attrBuilder(el)
const activeText = el['active-text'] ? `active-text="${el['active-text']
}"` : ''
const inactiveText = el['inactive-text'] ? `inactive-text="${el['inactive-text']
}"` : ''
const activeColor = el['active-color'] ? `active-color="${el['active-color']
}"` : ''
const inactiveColor = el['inactive-color'] ? `inactive-color="${el['inactive-color']
}"` : ''
const activeValue = el['active-value'] !== true ? `:active-value='${JSON.stringify(el['active-value']
)
}'` : ''
const inactiveValue = el['inactive-value'] !== false ? `:inactive-value='${JSON.stringify(el['inactive-value']
)
}'` : ''
return `<
${el.__config__.tag
} ${vModel
} ${activeText
} ${inactiveText
} ${activeColor
} ${inactiveColor
} ${activeValue
} ${inactiveValue
} ${disabled
}></${el.__config__.tag
}>
`
}
,
'el-cascader': el =>
{
const {
disabled, vModel, clearable, placeholder, width
} = attrBuilder(el)
const options = el.options ? `:options="${el.__vModel__
}Options"` : ''
const props = el.props ? `:props="${el.__vModel__
}Props"` : ''
const showAllLevels = el['show-all-levels'] ? '' : ':show-all-levels="false"'
const filterable = el.filterable ? 'filterable' : ''
const separator = el.separator === '/' ? '' : `separator="${el.separator
}"`
return `<
${el.__config__.tag
} ${vModel
} ${options
} ${props
} ${width
} ${showAllLevels
} ${placeholder
} ${separator
} ${filterable
} ${clearable
} ${disabled
}></${el.__config__.tag
}>
`
}
,
'el-slider': el =>
{
const {
disabled, vModel
} = attrBuilder(el)
const min = el.min ? `:min='${el.min
}'` : ''
const max = el.max ? `:max='${el.max
}'` : ''
const step = el.step ? `:step='${el.step
}'` : ''
const range = el.range ? 'range' : ''
const showStops = el['show-stops'] ? `:show-stops="${el['show-stops']
}"` : ''
return `<
${el.__config__.tag
} ${min
} ${max
} ${step
} ${vModel
} ${range
} ${showStops
} ${disabled
}></${el.__config__.tag
}>
`
}
,
'el-time-picker': el =>
{
const {
disabled, vModel, clearable, placeholder, width
} = attrBuilder(el)
const startPlaceholder = el['start-placeholder'] ? `start-placeholder="${el['start-placeholder']
}"` : ''
const endPlaceholder = el['end-placeholder'] ? `end-placeholder="${el['end-placeholder']
}"` : ''
const rangeSeparator = el['range-separator'] ? `range-separator="${el['range-separator']
}"` : ''
const isRange = el['is-range'] ? 'is-range' : ''
const format = el.format ? `format="${el.format
}"` : ''
const valueFormat = el['value-format'] ? `value-format="${el['value-format']
}"` : ''
const pickerOptions = el['picker-options'] ? `:picker-options='${JSON.stringify(el['picker-options']
)
}'` : ''
return `<
${el.__config__.tag
} ${vModel
} ${isRange
} ${format
} ${valueFormat
} ${pickerOptions
} ${width
} ${placeholder
} ${startPlaceholder
} ${endPlaceholder
} ${rangeSeparator
} ${clearable
} ${disabled
}></${el.__config__.tag
}>
`
}
,
'el-date-picker': el =>
{
const {
disabled, vModel, clearable, placeholder, width
} = attrBuilder(el)
const startPlaceholder = el['start-placeholder'] ? `start-placeholder="${el['start-placeholder']
}"` : ''
const endPlaceholder = el['end-placeholder'] ? `end-placeholder="${el['end-placeholder']
}"` : ''
const rangeSeparator = el['range-separator'] ? `range-separator="${el['range-separator']
}"` : ''
const format = el.format ? `format="${el.format
}"` : ''
const valueFormat = el['value-format'] ? `value-format="${el['value-format']
}"` : ''
const type = el.type === 'date' ? '' : `type="${el.type
}"`
const readonly = el.readonly ? 'readonly' : ''
return `<
${el.__config__.tag
} ${type
} ${vModel
} ${format
} ${valueFormat
} ${width
} ${placeholder
} ${startPlaceholder
} ${endPlaceholder
} ${rangeSeparator
} ${clearable
} ${readonly
} ${disabled
}></${el.__config__.tag
}>
`
}
,
'el-rate': el =>
{
const {
disabled, vModel
} = attrBuilder(el)
const max = el.max ? `:max='${el.max
}'` : ''
const allowHalf = el['allow-half'] ? 'allow-half' : ''
const showText = el['show-text'] ? 'show-text' : ''
const showScore = el['show-score'] ? 'show-score' : ''
return `<
${el.__config__.tag
} ${vModel
} ${max
} ${allowHalf
} ${showText
} ${showScore
} ${disabled
}></${el.__config__.tag
}>
`
}
,
'el-color-picker': el =>
{
const {
disabled, vModel
} = attrBuilder(el)
const size = `size="${el.size
}"`
const showAlpha = el['show-alpha'] ? 'show-alpha' : ''
const colorFormat = el['color-format'] ? `color-format="${el['color-format']
}"` : ''
return `<
${el.__config__.tag
} ${vModel
} ${size
} ${showAlpha
} ${colorFormat
} ${disabled
}></${el.__config__.tag
}>
`
}
,
'el-upload': el =>
{
const disabled = el.disabled ? ':disabled=\'true\'' : ''
const action = el.action ? `:action="${el.__vModel__
}Action"` : ''
const multiple = el.multiple ? 'multiple' : ''
const listType = el['list-type'] !== 'text' ? `list-type="${el['list-type']
}"` : ''
const accept = el.accept ? `accept="${el.accept
}"` : ''
const name = el.name !== 'file' ? `name="${el.name
}"` : ''
const autoUpload = el['auto-upload'] === false ? ':auto-upload="false"' : ''
const beforeUpload = `:before-upload="${el.__vModel__
}BeforeUpload"`
const fileList = `:file-list="${el.__vModel__
}fileList"`
const ref = `ref="${el.__vModel__
}"`
let child = buildElUploadChild(el)
if (child) child = `\n${child
}\n` // 换行
return `<
${el.__config__.tag
} ${ref
} ${fileList
} ${action
} ${autoUpload
} ${multiple
} ${beforeUpload
} ${listType
} ${accept
} ${name
} ${disabled
}>
${child
}</${el.__config__.tag
}>
`
}
,
tinymce: el =>
{
const {
tag, vModel, placeholder
} = attrBuilder(el)
const height = el.height ? `:height="${el.height
}"` : ''
const branding = el.branding ? `:branding="${el.branding
}"` : ''
return `<
${tag
} ${vModel
} ${placeholder
} ${height
} ${branding
}></${tag
}>
`
}
}
function attrBuilder(el
) {
return {
vModel: `v-model="${confGlobal.formModel
}.${el.__vModel__
}"`
,
clearable: el.clearable ? 'clearable' : ''
,
placeholder: el.placeholder ? `placeholder="${el.placeholder
}"` : ''
,
width: el.style && el.style.width ? ':style="{width: \'100%\'}"' : ''
,
disabled: el.disabled ? ':disabled=\'true\'' : ''
}
}
子元素构建
// el-button 子级
function buildElButtonChild(conf
) {
const children = []
if (conf.__config__.defaultValue) {
children.push(conf.__config__.defaultValue)
}
return children.join('\n'
)
}
// el-input innerHTML
function buildElInputChild(conf
) {
const children = []
if (conf.__slot__ && conf.__slot__.prepend) {
children.push(`<template slot="prepend">${conf.__slot__.prepend
}</template>
`
)
}
if (conf.__slot__ && conf.__slot__.append) {
children.push(`<template slot="append">${conf.__slot__.append
}</template>
`
)
}
return children.join('\n'
)
}
function buildElSelectChild(conf
) {
const children = []
if (conf.__slot__.options && conf.__slot__.options.length) {
children.push(`<el-option v-for="(item, index) in ${conf.__vModel__
}Options" :key="index" :label="item.label" :value="item.value" :disabled="item.disabled"></el-option>
`
)
}
return children.join('\n'
)
}
function buildElRadioGroupChild(conf
) {
const children = []
if (conf.__slot__.options && conf.__slot__.options.length) {
const tag = conf.__config__.optionType === 'button' ? 'el-radio-button' : 'el-radio'
const border = conf.__config__.border ? 'border' : ''
children.push(`<
${tag
} v-for="(item, index) in ${conf.__vModel__
}Options" :key="index" :label="item.value" :disabled="item.disabled" ${border
}>{{item.label}}</${tag
}>
`
)
}
return children.join('\n'
)
}
function buildElCheckboxGroupChild(conf
) {
const children = []
if (conf.__slot__.options && conf.__slot__.options.length) {
const tag = conf.__config__.optionType === 'button' ? 'el-checkbox-button' : 'el-checkbox'
const border = conf.__config__.border ? 'border' : ''
children.push(`<
${tag
} v-for="(item, index) in ${conf.__vModel__
}Options" :key="index" :label="item.value" :disabled="item.disabled" ${border
}>{{item.label}}</${tag
}>
`
)
}
return children.join('\n'
)
}
function buildElUploadChild(conf
) {
const list = []
if (conf['list-type'] === 'picture-card'
) list.push('<i class="el-icon-plus"></i>'
)
else list.push(`<el-button size="small" type="primary" icon="el-icon-upload">
${conf.__config__.buttonText
}</el-button>
`
)
if (conf.__config__.showTip) list.push(`<div slot="tip" class="el-upload__tip">只能上传不超过 ${conf.__config__.fileSize
}${conf.__config__.sizeUnit
} 的${conf.accept
}文件</div>
`
)
return list.join('\n'
)
}
表单模板
export
function dialogWrapper(str
) {
return `<el-dialog v-bind="$attrs" v-on="$listeners" @open="onOpen" @close="onClose" title="Dialog Title">${str}<div slot="footer"><el-button @click="close">取消</el-button><el-button type="primary" @click="handleConfirm">确定</el-button></div>
</el-dialog>
`
}
export
function vueTemplate(str
) {
return `<template><div>${str}</div>
</template>
`
}
export
function vueScript(str
) {
return `<script>${str}
</script>
`
}
export
function cssStyle(cssStr
) {
return `<style>${cssStr}
</style>
`
}
function buildFormTemplate(conf, child, type
) {
let labelPosition = ''
if (conf.labelPosition !== 'right'
) {
labelPosition = `label-position="${conf.labelPosition
}"`
}
const disabled = conf.disabled ? `:disabled="${conf.disabled
}"` : ''
let str = `<el-form ref="${conf.formRef
}" :model="${conf.formModel
}" :rules="${conf.formRules
}" size="${conf.size
}" ${disabled
} label-width="${conf.labelWidth
}px" ${labelPosition
}>
${child
}
${buildFromBtns(conf, type)
}
</el-form>
`
if (someSpanIsNot24) {
str = `<el-row :gutter="${conf.gutter
}">
${str
}
</el-row>
`
}
return str
}
function buildFromBtns(conf, type
) {
let str = ''
if (conf.formBtns && type === 'file'
) {
str = `<el-form-item size="large">
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
`
if (someSpanIsNot24) {
str = `<el-col :span="24">
${str
}
</el-col>
`
}
}
return str
}
渲染组件
render.js是表单生成器的渲染核心模块,负责将JSON配置转换为实际的Vue组件
。以下是详细分析:
- 文件结构和主要功能:
- 实现了一个基于Vue render函数的
表单渲染器
- 支持
动态插槽加载和事件处理
- 提供完整的数据对象构建流程
- 核心执行流程:
- 初始化时
动态加载插槽组件
- 通过render函数
将配置转换为DOM
- 处理
v-model
、事件绑定
和属性配置
- 主要函数功能:
vModel
: 处理双向数据绑定mountSlotFiles
:挂载插槽内容
emitEvents
: 处理事件发射- buildDataObject : 构建
渲染数据对象
- makeDataObject : 创建
基础数据结构
- 特点和优势:
- 灵活的
插槽系统
:支持动态加载和自定义插槽内容 - 完善的
事件处理
:自动转换事件处理器 - 强大的数据处理:支持
多种数据类型和属性合并
- 清晰的代码结构:职责分明,易于维护
- 与其他模块的关系:
- 配合 js.js 处理组件逻辑
- 与 html.js 协同生成完整组件
- 使用 deepClone 确保配置对象的独立性
- render.js 与 js.js 配合处理Vue组件逻辑生成
- 与 html.js 协同完成组件模板和数据绑定
- 通过 deepClone 工具函数确保配置对象的深拷贝独立性
- 使用不同样式区分核心模块( module )和工具函数( tool )
- render.js 与 js.js 配合处理Vue组件逻辑生成
- 与 html.js 协同完成组件模板和数据绑定
- 通过 deepClone 工具函数确保配置对象的深拷贝独立性
- 使用不同样式区分核心模块( module )和工具函数( tool )
/**
* 表单渲染器模块
* 负责将字符串形式配置转换为Vue渲染函数
* 支持自定义插槽和事件处理
*/
import {
deepClone
}
from '@/utils/index'
// 组件插槽配置对象
const componentChild = {
}
/**
* 动态导入并注册插槽组件
* 将./slots目录下的所有.js文件挂载到componentChild对象
* @param {Object} componentChild - 组件插槽配置对象
* @param {Function} require.context - Webpack的require.context函数
*/
const slotsFiles = require.context('./slots'
, false
, /\.js$/
)
const keys = slotsFiles.keys(
) || []
keys.forEach(key =>
{
const tag = key.replace(/^\.\/(.*)\.\w+$/
, '$1'
)
componentChild[tag] = slotsFiles(key).default
}
)
/**
* 处理组件的v-model指令
* @param {Object} dataObject - 组件数据对象
* @param {*} defaultValue - 默认值
*/
function vModel(dataObject, defaultValue
) {
dataObject.props.value = defaultValue
dataObject.on.input = val =>
{
this.$emit('input'
, val)
}
}
/**
* 挂载插槽内容
* @param {Function} h - Vue的createElement函数
* @param {Object} confClone - 组件配置对象的克隆
* @param {Array} children - 子节点数组
*/
function mountSlotFiles(h, confClone, children
) {
const childObjs = componentChild[confClone.__config__.tag]
if (childObjs) {
Object.keys(childObjs).forEach(key =>
{
const childFunc = childObjs[key]
if (confClone.__slot__ && confClone.__slot__[key]
) {
children.push(childFunc(h, confClone, key)
)
}
}
)
}
}
/**
* 处理事件发射
* 将字符串类型的事件处理器转换为实际的函数
* @param {Object} confClone - 组件配置对象的克隆
*/
function emitEvents(confClone
) {
['on'
, 'nativeOn'].forEach(attr =>
{
const eventKeyList = Object.keys(confClone[attr] || {
}
)
eventKeyList.forEach(key =>
{
const val = confClone[attr][key]
if (
typeof val === 'string'
) {
confClone[attr][key] = event =>
this.$emit(val, event)
}
}
)
}
)
}
/**
* 构建组件的数据对象
* 处理props、attrs等配置
* @param {Object} confClone - 组件配置对象的克隆
* @param {Object} dataObject - 渲染数据对象
*/
function buildDataObject(confClone, dataObject
) {
Object.keys(confClone).forEach(key =>
{
const val = confClone[key]
if (key === '__vModel__'
) {
vModel.call(
this
, dataObject, confClone.__config__.defaultValue)
}
else
if (dataObject[key] !==
undefined
) {
if (dataObject[key] ===
null
|| dataObject[key]
instanceof RegExp
|| ['boolean'
, 'string'
, 'number'
, 'function'].includes(
typeof dataObject[key]
)
) {
dataObject[key] = val
}
else
if (Array.isArray(dataObject[key]
)
) {
dataObject[key] = [...dataObject[key]
, ...val]
}
else {
dataObject[key] = {
...dataObject[key]
, ...val
}
}
}
else {
dataObject.attrs[key] = val
}
}
)
// 清理属性
clearAttrs(dataObject)
}
/**
* 清理内部使用的属性
* @param {Object} dataObject - 渲染数据对象
*/
function clearAttrs(dataObject
) {
delete dataObject.attrs.__config__
delete dataObject.attrs.__slot__
delete dataObject.attrs.__methods__
}
/**
* 创建渲染函数数据对象的基础结构
* 包含class、attrs、props等Vue渲染函数所需的所有属性
* @returns {Object} 渲染数据对象
*/
function makeDataObject(
) {
// 深入数据对象:
// https://cn.vuejs.org/v2/guide/render-function.html#%E6%B7%B1%E5%85%A5%E6%95%B0%E6%8D%AE%E5%AF%B9%E8%B1%A1
return {
class: {
}
,
attrs: {
}
,
props: {
}
,
domProps: {
}
,
nativeOn: {
}
,
on: {
}
,
style: {
}
,
directives: []
,
scopedSlots: {
}
,
slot:
null
,
key:
null
,
ref:
null
,
refInFor: true
}
}
/**
* 表单渲染器组件
* 使用Vue的render函数将JSON配置转换为实际的DOM
*/
export
default {
props: {
conf: {
type: Object,
required: true
}
}
,
render(h
) {
const dataObject = makeDataObject(
)
const confClone = deepClone(
this.conf)
const children =
this.$slots.default || []
// 如果slots文件夹存在与当前tag同名的文件,则执行文件中的代码
mountSlotFiles.call(
this
, h, confClone, children)
// 将字符串类型的事件,发送为消息
emitEvents.call(
this
, confClone)
// 将json表单配置转化为vue render可以识别的 “数据对象(dataObject)”
buildDataObject.call(
this
, confClone, dataObject)
return h(
this.conf.__config__.tag, dataObject, children)
}
}