form-generator: Element UI表单设计及代码生成器
form-generator经典vue的表单设计器一些设计原理记录
目前有的布局方式:
属性 | 可选性 | 说明 | 默认值 |
---|---|---|---|
__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 ) |
属性 | 可选性 | 说明 | 默认值 |
---|---|---|---|
__config__.layout | 可选 | 组件使用的布局方式 | colFormItem |
__config__.componentName | 必选 | 组件名,无需操作 | 系统自增 |
__config__.tagIcon | 必选 | 组件svg图标名称 | |
__config__.layoutTree | 可选 | 是否显示布局树 | |
__config__.children | 必选 | 子组件,组件嵌套的关键 | [] |
__config__.document | 可选 | 组件说明文档地址 | |
其余属性 | 可选 | 可参照?el-row属性表按需配置 |
项目使用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代码生成器生成规则。
本文描述的解析器,是一个能将form-generator导出的json表单,解析为一个真实表单的程序。
接下来的行文中使用【json表单】表示form-generator导出的json表单。
剧透:本文其实就是带大家阅读parser.vue源码,哈哈。
json表单目前支持两种布局:
colFormItem和rowFormItem
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>
)
},
...
}
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
},
上边的一系列操作,都是建立在理解json表单都有哪些内容的基础上的。详细请参阅JSON参数对照表
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)
}
在《表单设计器 · 开发教程》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对象中即可。
后续再补充。