低代码——表单生成器Form Generator详解(二)——从JSON配置项到动态渲染表单渲染
在设计低代码表单生成器之前,需要了解组件库相关内容的基础内容
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} ` 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\')} ` 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 innerHTMLfunction buildElInputChild(conf) { const children = [] if (conf.__slot__ && conf.__slot__.prepend) { children.push(`${conf.__slot__.prepend}`) } if (conf.__slot__ && conf.__slot__.append) { children.push(`${conf.__slot__.append}`) } 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\">`) } 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(\'\') else list.push(`${conf.__config__.buttonText}`) if (conf.__config__.showTip) list.push(`只能上传不超过 ${conf.__config__.fileSize}${conf.__config__.sizeUnit} 的${conf.accept}文件`) return list.join(\'\\n\')}
表单模板
export function dialogWrapper(str) { return ` ${str} 取消 确定 `}export function vueTemplate(str) { return ` ${str} `}export function vueScript(str) { return ` ${str} `}export function cssStyle(cssStr) { return ` ${cssStr} `}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)} ` if (someSpanIsNot24) { str = `<el-row :gutter=\"${conf.gutter}\"> ${str} ` } return str}function buildFromBtns(conf, type) { let str = \'\' if (conf.formBtns && type === \'file\') { str = ` 提交 重置 ` if (someSpanIsNot24) { str = ` ${str} ` } } 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) }}