form-generator: Element UI表单设计及代码生成器
form-generator经典vue的表单设计器一些设计原理记录
1、JSON表单参数对照表
对项目中的json表单配置做一些参数说明。
内置布局及其属性
目前有的布局方式:
- colFormItem:生成el-col包裹的组件布局
- rowFormItem:生成一个空的el-row
colFormItem
属性 | 可选性 | 说明 | 默认值 |
__config__.layout |
可选 | 组件使用的布局方式 | colFormItem |
__vModel__ |
必选 | 表单字段的属性名,可自定义 | 系统自增 |
__config__.defaultValue |
可选 | 默认值;与__vModel__ 对应使用,可指定表单字段的默认值;可用于表单数据回填 |
|
__config__.tag |
必选 | 组件名称 | |
__config__.changeTag |
必选 | 是否允许显示切换组件面板 | |
__config__.tagIcon |
必选 | 组件svg图标名称 | |
__config__.label |
必选 | 表单标题 | |
__config__.showLabel |
必选 | 是否显示表单标题 | |
__config__.labelWidth |
必选 | 表单标题区域宽度(px) | |
__config__.required |
必选 | 是否要求表单校验 | |
__config__.regList |
可选 | 表单正则校验;赋值为数组时,显示配置项 | |
__config__.span |
必选 | 24栅格系统,表示组件的栅格数 | |
__config__.children |
可选 | 子组件,目前仅保留字段,实际并没有做解析 | |
__config__.document |
可选 | 组件说明文档地址 | |
__slot__ |
可选 | 对应,需在工程文件夹src\components\render\slots中添加与__config__.tag 同名的.js文件解析该配置。 |
|
其余属性 | 可选 | 根据不同组件的属性灵活配置。属于本组件的属性写在一级(与__config__ 同级);若需自定义属性以达到控制右侧面板或其他目的的,可在__config__ 中自定义属性(如:__config__.showLabel ) |
rowFormItem
属性 | 可选性 | 说明 | 默认值 |
__config__.layout |
可选 | 组件使用的布局方式 | colFormItem |
__config__.componentName |
必选 | 组件名,无需操作 | 系统自增 |
__config__.tagIcon |
必选 | 组件svg图标名称 | |
__config__.layoutTree |
可选 | 是否显示布局树 | |
__config__.children |
必选 | 子组件,组件嵌套的关键 | [] |
__config__.document |
可选 | 组件说明文档地址 | |
其余属性 | 可选 | 可参照 el-row属性表按需配置 |
2、表单设计器
项目使用vue-cli4生成。用到了jsx,所以要对vue render比较熟悉!!! 如果对render和jsx还不熟悉,一定要反复阅读并理解:渲染函数 & JSX。二开对于初学者,有一定的难度。
项目由四部分组成:表单设计器,.vue代码生成器,.vue代码预览器,表单json解析器。
接下来通过添加一个《按钮 el-button》来带大家感受下这四部分。
前置准备:将项目下载到本地,然后安装依赖。如有需要可参阅运行
一、在添加一个新组件前,首先要思考的是,项目中有没有引入该组件?
对于el-button,它是随element UI全局注册的组件,所以不需要再引入。如果是一个没有引入的组件,需要引入,引入方法参阅vue官方文档组件注册
二、将组件添加到表单设计器
确保el-button组件可用后,将其添加到表单设计器。
2.1 在文件src\components\generator\config.js中添加一个布局型组件
... export const layoutComponents = [ ..., { __config__: { label: '按钮', showLabel: true, changeTag: true, labelWidth: null, tag: 'el-button', tagIcon: 'button', defaultValue: undefined, span: 24, layout: 'colFormItem', document: 'https://element.eleme.cn/#/zh-CN/component/button' }, __slot__: { default: '主要按钮' }, type: 'primary', icon: 'el-icon-search', round: false, size: 'medium', plain: false, circle: false, disabled: false } ]
其中__config__和__slot__是本项目自定义的属性,自定义属性的格式均为__XXX__;
其余属性与el-button组件属性对应;
config.tagIcon中使用的是svg图标。图标来自iconfont,下载后放在src\icons\svg文件夹中。
此时,左侧备选组件会出现【按钮】组件,但是,按钮不能显示文字。
2.2 新建与__config__.tag的值同名的__slot__解析文件el-button.js
src\components\render\slots\el-button.js,代码如下:
export default { default(h, conf, key) { return conf.__slot__[key] } }
default函数解析将json配置中的default属性:
__slot__: { default: '主要按钮' }
解析为按钮上的文字。
此时,中间设计器中,按钮上的文字已经可以显示出来了。但是,右侧面板中,可配置属性还比较少,需要添加属性配置。
__slot__解析文件是支持jsx语法的,本例中表现的不够具体,更多的使用方式可以翻阅源码中slots文件夹;其中el-input.js代表性强,建议理解。
__slot__的解析流程设计得比较绕,主要的出发点是为了:保证表单的配置是纯json格式的,方便数据库存储和用户配置。这里的【用户】指的是:没有编程基础的普通用户。
2.3 接下来我们让设计器支持type,icon等组件属性的可视化修改。
在src\views\index\RightPanel.vue中添加相应的编辑表单项。
2.3.1 type属性配置项:
<el-form-item v-if="activeData.type !== undefined && activeData.__config__.tag === 'el-button'" label="按钮类型" > <el-select v-model="activeData.type" :style="{ width: '100%' }"> <el-option label="primary" value="primary" /> <el-option label="success" value="success" /> <el-option label="warning" value="warning" /> <el-option label="danger" value="danger" /> <el-option label="info" value="info" /> <el-option label="text" value="text" /> </el-select> </el-form-item>
2.3.2 size属性配置项:经过检查el-color-picker已经有size属性的配置项了,所以重用原有的就行了。
增加
activeData.__config__.tag === 'el-button'
增加后的配置项如下:
<el-form-item v-if="activeData.size !== undefined && (activeData.__config__.optionType === 'button' || activeData.__config__.border || activeData.__config__.tag === 'el-color-picker' || activeData.__config__.tag === 'el-button')" label="选项尺寸" > <el-radio-group v-model="activeData.size"> <el-radio-button label="medium"> 中等 </el-radio-button> <el-radio-button label="small"> 较小 </el-radio-button> <el-radio-button label="mini"> 迷你 </el-radio-button> </el-radio-group> </el-form-item>
2.3.3 icon属性配置项:复制el-input的前图标配置项,修改为:
<el-form-item v-if="activeData['icon']!==undefined && activeData.__config__.tag === 'el-button'" label="按钮图标" > <el-input v-model="activeData['icon']" placeholder="请输入按钮图标名称"> <el-button slot="append" icon="el-icon-thumb" @click="openIconsDialog('icon')"> 选择 </el-button> </el-input> </el-form-item>
此处使用了openIconsDialog调用封装好的图标选择器,方便快速选取图标。
组件属性的可视化配置是一项需要耐心的操作,以上列举了3个属性的配置,更多的属性也都是配置在RightPanel.vue中。当然,现有的配置方式存在一定的问题,这是需要在以后项目中逐步优化的。
总结
表单设计器的开发流程基本就是上边这三步。config.js配置备选图标;在有使用__slot__时需要编写解析文件;在RightPanel.vue可视化配置组件属性。
接下来,当点击运行按钮的时候,发现新加的组件并不能显示。这是因为没有编写相应的.vue代码生成器生成规则。
3、表单解析器
本文描述的解析器,是一个能将form-generator导出的json表单,解析为一个真实表单的程序。
接下来的行文中使用【json表单】表示form-generator导出的json表单。
剧透:本文其实就是带大家阅读parser.vue源码,哈哈。
布局
json表单目前支持两种布局:
colFormItem和rowFormItem
1.1 colFormItem布局
colFormItem布局(以el-input为例)对应的json形式如下:
{ "__config__": { "label": "单行文本", "labelWidth": null, "showLabel": true, "changeTag": true, "tag": "el-input", "tagIcon": "input", "required": true, "layout": "colFormItem", "span": 12, "document": "https://element.eleme.cn/#/zh-CN/component/input", "regList": [{ "pattern": "/^1(3|4|5|7|8|9)\\d{9}$/", "message": "手机号格式错误" }] }, "__slot__": { "prepend": "", "append": "" }, "__vModel__": "mobile", "placeholder": "请输入手机号", "style": { "width": "100%" }, "clearable": true, "prefix-icon": "el-icon-mobile", "suffix-icon": "", "maxlength": 11, "show-word-limit": true, "readonly": false, "disabled": false }
colFormItem布局对应的目标实际代码如下 :
<el-col :span="12"> <el-form-item label="单行文本" prop="mobile"> <el-input v-model="formData.mobile" placeholder="请输入手机号" :maxlength="11" show-word-limit clearable prefix-icon='el-icon-mobile' :style="{width: '100%'}"></el-input> </el-form-item> </el-col>
在这个json到xml的解析过程中,form-generator的parser使用jsx来完成
... const layouts = { colFormItem(h, scheme) { const config = scheme.__config__ const listeners = buildListeners.call(this, scheme) let labelWidth = config.labelWidth ? `${config.labelWidth}px` : null if (config.showLabel === false) labelWidth = '0' return ( <el-col span={config.span}> <el-form-item label-width={labelWidth} prop={scheme.__vModel__} label={config.showLabel ? config.label : ''}> <render conf={scheme} {...{ on: listeners }} /> </el-form-item> </el-col> ) }, ... }
1.2 rowFormItem布局
rowFormItem布局对应的json形式如下:
{ "__config__": { "layout": "rowFormItem", "tagIcon": "row", "layoutTree": true, "document": "https://element.eleme.cn/#/zh-CN/component/layout#row-attributes", "span": 12, "formId": 104, "renderKey": 1594570310282, "componentName": "row104", "children": [] }, "type": "default", "justify": "start", "align": "top" }
对应的目标代码如下:
<el-col :span="12"> <el-row> </el-row> </el-col>
同样使用jsx来完成布局解析:
rowFormItem(h, scheme) { let child = renderChildren.apply(this, arguments) if (scheme.type === 'flex') { child = <el-row type={scheme.type} justify={scheme.justify} align={scheme.align}> {child} </el-row> } return ( <el-col span={scheme.span}> <el-row gutter={scheme.gutter}> {child} </el-row> </el-col> ) }
值得注意的是,json表单支持嵌套; 通过__config__.children记录嵌套关系。使用renderChildren递归解析。(目前仅对rowFormItem布局的children做解析)
function renderChildren(h, scheme) { const config = scheme.__config__ if (!Array.isArray(config.children)) return null return renderFormItem.call(this, h, config.children) }
完整的代码,请阅读parse源码,此链接中的版本并不算复杂。
数据和逻辑
传统的vue格式表单,我们可能需要写如下格式的js,完成element UI表单的数据和逻辑。
export default { data() { return { formData: { mobile: undefined, field103: undefined, }, rules: { mobile: [{ required: true, message: '请输入手机号', trigger: 'blur' }, { pattern: /^1(3|4|5|7|8|9)\d{9}$/, message: '手机号格式错误', trigger: 'blur' }], field103: [{ required: true, message: '请输入密码', trigger: 'blur' }], }, } }, methods: { submitForm() { this.$refs['elForm'].validate(valid => { if (!valid) return // TODO 提交表单 }) }, resetForm() { this.$refs['elForm'].resetFields() }, } }
对于解析器来说,这是一个抽象的过程:
- 数据部分:
构建表单数据实现如下:
data() { const data = { formConfCopy: deepClone(this.formConf), [this.formConf.formModel]: {}, [this.formConf.formRules]: {} } this.initFormData(data.formConfCopy.fields, data[this.formConf.formModel]) this.buildRules(data.formConfCopy.fields, data[this.formConf.formRules]) return data },
- 逻辑部分:
请阅读,源码 methods 部分。这块和你日常vue编程差不多,只不过属性都是抽象的。
JSON表单结构说明
上边的一系列操作,都是建立在理解json表单都有哪些内容的基础上的。详细请参阅JSON参数对照表
form-generator中的render.js
render.js就是对vue的render函数的简单定制封装。如果你还不理解vue的render函数,请移步至:渲染函数 & JSX
render.js实现的功能是将json表单中的__config__.tag解析为具体的vue组件; 其工作过程可以理解为以下3个部分:
render(h) { const dataObject = makeDataObject() const confClone = deepClone(this.conf) const children = [] // 1 如果slots文件夹存在与当前tag同名的文件,则执行文件中的代码 mountSlotFlies.call(this, h, confClone, children) // 2 将字符串类型的事件,发送为消息 emitEvents.call(this, confClone) // 3 将json表单配置转化为vue render可以识别的 “数据对象(dataObject)” buildDataObject.call(this, confClone, dataObject) return h(this.conf.__config__.tag, dataObject, children) }
4、vue代码生成器
在《表单设计器 · 开发教程》el-button已经可以可视化配置属性了。如果你仅仅想使用json格式的表单配置,可以跳过本文,直接阅读《表单解析器 · 开发教程》。
本文将继续完成vue代码生成器部分的教程。
点击【导出vue文件】按钮的时候,需要选择一个【生成类型】。说明目前支持生成,文件和弹框两种类型的代码。其实文件类型的代码用el-dialog包裹下就是弹框类型的代码了。
而生成代码的本质就是简单的字符串拼接。分别拼接出html、js、css三种类型的代码,最后组装成vue代码。
代码生成器中大量使用了:es6 模板字符串
一、生成html代码
在文件src\components\generator\html.js中添加el-button的html代码生成规则:
1.1 在tags对象中添加el-button属性,生成html
... 'el-button': el => { const { tag, disabled } = attrBuilder(el) const type = el.type ? `type="${el.type}"` : '' const icon = el.icon ? `icon="${el.icon}"` : '' const round = el.round ? 'round' : '' const size = el.size ? `size="${el.size}"` : '' const plain = el.plain ? 'plain' : '' const circle = el.circle ? 'circle' : '' let child = buildElButtonChild(el) if (child) child = `\n${child}\n` // 换行 return `<${tag} ${type} ${icon} ${round} ${size} ${plain} ${disabled} ${circle}>${child}</${tag}>` }, ...
attrBuilder会生成常用的属性,这里与el-button匹配的是tag, disabled;其余属性都是和el-button组件属性对应的,目标是生成字符串:
`<el-button type="success" icon="el-icon-warning" size="medium"> 主要按钮 </el-button>`
1.2 由于按钮内的文字是配置在__slot__中的
__slot__: { default: '主要按钮' }
所以相应的应该去读取__slot__.default。为了保持和其他组件的统一,定义了函数buildElButtonChild读取__slot__.default。
在文件src\components\generator\html.js中添加buildElButtonChild函数:
// el-buttin 子级 function buildElButtonChild(scheme) { const children = [] const slot = scheme.__slot__ || {} if (slot.default) { children.push(slot.default) } return children.join('\n') }
写好了tags['el-button']和buildElButtonChild函数后,当再次点击运行按钮预览时,发现el-button组件已经可以预览了。
html.js中的代码都是字符串拼接处理并不高深,如需进一步的处理可以从入口函数
makeUpHtml
顺着结构阅读源码。
二、生成js脚本代码
在文件src\components\generator\js.js中,依然是通过字符串拼接的方式,生成脚本代码。
由于el-button无需js脚本,所以本文用el-input组件做讲解:
假设我们有如下的json配置:
{ __config__: { tag: 'el-input', required: true, regList: [{ pattern: '/^1(3|4|5|7|8|9)\\d{9}$/', message: '手机号格式错误' }] }, __vModel__: 'mobile', placeholder: '请输入手机号', }
目标是生成element UI表单校验js代码:
mobile: [{ required: true, message: '请输入手机号', trigger: 'blur' }, { pattern: /^1(3|4|5|7|8|9)\d{9}$/, message: '手机号格式错误', trigger: 'blur' }]
json配置中有两个校验规则:required和regList,我们要做的代码生成,无非就是将json配置中的key和value,转化成js代码字符串。源码中的转化实现如下:
// 构建校验规则 function buildRules(scheme, ruleList) { const config = scheme.__config__ if (scheme.__vModel__ === undefined) return const rules = [] if (ruleTrigger[config.tag]) { if (config.required) { const type = isArray(config.defaultValue) ? 'type: \'array\',' : '' let message = isArray(config.defaultValue) ? `请至少选择一个${config.label}` : scheme.placeholder if (message === undefined) message = `${config.label}不能为空` rules.push(`{ required: true, ${type} message: '${message}', trigger: '${ruleTrigger[config.tag]}' }`) } if (config.regList && isArray(config.regList)) { config.regList.forEach(item => { if (item.pattern) { rules.push( `{ pattern: ${eval(item.pattern)}, message: '${item.message}', trigger: '${ruleTrigger[config.tag]}' }` ) } }) } ruleList.push(`${scheme.__vModel__}: [${rules.join(',')}],`) } }
上边的函数就是一个json配置key和value的搬运工,很朴实的一段代码,所以js.js中其他生成脚本的代码也不神秘,如有需要放开去看源码就行了,入口函数:
makeUpJs
三、生成css
css部分请直接看源码。文件:src\components\generator\css.js
重点看:
const styles = { 'el-rate': '.el-rate{display: inline-block; vertical-align: text-top;}', 'el-upload': '.el-upload__tip{line-height: 1.2;}' }
此文件只做了一件简单事情:遍历待生成代码的json表单配置。如果配置中使用了el-rate或el-upload,将他们的css样式生成出去。这就是全部。入口函数:
makeUpCss
如果你要改写某个组件的默认样式,比如el-button,将你需要的css加进styles对象中即可。
5、vue代码预览器
后续再补充