重新梳理了一下 form-generator 的实现原理,扒了一下源码。虽然项目是 vue2 的,但其实低代码思路是一致的,所以还是有价值的,源码版本使用的
0.2.0
,一起来学习一下吧
使用
如上图所示,从左侧组件列表将组件拖入中间,中间可以看到整体表单的效果,同时支持上下拖动、调整顺序;组件的属性可以通过右侧的配置面板修改,例如标签、大小、字段名等
支持出码,点击上方运行后,进入预览页面
左侧为产出的代码,右侧为渲染出的表单效果
这是产出的 JSON DSL,不用细看,贴出来只是为了后面讲源码可以对照
json
{
"fields": [
{
"__config__": {
"layout": "rowFormItem",
"tagIcon": "row",
"layoutTree": true,
"document": "https://element.eleme.cn/#/zh-CN/component/layout#row-attributes",
"span": 24,
"formId": 101,
"renderKey": "1011701267042417",
"componentName": "row101",
"children": [
{
"__config__": {
"label": "单行文本",
"labelWidth": null,
"showLabel": true,
"changeTag": true,
"tag": "el-input",
"tagIcon": "input",
"required": true,
"layout": "colFormItem",
"span": 24,
"document": "https://element.eleme.cn/#/zh-CN/component/input",
"regList": [],
"formId": 102,
"renderKey": "1021701267046717"
},
"__slot__": {
"prepend": "",
"append": ""
},
"placeholder": "请输入单行文本",
"style": {
"width": "100%"
},
"clearable": true,
"prefix-icon": "",
"suffix-icon": "",
"maxlength": null,
"show-word-limit": false,
"readonly": false,
"disabled": false,
"__vModel__": "field102"
}
]
},
"type": "default",
"justify": "start",
"align": "top"
}
],
"formRef": "elForm",
"formModel": "formData",
"size": "medium",
"labelPosition": "right",
"labelWidth": 100,
"formRules": "rules",
"gutter": 15,
"disabled": false,
"span": 24,
"formBtns": true
}
感兴趣地可以自行体验一波 mrhj.gitee.io/form-genera...
思路
低代码的本质无非就是
可视化配置 => JSON DSL => 组件组合 => 生成页面
本次我打算将这个过程拆成两部分来介绍
- 出码:
JSON DSL => vue 代码 => 生成页面
- 配置:
可视化配置 => JSON DSL => 生成页面
出码
代码做了简化,仅保留了主流程代码,感兴趣可以自行查阅完整代码
template
js
// src\components\generator\html.js
export function makeUpHtml(formConfig, type) {
const htmlList = [];
// formConfig 即上面的 json 串
confGlobal = formConfig;
/**
* 每行默认 24 列栅格,可手动修改
* 这里先判断布局是否都沾满了24个栅格
* 先将结果缓存起来
*/
someSpanIsNot24 = formConfig.fields.some(
(item) => item.__config__.span !== 24
);
// 遍历渲染每个组件成html
formConfig.fields.forEach((el) => {
// 通过 layout 字段,判断使用哪种布局
htmlList.push(layouts[el.__config__.layout](el));
});
const htmlStr = htmlList.join("\n");
// 将组件代码放进form标签
let temp = buildFormTemplate(formConfig, htmlStr, type);
// dialog 标签包裹代码
if (type === "dialog") {
temp = dialogWrapper(temp);
}
confGlobal = null;
return temp;
}
layouts 中声明了两种布局:colFormItem 和 rowFormItem,关系如下:
下面开始扒源码
js
// src\components\generator\html.js
const layouts = {
colFormItem(scheme) {
/**
* 从 __config__ 字段获取配置
* 组装 label、label-width 等属性
*/
const config = scheme.__config__;
let labelWidth = "";
let label = `label="${config.label}"`;
if (config.labelWidth && config.labelWidth !== confGlobal.labelWidth) {
labelWidth = `label-width="${config.labelWidth}px"`;
}
if (config.showLabel === false) {
labelWidth = 'label-width="0"';
label = "";
}
const required =
!ruleTrigger[config.tag] && config.required ? "required" : "";
/**
* 这一步就是加载 输入型 或 选择型 组件
* 组件信息同样存在一个对象中 tags
*/
const tagDom = tags[config.tag] ? tags[config.tag](scheme) : null;
/**
* 这里就能明显看出
* colFormItem 布局就是直接使用的 el-form-item 组件
*/
let str = `<el-form-item ${labelWidth} ${label} prop="${scheme.__vModel__}" ${required}>
${tagDom}
</el-form-item>`;
/**
* 这里判断当前行是不是占用 24 列栅格
* 通过 __config__ 下的 span 字段判断
* 如果不是 24
* 将在外层再包裹一个 el-col 组件
*/
str = colWrapper(scheme, str);
return str;
},
rowFormItem(scheme) {
/**
* 同样从 __config__ 字段获取配置
* 组装属性
*/
const config = scheme.__config__;
// type 有 default 和 flex 两种布局
const type = scheme.type === "default" ? "" : `type="${scheme.type}"`;
const justify =
scheme.type === "default" ? "" : `justify="${scheme.justify}"`;
const align = scheme.type === "default" ? "" : `align="${scheme.align}"`;
const gutter = scheme.gutter ? `:gutter="${scheme.gutter}"` : "";
/**
* 与 colFormItem 不同的是
* rowFormItem 的子组件还是一个布局组件
* 里面包裹的正是 colFormItem
*/
const children = config.children.map((el) =>
layouts[el.__config__.layout](el)
);
// rowFormItem 布局使用的是 el-row 组件
let str = `<el-row ${type} ${justify} ${align} ${gutter}>
${children.join("\n")}
</el-row>`;
// 同样判断是否需要加 el-col
str = colWrapper(scheme, str);
return str;
},
};
布局的问题就搞定了,接下来就是 输入型组件 和 选择型组件,它们的逻辑在 tags 对象内
源码中定义了 15 种类型的组件,这里我们就只看其中一种就好,原理都大差不差
js
// src\components\generator\html.js
const tags = {
...
// 输入框组件
"el-input": (el) => {
/**
* attrBuilder 提前解析并配置好属性值
* 如:__vModel__ 若存在,则拼接 v-model 字符串
* `v-model="${confGlobal.formModel}.${el.__vModel__}"`
* 再赋值给 vModel。
* 猜测是大部分组件都有这些属性
* 所以单独提出了个函数
*/
const { tag, 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}}"`
: "";
/**
* 这里处理子组件
* 如 el-select 组件的 el-option
* el-input 组件的子组件是两个插槽:prepend 与 append
* `<template slot="append">${slot.append}</template>`
*/
let child = buildElInputChild(el);
/**
* 给子组件换行
* 因为最终得到的代码不止是用来渲染
* 还需要展示以及出码,还是要尽量美观(-_-)
*/
if (child) child = `\n${child}\n`;
/**
* 这里 tag 就是 el-input
* 然后拼接属性、拼接子组件
*/
return `<${tag} ${vModel} ${type} ${placeholder} ${maxlength} ${showWordLimit} ${readonly} ${disabled} ${clearable} ${prefixIcon} ${suffixIcon} ${showPassword} ${autosize} ${width}>${child}</${tag}>`;
},
...
};
到这里就完成了 vue 组件中 template 部分的代码
js
接下来是 js 代码的生成
js
// src\components\generator\js.js
export function makeUpJs(formConfig, type) {
// 深拷贝一份 json
confGlobal = formConfig = deepClone(formConfig);
const dataList = [];
const ruleList = [];
const optionsList = [];
const propsList = [];
// mixinMethod 混入通用函数
/**
* 如果是生成页面
* 1. 在提交表单时增加表单验证,即:
* this.$refs['${confGlobal.formRef}'].validate(valid => {
* if(!valid) return
* // TODO 提交表单
* })
* 2. 在重置表单时,重置掉表单字段
* this.$refs['${confGlobal.formRef}'].resetFields()
*/
/**
* 如果是生成弹窗
* 1. 监听弹窗唤起:onOpen
* 2. 监听弹窗关闭:onClose
* 同时在关闭后重置表单字段
* 3. 添加关闭事件:close
* this.$emit('update:visible', false)
* 4. 添加确认事件:handelConfirm
* 同时在确认后校验表单
*/
const methodList = mixinMethod(type);
const uploadVarList = [];
const created = [];
// 核心部分,下面展开细说
formConfig.fields.forEach((el) => {
buildAttributes(
el,
dataList,
ruleList,
optionsList,
methodList,
propsList,
uploadVarList,
created
);
});
...
}
js
// src\components\generator\js.js
// 构建组件属性
function buildAttributes(...) {
const config = scheme.__config__
const slot = scheme.__slot__
/**
* 以 __vModel__ 为键名,defaultValue 为值
* 以键值对的方式 push 进 dataList
*/
buildData(scheme, dataList)
/**
* 仅处理了必填和自定义正则
* 1. 必填设置
* 补充 message 等字段,push 到 ruleList
* 2. 正则列表
* 遍历正则列表,使用 pattern 字段,补充 message、trigger
* push 到 ruleList
*/
buildRules(scheme, ruleList)
// 特殊处理options属性
if (scheme.options || (slot && slot.options && slot.options.length)) {
/**
* 处理静态 options
* el-cascader 组件
*/
buildOptions(scheme, optionsList)
// 处理动态 options
if (config.dataType === 'dynamic') {
const model = `${scheme.__vModel__}Options`
const options = titleCase(model)
const methodName = `get${options}`
/**
* 定义动态获取 options 的函数
* methodName 就是函数名
* 内部使用 axios 获取
*/
buildOptionMethod(methodName, model, methodList, scheme)
/**
* 将上一步定义的获取函数 push 入 created 列表
* created 列表在 vue 组件的 creaded 阶段被调用
*/
callInCreated(methodName, created)
}
}
// 处理props
if (scheme.props && scheme.props.props) {
buildProps(scheme, propsList)
}
// 处理 el-upload 的action
if (scheme.action && config.tag === 'el-upload') {
// 处理 upload 的 aciton 和 fileList 变量
uploadVarList.push(
`${scheme.__vModel__}Action: '${scheme.action}',
${scheme.__vModel__}fileList: [],`
)
/**
* 增加文件上传相关函数
* 1. 若设置了fileSize,增加判断文件大小的函数
* 2. 若设置了 accept,增加判断文件类型的函数
*/
methodList.push(buildBeforeUpload(scheme))
// 非自动上传时,生成手动上传的函数
if (!scheme['auto-upload']) {
/**
* 增加 submitUpload 函数
* this.$refs['${scheme.__vModel__}'].submit()
*/
methodList.push(buildSubmitUpload(scheme))
}
}
// 构建子级组件属性
if (config.children) {
config.children.forEach(item => {
buildAttributes(...)
})
}
}
解析完成后就得到了几个列表: dataList、ruleList、optionsList、methodList、propsList、uploadVarList、created
最后一步,使用 join 将列表每一项通过换行符连接,拼接为 vue2 Option API 的形式
js
// src\components\generator\js.js
export function makeUpJs(formConfig, type) {
...
/**
* buildexport 拼接整体js
* 也就是将上一步得到的 data、methods 等信息
* 以 vue2 option API 的方式拼接
* export default {
components: {},
props: [],
data () {
return {
${conf.formModel}: {
${data}
},
${conf.formRules}: {
${rules}
},
${uploadVar}
${selectOptions}
${props}
}
},
computed: {},
watch: {},
created () {
${created}
},
mounted () {},
methods: {
${methods}
}
}
*/
const script = buildexport(
formConfig,
type,
dataList.join("\n"),
ruleList.join("\n"),
optionsList.join("\n"),
uploadVarList.join("\n"),
propsList.join("\n"),
methodList.join("\n"),
created.join("\n")
);
confGlobal = null;
return script;
}
以上就是 js 部分的解析
css
css 部分就简单很多了,因为 form-generator 根本就没想让你通过自定义设置样式,也可能没空做那部分的功能
js
// src\components\generator\css.js
/**
* 仅新增了 el-rate、el-upload
* 两个组件的自定义样式
* 通过类名的方式实现
*/
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");
}
上面的代码就是 css.js 完整的代码,没啥特别的东西就不解释了
最终的代码会使用 style 标签包裹
如果要预览,会直接插入到 html 文件中;当然了,会插入到根结点之前,element-ui 样式链接之后
如果要出码,也就是将代码下载下来,直接放在 vue 文件中
展示/渲染/导出
到这里就完成了 json DSL 到可用代码的转换,接下来无论是想出码还是直接渲染,都不是啥大问题了。
展示
form-generator 展示代码时用到了第三方库 monaco-editor
,感兴趣的可以自行研究一下这个库。
预览
预览本质就是要将转换后的代码渲染出来;
form-generator 这里加了一个 iframe,iframe 加载的是提前写好的 html
- head 部分提前加载 vue、vue-router、element-ui 的 CDN 文件
- body 部分声明一个 id 为 previewApp 的 div 结点
这个 previewApp 的根结点并不直接挂载 vue 实例,实际执行预览时,还会往里加入 style 标签包裹的样式(上一步生成的 css 部分的代码)、一个 id 为 app 的 div 结点
接下来就只要将上面生成的代码搞成 vue 实例挂载到 id 为 app 的结点上
js
// src\views\preview\main.js
/**
* attrs: 仅当渲染的是弹窗时用到,添加 width、visible 等属性
* main:上一步的 js 部分生成的代码,移除了 `export default`
* html:上一步的 template 部分生成的代码
*/
function newVue(attrs, main, html) {
main = eval(`(${main})`);
main.template = `<div>${html}</div>`;
/**
* 新建 vue 实例
* 生成的表单以子组件的形式引入
*/
new Vue({
components: {
child: main,
},
data() {
return {
visible: true,
};
},
template: `<div><child ${attrs}/></div>`,
}).$mount("#app");
}
导出 vue 文件
拼接上面得到的代码,组成 vue 文件(由 template、script、style 三部分组成),再自动下载下来。
配置
可以看出来,可视化配置页面主要分为三部分:左侧的组件列表、中间的渲染面板、右侧的配置面板
为了实现可拖拽,引入了第三方库 vuedraggable
组件列表
这一部分没啥好说的,维护一个组件列表数组,通过 draggable 实现可拖拽,值得一说的也就只有 draggable 组件了
html
<!-- 组件列表 -->
<!-- src\views\index\Home.vue -->
<div v-for="(item, listIndex) in leftComponents" :key="listIndex">
<div class="components-title">
<svg-icon icon-class="component" />
{{ item.title }}
</div>
<!--
这里说一下 group 参数的配置
name 可跨越拖拽的共同类名
pull: clone 拖出操作,变成了复制
put: false 禁止拖入这里
-->
<draggable
class="components-draggable"
:list="item.list"
:group="{ name: 'componentsGroup', pull: 'clone', put: false }"
:clone="cloneComponent"
draggable=".components-item"
:sort="false"
@end="onEnd"
>
<div
v-for="(element, index) in item.list"
:key="index"
class="components-item"
@click="addComponent(element)"
>
<div class="components-body">
<svg-icon :icon-class="element.__config__.tagIcon" />
{{ element.__config__.label }}
</div>
</div>
</draggable>
</div>
draggable 组件,同名的 group 之间可以实现拖拽效果。
在 form-generator 中,组件列表的 draggable 的 group 名为 componentsGroup,与中间渲染面板的 draggable 的 group 名称一致;所以,组件列表与渲染面板之间可以进行拖拽操作。
html
<!-- 渲染面板使用的 draggable -->
<!-- src\views\index\Home.vue -->
...
<draggable
class="drawing-board"
:list="drawingList"
:animation="340"
group="componentsGroup"
>
...
</draggable>
...
drawingList 是渲染面板用到的组件列表
当组件列表中的组件拖拽到中间渲染面板,渲染面板中绑定的 drawingList 变量将会改变
- 从 item.list 中拿到拖拽出来的元素
- 经过 cloneComponent 拷贝一个新元素
- 将新元素放入 drawingList 中
上述的三个步骤都是 draggable 组件内部完成的,也就是 draggable 实现了跨组件的拖拽,并且自己完成了两个 draggable 组件间的通信(自动更新 drawingList),还直接更新了父组件的 drawingList 变量
跨组件
这里不打算深究,直接讲原理 (懒)
vuedraggable 组件本身是基于 sortablejs 实现的(这俩项目是同一个大佬的手笔)
在 vuedraggable 组件的 mounted 阶段,新建了一个 sortable 实例
js
// Vue.Draggable-2.23.2\src\vuedraggable.js
mounted() {
...
this._sortable = new Sortable(this.rootContainer, options);
this.computeIndexes();
}
让我们把视线再转向 sortablejs
sortablejs 内部维护了一个数组 sortables
js
// Sortable\src\Sortable.js
function Sortable(el, options) {
...
/**
* 创建实例时
* 将 html 元素存入 sortables
* 这里的 el 我们可以简单理解为 draggable 组件
*/
sortables.push(this.el);
...
}
每新建一个 Sortable 实例,sortables 中就会多一个实例
在拖拽过程中以及完成时,sortablejs 会去查找可拖入的元素(组件),怎么找呢?
就是通过 sortables
js
_detectNearestEmptySortable = function(x, y) {
let ret;
// 遍历组件们
sortables.some((sortable) => {
...
/**
* 获取 draggable 组件的位置信息
* 以此确定拖入的地方在不在可拖入的范围内
*/
const rect = getRect(sortable),
insideHorizontally =
x >= rect.left - threshold && x <= rect.right + threshold,
insideVertically =
y >= rect.top - threshold && y <= rect.bottom + threshold;
if (insideHorizontally && insideVertically) {
// 在范围内,直接返回组件
return (ret = sortable);
}
});
return ret;
}
之后的事就简单了,通知 vuedraggable,该更新更新
更新父组件变量
你们以为 vuedraggable 会使用 $emit
$ref
吗?
No~ No~ No~
从上面的使用中也能看到,list 属性并没有加 sync
我直接贴段源码
js
// Vue.Draggable-2.23.2\src\vuedraggable.js
alterList(onList) {
if (this.list) {
onList(this.list);
return;
}
const newList = [...this.value];
onList(newList);
/**
* 用的 list 属性,没有 v-model
* 忽略这行
*/
this.$emit("input", newList);
},
updatePosition(oldIndex, newIndex) {
const updatePosition = list =>
list.splice(newIndex, 0, list.splice(oldIndex, 1)[0]);
this.alterList(updatePosition);
},
看到了吗?就是简单粗暴的 splice,然后?然后就没了
渲染展示
html
<!-- src\views\index\Home.vue -->
<!-- 页面滚动,增加 scrollbar -->
<el-scrollbar class="center-scrollbar">
<!-- 布局组件,美观 -->
<el-row class="center-board-row" :gutter="formConf.gutter">
<!-- 直接写好 el-form -->
<el-form
:size="formConf.size"
:label-position="formConf.labelPosition"
:disabled="formConf.disabled"
:label-width="formConf.labelWidth + 'px'"
>
<!-- 老熟人了,draggable -->
<draggable
class="drawing-board"
:list="drawingList"
:animation="340"
group="componentsGroup"
>
<!-- 自定义组件,一会儿展开细嗦 -->
<draggable-item
v-for="(item, index) in drawingList"
:key="item.renderKey"
:drawing-list="drawingList"
:current-item="item"
:index="index"
:active-id="activeId"
:form-conf="formConf"
@activeItem="activeFormItem"
@copyItem="drawingItemCopy"
@deleteItem="drawingItemDelete"
/>
</draggable>
<div v-show="!drawingList.length" class="empty-info">
从左侧拖入或点选组件进行表单设计
</div>
</el-form>
</el-row>
</el-scrollbar>
这里主要的重点在于 draggable-item
组件的实现
draggable-item
组件仅有 scripts 部分,它没有使用 template,而是使用了 vue 的 render 函数替代 template
html
<!-- src\views\index\DraggableItem.vue -->
<script>
...
export default {
components: {
render,
draggable
},
...
render(h) {
const layout = layouts[this.currentItem.__config__.layout]
if (layout) {
return layout.call(this, h, this.currentItem, this.index, this.drawingList)
}
return layoutIsNotFound.call(this)
}
}
</script>
布局这里就不细讲了,与前面渲染的区别就是渲染时返回的是字符串,而这里直接返回的是 JSX;同时,在外层多加了几个按钮(复制、删除)
js
// src\views\index\DraggableItem.vue
const layouts = {
colFormItem(h, currentItem, index, list) {
...
return (
<el-col>
<el-form-item>
<render
key={config.renderKey}
conf={currentItem}
onInput={(event) => {
this.$set(config, "defaultValue", event);
}}
>
{child}
</render>
</el-form-item>
{components.itemBtns.apply(this, arguments)}
</el-col>
);
},
...
};
很显然,里面有个很突兀的 render 组件,它是渲染输入型组件、选择型组件的关键
js
// src\components\render\render.js
export default {
props: {
conf: {
type: Object,
required: true,
},
},
render(h) {
/**
* 先整个默认属性对象
* 参考链接 4
*/
const dataObject = makeDataObject();
// 拷贝一份 json DSL
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可以识别的 "数据对象"
* 同样参考链接 4
*/
buildDataObject.call(this, confClone, dataObject);
/**
* 调用 vue 的 h 函数(render)
*/
return h(this.conf.__config__.tag, dataObject, children);
},
};
就是调用 vue 的 render 函数,也即代码中的 h 函数,要做的只是将 json DSL 转换为 render 函数能识别的对象
转换也很简单,甚至没有解析,只有二十几行代码
篇幅有限,可自行查阅 vue2 文档关于渲染函数的部分(参考链接 4)
配置面板
这一块内容就真的没啥好说的了,所有配置项都写死在 Rightpanel.vue
组件中,通过 v-if
决定该展示哪个配置项
html
<!-- src\views\index\RightPanel.vue -->
<!-- 截取部分 -->
<el-form-item v-if="activeData.__vModel__!==undefined" label="字段名">
<el-input
v-model="activeData.__vModel__"
placeholder="请输入字段名(v-model)"
/>
</el-form-item>
<el-form-item
v-if="activeData.__config__.componentName!==undefined"
label="组件名"
>
{{ activeData.__config__.componentName }}
</el-form-item>
<el-form-item v-if="activeData.__config__.label!==undefined" label="标题">
<el-input
v-model="activeData.__config__.label"
placeholder="请输入标题"
@input="changeRenderKey"
/>
</el-form-item>
如何与渲染面板通信呢?以下是可视化配置页面引用的 RightPanel 组件代码
html
<!-- src\views\index\Home.vue -->
<right-panel
:active-data="activeData"
:form-conf="formConf"
:show-field="!!drawingList.length"
@tag-change="tagChange"
@fetch-data="fetchData"
/>
其中 tag-change
用于监听组件类型的切换(输入型组件与选择型组件间地切换)
fetch-data
用于监听 级联组件、表格组件动态化数据的接口配置更改
其余数据通过 activeData
、formConf
两个 prop 传递
将 formConf、activeData 对象中的属性直接作为值绑定
html
<!-- src\views\index\RightPanel.vue -->
...
<el-form-item label="表单尺寸">
<el-radio-group v-model="formConf.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>
...
直接修改属性,父级的 formConf、activeData 也会被修改
生成 JSON
JSON DSL
就是 drawingList 与 formConf 的组合
总结
大概是考虑到要做出码的缘故,在 出码 与 配置 所用的页面渲染方式是不一样的
出码预览时使用 new Vue 实例的方式;可视化配置时,中间面板渲染表单时巧妙地使用了 JSX + render 函数的方式。在执行效率上,明显是后者更胜一筹
But,该项目仅适用于 Vue2 + elementUI 的场景。
题外话
vuedraggable 的源码地址是这个:github.com/SortableJS/...
你上 github 搜,排名前面的都是假的,只是使用了 vuedraggable 的同名 demo 项目,居然还能有几百个 star,属实是羡慕了