在设计低代码表单生成器之前,需要了解组件库相关内容的基础内容
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)
}
}