文章目录
低代码开发
前言
我的前端页面比较简单,多为表单类的小工具,所以研究了一下开源的表单设计组件。
例如:
- form-generator 表单设计器:https://mrhj.gitee.io/form-generator/#/
- form-create 表单设计器:https://www.form-create.com/designer/?fr=home
form-create,他的官方示例,支持生成json和导入json。
考虑到集成到项目后,也可以使用官方demo进行解析json,因此采用form-create。
form-create使用
安装和引入
文档:https://www.form-create.com/v2/guide/
根据文档进行安装和引入
npm i @form-create/element-ui
在main.js里引入
import Vue from 'vue'
import formCreate from '@form-create/iview'
Vue.use(formCreate)
低代码之事件注入
本次实践的低代码,写好一个框架,通过json配置实现表单的生成、提交等操作,期间涉及到的change、click事件也通过执行自定义脚本完成。
低代码时,最需要解决的是如何执行脚本,脚本格式是什么。
经过一些非开源平台的体验,查看他们请求的响应结果,小有思路后付诸实践,目前有两个方案,但pass了一个。
下面小小的记录一下吧。
第一种,也是被pass掉的一种:
- 由于对开源组件的不熟悉,想不修改他们生成的json,新建一个对象记录自定义的方法;
- 方法格式:分为方法名、方法入参和方法体,通过new Function(x,x,x)来创建;
- 创建完不知道放哪,浅浅的挂载到了vue上;(此操作影响巨大)
- 发现自定义的方法,没有办法和组件对应起来,还得处理重名问题;
- 到此,另寻出路
第二种,利用事件注入:
- 事件注入,利用inject对象传入自定义数据;将自定义的脚本放在这里,就可以通过eval执行,解决了change和click等事件;
- 事件注入要自定义事件前缀,可以统一一下
设计方案
在了解官方文档后,我的设计为:
事件名 | 说明 | 组件返回参数 | 自定义脚本入参 |
---|---|---|---|
submit | 表单提交事件 | formData, fapi | formData |
gs-change | change事件 | inject, val | formData,val,inject |
submit事件详细解释
- 组件事件submit,开源组件给的返回值有(formData, fapi);
- 固定方法onSubmit,思考用户可能需要的数据,目前给脚本的入参为(formData);
- 内部使用eval 执行用户的自定义脚本;
- 脚本位置 options.submitEvent,默认值为(formData) => {};
submit事件的json示例:
{
"formData": {},
"rule": [],
"options": {
"form": {
"labelPosition": "left",
"size": "mini",
"labelWidth": "125px",
"hideRequiredAsterisk": false,
"showMessage": true,
"inlineMessage": false
},
"submitBtn": true,
"resetBtn": false,
"submitEvent": "(formData) => {\n // 在此编写代码 \n alert(JSON.stringify(formData)) \n}"
}
}
gs-change事件详细解释
- 自定义事件gs-change,开源组件给的返回值有(inject, val);
- 固定方法onChange,思考用户可能需要的数据,目前给脚本的入参为(formData, val, inject);
- 内部使用eval 执行用户的自定义脚本;
- 脚本位置 inject[0],默认值为(formData) => {};
gs-change事件的json示例:
"emitPrefix": "gs",
"emit": [
{
"name": "change",
"inject": [
"(formData) => {\n // 在此编写代码}"
]
}
]
change注入实践
vue示例
<template>
<div class="app-container">
<form-create
v-model="egData.formData"
:value.sync="egData.formData"
:rule="egData.rule"
:option="egData.options"
@submit="onSubmit"
@gs-change="onChange"
@gs-blur="onBlur"
/>
</div>
</template>
<script>
import axios from 'axios'
export default {
data() {
return {
egData: {
// 表单默认值,也作为双向绑定的表单数据
formData: {},
rule: [],
options: {}
}
}
},
created() {
this.getConfig()
},
methods: {
getConfig() {
axios.get('/json/form-create/eg.json').then(res => {
this.egData = res.data
})
},
onSubmit(formData, fapi) {
console.log(fapi)
// eslint-disable-next-line no-eval
const func = eval(fapi.options.submitEvent)
func(formData)
},
onChange(inject, val) {
console.log(inject)
// 具体脚本通过inject指定,格式为 "inject": ["(data, formData)=>{// 自定义}"]
// eslint-disable-next-line no-eval
const func = eval(inject.inject[0])
func(inject.$f.form, inject.self.value, inject)
},
onBlur(inject) {
console.log(inject)
// eslint-disable-next-line no-eval
const func = eval(inject.inject[0])
func(inject.$f.form, inject.self.value, inject)
}
}
}
</script>
<style scoped>
</style>
eg.json全部数据:
{
"formData": {},
"rule": [
{
"type": "FcRow",
"children": [
{
"type": "col",
"props": {
"span": 24,
"offset": 0,
"push": 0,
"pull": 0
},
"children": [
{
"type": "input",
"field": "f1",
"title": "change事件",
"info": "",
"_fc_drag_tag": "input",
"hidden": false,
"display": true,
"props": {
"type": "text",
"maxlength": 20,
"minlength": 1,
"showWordLimit": true,
"clearable": true
},
"$required": "测试必填项",
"validate": [
{
"trigger": "change",
"mode": "required",
"message": "",
"required": true
}
],
"emitPrefix": "gs",
"emit": [
{
"name": "change",
"inject": [
"(formData, data) => {\n // 在此编写代码\n alert('change已输入:' + data)\n}"
]
}
]
}
],
"_fc_drag_tag": "col",
"hidden": false,
"display": true
}
],
"_fc_drag_tag": "row",
"hidden": false,
"display": true
}
],
"options": {
"form": {
"labelPosition": "left",
"size": "mini",
"labelWidth": "125px",
"hideRequiredAsterisk": false,
"showMessage": true,
"inlineMessage": false
},
"submitBtn": true,
"resetBtn": false,
"submitEvent": "(formData) => {\n // 在此编写代码 \n alert(JSON.stringify(formData)) \n}"
}
}
form-create-designer使用
安装和引入
文档:https://designer.form-create.com/guide/
根据文档进行安装和引入
npm i @form-create/designer
在main.js里引入
import FcDesigner from '@form-create/designer'
Vue.use(FcDesigner)
使用
<fc-designer ref="designer"/>
官方提供的API,常用到的有getRule、getOption、setRule、setOption,基本这四个就可以实现基础的表单;
当然,有更高级的需求,可以去查看文档。
// 拖拽好后生成json
this.$refs.designer.getRule()
this.$refs.designer.getOption()
// 根据json生成表单
this.$refs.designer.setRule()
this.$refs.designer.setOption()
绑定事件
本次主要想解决表单组件在线绑定事件,使用过一些可体验的低代码平台,最理想的体验方式是把绑定事件放在组件配置里,但是看完一圈API并没有发现可以修改。
后来便试了一下自定义按钮,插槽也并没有返回数据,无法做到组件和事件的一一对应了。
没事,坚信一定可以解决,便打印了this.$refs.designer,发现在选中组件后,输出结果里面有一个_self对象,包含了当前选中组件的rule。
这意味着,组件和事件可以一一对应上了,那后续的事件注入便没有难度了。
效果图
由于自定义了事件,组件内置的预览功能便无法满足了,自行开发一个预览。
代码
<gen-func :active-rule="activeRule" @success="setEvent" />
setEvent(obj) {
// 用户手动输入事件脚本后,回传到最终数据里
var rule = this.$refs.designer.getRule()
var activeRule = this.$refs.designer._self.activeRule
// 遍历找到当前选中的表单组件,赋值
var firstElement = rule[0]
if (firstElement.type === 'FcRow') {
var cols = firstElement.children
cols.forEach(col => {
var formItem = col.children
if (formItem.field === activeRule.field) {
formItem.emitPrefix = 'gs'
formItem.emit = []
formItem.emit.push(obj)
}
})
} else {
rule.forEach(formItem => {
if (formItem.field === activeRule.field) {
formItem.emitPrefix = 'gs'
formItem.emit = []
formItem.emit.push(obj)
}
})
}
this.$refs.designer.setRule(rule)
this.msgSuccess('添加成功')
}
genFunc.vue,代码编辑器用的vue-codemirror(ps:据说最新版仅支持vue3上,vue2可以下载4.x版本:
npm i vue-codemirror@4.x --save)
<template>
<div class="app-container">
<el-form ref="form" class="form" :model="formData" label-position="left" label-width="68px">
<el-form-item label="事件">
<el-select v-model="formData.name" style="width: 100%">
<el-option
v-for="item in eventList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
<codemirror
ref="myCodeMirror"
v-model="code"
:options="cmOptions"
/>
<div class="el-dialog__footer dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
</div>
</div>
</template>
<script>
import { codemirror } from 'vue-codemirror'
// 组件样式
import 'codemirror/lib/codemirror.css'
// 主题
import 'codemirror/theme/eclipse.css'
// 语言模式
import 'codemirror/mode/javascript/javascript.js'
import 'codemirror/mode/css/css.js'
import 'codemirror/mode/xml/xml.js'
// 代码展开折叠
import 'codemirror/addon/fold/foldcode.js'
import 'codemirror/addon/fold/foldgutter.js'
import 'codemirror/addon/fold/foldgutter.css'
import 'codemirror/addon/fold/brace-fold.js'
export default {
components: { codemirror },
props: {
activeRule: {
type: Object,
required: true
}
},
data() {
return {
eventList: [
{ label: 'blur', value: 'blur' },
{ label: 'change', value: 'change' },
{ label: 'click', value: 'click' }
],
cmOptions: {
mode: 'text/javascript', // 实现javascript代码高亮
tabSize: 4, // tab的空格宽度
styleActiveLine: true, // 设置光标所在行高亮true/false
lineNumbers: true, // 显示行号
theme: 'eclipse', // 设置主题cobalt/monokai
json: true,
readOnly: false, // 设置为只读true/false;也可设置为"nocursor"失去焦点
lineWrapping: false,
foldGutter: true,
gutters: [
'CodeMirror-lint-markers', // 代码错误检测
'CodeMirror-linenumbers',
'CodeMirror-foldgutter' // 展开折叠
]
},
code: '(formData) => {\n // 在此编写代码\n}',
formData: {
name: 'change',
inject: []
}
}
},
mounted() {
var emit = this.activeRule.emit
if (emit) {
this.formData.name = emit[0].name
this.code = emit[0].inject[0]
}
},
methods: {
submitForm() {
this.formData.inject[0] = this.code
this.$emit('success', this.formData)
}
}
}
</script>
<style scoped>
</style>